Android布局优化(二)优雅获取界面布局耗时

如需转载请评论或简信,并注明出处,未经允许不得转载

系列文章

目录

前言

在了解了布局加载原理之后,我们就要开始布局优化实战了,但是为了能够判断哪些页面布局耗时较长,以及正确的衡量布局优化的成果,我们先来了解一些获取界面布局耗时的方法

获取界面布局耗时

基本方式

布局的过程也就是setContentView的过程,所以想要获取布局耗时,我们最容易想到的就是下面这种方式,就是在setContentView的前后分别进行打点,计算时间差

@Override
protected void onCreate(Bundle savedInstanceState) {    
    long startTime = System.currentTimeMillis();
    setContentView(R.layout.activity_main);
    long cost = System.currentTimeMillis() - startTime;
    Log.i(TAG, "setContentView cost " + cost);
}

这种方式有两个缺点

  1. 代码侵入性比较强
  2. 如果有很多Activity需要统计,不太方便

AOP的方式

Android AOP — AspectJ的使用中我们说到了AOP相关的概念及使用,所以我们可以通过AOP的方式更优雅、侵入性更低的统计页面布局耗时,如下方法能够Hook所有的ActivitysetContentView方法

@Aspect
public class setContentViewAspect {

    @Around("execution(* android.app.Activity.setContentView(..))")
    public void hookSetContentView(ProceedingJoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        //开始处打点
        String name = signature.toShortString();
        long startTime = System.currentTimeMillis();
        try {
            joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        //结束打点,并计算耗时。
        long costTime = System.currentTimeMillis() - startTime;
        Log.i("Geekholt", "method " + name + " cost:" + costTime);
    }
}

获取控件加载耗时

有时候我们还希望监测一些自定义控件的加载耗时,那么我们要怎么做呢?

布局加载原理中我们有介绍过FactoryFactory2继承自Factory接口,在API 11(HONEYCOMB)中引入的。Factory2onCreateView(View parent, String name, Context context, AttributeSet attrs)Factory多了一个parent参数,用来存放构建出的View

系统通过Factory提供了一种hook的方法,方便开发者拦截LayoutInflater创建View的过程。应用场景包括

  1. 在XML布局中自定义标签名称;

  2. 全局替换系统控件为自定义View;

  3. 替换app中字体;

  4. 全局换肤;

  5. 获取控件加载耗时等

在API Level 26及以后,LayoutInflaterCompat.setFactory被标记为Deprecated,故我们这里使用的是LayoutInflaterCompat.setFactory2

我们可以在项目基类BaseActivitysuper.onCreate()前调用setFactory2,还可以将控件加载耗时上传到我们自己的数据后台用于控件性能检测

@Override
protected void onCreate(Bundle savedInstanceState) {
    setFactory2();
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

private void setFactory2() {
    LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            long startTime = System.currentTimeMillis();
            View view = getDelegate().createView(parent, name, context, attrs);
            long endTime = System.currentTimeMillis();
            long cost = endTime - startTime;
            Log.i("geekholt", name + "cost " + cost + "ms");
            return view;
        }

        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            return null;
        }
    });
}

运行结果如下所示

扩展

这里思考一个问题,为什么setFactory2一定要在super.onCreate()前调用呢?

//AppCompatActivity.java
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    final AppCompatDelegate delegate = getDelegate();
    //这里主要关注installViewFactory方法
    delegate.installViewFactory();
    delegate.onCreate(savedInstanceState);
    ...
    super.onCreate(savedInstanceState);
}

官方提供的AppCompatDelegate子类实现,如AppCompatDelegateImplN。帮助我们实现了AppCompat风格组件的向下兼容,利用AppCompatDelegateImplN提供的Factory2TextView等组件替换为AppCompatTextView,这样就可以使用一些新的属性,如autoSizeMinTextSize

//AppCompatDelegateImplV9.java
@Override
public void installViewFactory() {
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    if (layoutInflater.getFactory() == null) {
        LayoutInflaterCompat.setFactory2(layoutInflater, this);
    } else {
        if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
            Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                    + " so we can not install AppCompat's");
        }
    }
}

    /**
     * From {@link LayoutInflater.Factory2}.
     */
    @Override
    public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        // First let the Activity's Factory try and inflate the view
        final View view = callActivityOnCreateView(parent, name, context, attrs);
        if (view != null) {
            return view;
        }

        // If the Factory didn't handle it, let our createView() method try
        return createView(parent, name, context, attrs);
    }

上述代码可知,如果AppCompatActivity未在onCreate之前设置LayoutInflaterFactory,则AppCompatActivity会尝试设置一个Factory2,其中Factory2AppCompatDelegate的具体子类代码中实现

//LayoutInflate.java
public void setFactory2(Factory2 factory) {
    if (mFactorySet) {
        throw new IllegalStateException("A factory has already been set on this LayoutInflater");
    }
    if (factory == null) {
        throw new NullPointerException("Given factory can not be null");
    }
    mFactorySet = true;
    if (mFactory == null) {
        mFactory = mFactory2 = factory;
    } else {
        mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
    }
}

在同一个Actiivty内,setFactory2不可以重复调用,所以,如果在super.onCreate之后设置就会抛出异常

这里有一个问题不知道读者有没有注意到,既然setFactory2不能重复调用,那如果我们主动在onCreate()之前调用了setFactory2,会不会使系统的AppCompatTextView等失效呢?答案其实是不会的,我们只需要在执行自己的定制化操作之前调用 getDelegate().createView(parent, name, context, attrs)就不会有问题了,原因是什么,我想大家看过文章之后再自己翻阅一下源码应该就很容易理解了

如现在有一个全局替换Textview字体需求,我们可以这么做

LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {
    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        //调用兼容类的createView
        View view = getDelegate().createView(parent, name, context, attrs);

        if (view != null && (view instanceof TextView)) {
            //你可以在这里直接new自定义View
            //你可以在这里将系统类替换为自定义View
            
            //替换字体示例
            ((TextView) view).setTypeface(typeface);
        }

        return view;
    }
});

总结

本文介绍了如何获取布局加载耗时以及单个控件的加载耗时,同时还介绍了Factory2的使用,掌握了Factory2的原理和使用方法后,还能帮助我们做很多非常有意思的事情,本文抛砖引玉,更多的使用方法就需要大家自己手动尝试啦!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,099评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,828评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,540评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,848评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,971评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,132评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,193评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,934评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,376评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,687评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,846评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,537评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,175评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,887评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,134评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,674评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,741评论 2 351