Android开发Android开发经验谈Android技术知识

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

2019-12-04  本文已影响0人  Geekholt

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

系列文章

目录

前言

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

获取界面布局耗时

基本方式

布局的过程也就是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的原理和使用方法后,还能帮助我们做很多非常有意思的事情,本文抛砖引玉,更多的使用方法就需要大家自己手动尝试啦!

上一篇下一篇

猜你喜欢

热点阅读