美文网首页
Android启动速度优化二测量和优化View的加载时间

Android启动速度优化二测量和优化View的加载时间

作者: 罗公子Coding | 来源:发表于2020-05-19 13:04 被阅读0次

在第一篇《Android启动速度优化一启动速度测量》日志里面有提到怎样在APP启动过程中测量整个启动过程的时间,这一节记录了在启动过程中如何去测量和优化View的创建时间。
我们知道在Android中,Activity都是通过调用setContentView这个方法来给Activity设置视图。一般的开发过程是:

  • 依据UI、UX在layout目录下定义好布局的XML
  • 创建Activity
  • 在Activity的onCreate方法中调用setContentView加载布局
  • 加载刷新视图的数据(这部分可能是预加载,也可能是延迟加载)
  • 通过findViewById获取目标View,并设置用户响应事件(例如点击,滑动等)
  • 使用加载好的数据对目标View进行视觉刷新

在这个过程中,本文重点关注布局的定义和setContentView这两个事件,其他的在后面的章节进行讨论。

一、View加载时间的测量

在做优化之前我们需要先知道View加载的实际耗时是多少,加载过程中是哪些维度占用时间多,了解了时间占用情况后才能结合实际场景和业务逻辑进行加载优化。

1.1、测量setContentView的总耗时

我们知道setContentView里面会去解析布局文件,而Android的View体系是一个树状的数据结构,这个就使得系统需要通过递归的形式来解析,那如果你的布局层次越多,递归的时间就会越长。
在解析完了之后Android实际上是通过反射来实例化View的。基于java发射机制的话本身就会有一定的性能影响,如果这个时候你在自己的View的构造函数中执行过多的且耗时的操作,也是会直接影响View加载的时间。
基于以上现象,我们需要在启动过程中知道整个View加载的总时间和每一个View的创建时间

  • 侵入式打点获得总耗时
    这个比较简单,在setContentView前后打点记录时间就好
        long start = System.currentTimeMillis();
        setContentView(R.layout.activity_main);
        Log.d("SpeedTest","setContentView cost:" + (System.currentTimeMillis() - start)+ "ms");
  • AOP实现非侵入式获得总耗时
    为了能够快速且方便的集成AOP,这里提供一个集成AOP的插件,可以直接使用,简单两步就可以集成。
    https://github.com/alexluotututu/aop_plugin
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class TestViewInflate {

    @Around("call(* android.app.Activity.setContentView(..))")
    public void setContentViewTest(ProceedingJoinPoint point) {
        long start = System.currentTimeMillis();
        Signature signature = point.getSignature();
        try {
            point.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        Log.d("SpeedTest", point.getTarget().getClass().getName()
                + " setContentView total time:" + (System.currentTimeMillis() - start));
    }

}

我们这里不讨论AOP是怎样集成到项目中的,这个不是本章的重点,需要集成的话可以参考其他博客的文章。
我们可以看到以上两种方式都可以统计到setContentView的总耗时,输出结果如下:

05-19 10:39:11.061 26091 26091 D SpeedTest: com.snail.speeddemo.MainActivity setContentView total time:83
05-19 10:39:11.061 26091 26091 D SpeedTest: setContentView cost:83ms

两者的耗时统计是一致的。

  • Systrace统计总耗时
        TraceCompat.beginSection("setContentView");
        setContentView(R.layout.activity_main);
        TraceCompat.endSection();

我们抓到的trace中找到setContentView这个节点,便可以知道总的耗时是多少了


systrace获取setContentView时间 systrace获取setContentView时间

1.2、测量每个View的加载时间

在第1.1节中我们通过打点或者Systrace来获取到setContentView的总耗时,但是在优化过程中我们还是需要知道每个View的加载耗时,这个时候怎么办。结合Android的View加载机制和网上一些大神们的做法,可以通过设置LayoutInflater的factory来实现,下面是针对两种不同的父Activity的设置方法

  • 继承自AppCompatActivity的实现方法
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
            @Nullable
            @Override
            public View onCreateView(@Nullable View view, @NonNull String s, @NonNull Context context, @NonNull AttributeSet attributeSet) {
                long start = System.currentTimeMillis();
                View createdView = getDelegate().createView(view,s,context,attributeSet);
                Log.d("SpeedTest","create "+s+" cost:"+(System.currentTimeMillis() - start));

                return createdView;
            }

            @Nullable
            @Override
            public View onCreateView(@NonNull String s, @NonNull Context context, @NonNull AttributeSet attributeSet) {
                return null;
            }
        });

我们需要注意的是需要在super.onCreate之前调用这段代码,如果不是的话会出现crash的问题,因为super里面会设置factory,再重新设置的话系统会抛出以下异常

Caused by: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater

我们最终的输出结果:

05-19 11:20:26.722  6894  6894 D SpeedTest: create LinearLayout cost:3
05-19 11:20:26.722  6894  6894 D SpeedTest: create ViewStub cost:0
05-19 11:20:26.723  6894  6894 D SpeedTest: create FrameLayout cost:0
05-19 11:20:26.725  6894  6894 D SpeedTest: create androidx.appcompat.widget.ActionBarOverlayLayout cost:0
05-19 11:20:26.727  6894  6894 D SpeedTest: create androidx.appcompat.widget.ContentFrameLayout cost:0
05-19 11:20:26.728  6894  6894 D SpeedTest: create androidx.appcompat.widget.ActionBarContainer cost:0
05-19 11:20:26.731  6894  6894 D SpeedTest: create androidx.appcompat.widget.Toolbar cost:0
05-19 11:20:26.736  6894  6894 D SpeedTest: create androidx.appcompat.widget.ActionBarContextView cost:0
05-19 11:20:26.745  6894  6894 D SpeedTest: create androidx.constraintlayout.widget.ConstraintLayout cost:0
05-19 11:20:26.755  6894  6894 D SpeedTest: create TextView cost:1

这样一来我们就可以看到每个View的创建时间了

  • 继承自framework中的Activity或者FragmentActivity的设置方法
LayoutInflaterCompat.setFactory(getLayoutInflater(), new LayoutInflaterFactory() {

            @Override
            public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
                long start = System.currentTimeMillis();
                View view = null;
                try{
                    view = getLayoutInflater().createView(name,null,attrs);
                }catch (ClassNotFoundException ex){

                }
                Log.d("SpeedTest","create "+name+" cost:"+(System.currentTimeMillis() - start));

                return view;
            }
        });

同样也是可以得到每个View的加载时间

二、优化View的加载时间

通过第一节中的测量View加载方法,我们可以知道加载的总耗时和每个View的加载耗时,为我们做进一步优化加载时间提供更多的输入。

2.1、优化布局层次

布局层次会影响系统递归遍历布局的时间,因此我们在布局的时候应该要劲量采用轻量化的布局层次来实现功能。

2.1.1、 查看布局层次

我们可以使用Android Studio提供的Layout Inspector来查看布局层级


Layout Inspector
2.1.2、 ConstraintLayout VS RelativeLayout

我们在布局的时候为了实现一些特殊的布局,可能会用到LinearLayout,RelativeLayout或者ConstraintLayout。后两者都是可以减小布局层级的,但是这两位中RelativeLayout如果子View太多也是有性能问题。因此我们在布局的时候还是要做一个综合的评估。总体上ConstraintLayout的性能优于RelativeLayout,因此都会建议大家用ConstraintLayout来实现复杂布局

2.1.3、 merge标签减少布局层次

merge标签可以减少一个布局层次,使用merge需要注意以下几点

  • merge标签只能作为布局xml中的root节点
  • merge不是View,对它设置布局参数是没用的
  • 使用merge的时候如果要动态inflate布局,需要设置parent,且attachToParent参数要为true
getLayoutInflater().inflate(R.layout.activity_main,mParent,true);

  • merge标签如果需要在Android Studio中预览到视图的话需要添加parentTag
tools:parentTag="android.widget.LinearLayout"
2.1.4、 ViewStub懒加载

ViewStub的使用方法网上已经有很多了,因此我们这里不讨论怎样用。主要看一下ViewStub为什么会可以后话加载时间。

  • ViewStub是一个轻量化的View,它默认的显示状态是View.GONE,并且不会绘制任何东西。我们从它的源码里面可以看出来
    构造函数中把自己设置为不可见的
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context);

  //其他代码

        setVisibility(GONE);
        setWillNotDraw(true);
    }

  • ViewStub本身不绘制任何东西,所有加载时间很快

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(0, 0);
    }

    @Override
    public void draw(Canvas canvas) {
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
    }


可以从源码上看到,它的draw方法和dispatchDraw直接是空的,onMeasure方法也是直接设置宽度和高度为0

  • ViewStub在调用inflate加载真正的View之后自己会被替换掉
    我们可以从源码中看到有这段逻辑,因此在使用它的时候要注意这种情况
    inflate方法
    public View inflate() {
        //ViewStub和真正加载的View的parent为同一个
        final ViewParent viewParent = getParent();

        if (viewParent != null && viewParent instanceof ViewGroup) {
            if (mLayoutResource != 0) {
                final ViewGroup parent = (ViewGroup) viewParent;
                //这里会先通过LayoutInfater把ViewStub中指定的layout加载进来
                final View view = inflateViewNoAdd(parent);
                //这里就会把真正的View添加到parent里面,并且把ViewStub移除掉
                replaceSelfWithView(view, parent);

                mInflatedViewRef = new WeakReference<>(view);
                if (mInflateListener != null) {
                    mInflateListener.onInflate(this, view);
                }

                return view;
            } else {
                throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
            }
        } else {
            throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
        }
    }

replaceSelfWithView把自己干掉

    private void replaceSelfWithView(View view, ViewGroup parent) {
        final int index = parent.indexOfChild(this);
        //先把自己从parent中移除
        parent.removeViewInLayout(this);

        final ViewGroup.LayoutParams layoutParams = getLayoutParams();
        //下面的这段代码就是把真正的View添加到原来ViewStub的位置上
        if (layoutParams != null) {
            parent.addView(view, index, layoutParams);
        } else {
            parent.addView(view, index);
        }
    }

三、其他的一些关键优化项

3.1、APP主界面需要平衡包大小和启动速度的关系,劲量少使用webp

webp和png之间的差异主要在于压缩这个维度,但是在内存占用上两者是差不多的。因为压缩率的问题,导致webp在解码的时候会比png慢4~5倍的样子

3.2、不是很必要的话不要在布局中使用大量的TextView

TextView因为要处理文本排班和测量绘制的原因,导致TextView的性能比较低,主界面不是非必要的话文本内容可以做懒加载

3.3、 Lottie动画文件的加载

现在很多APP可能都会使用Lottie来实现一些动画,为了方便动画的加载,会直接在布局文件中制定动画json文件。这个也是会消耗掉一部分的加载时间,因此如果可以做懒加载的话会比较好

四、总结

启动速度和性能有话是一个看上去简单,但是做起来难,特别是持续做比较难的一件事情。需要细心持续的去做,本节罗列和记录了一些普遍常用的工具和方法。后续会有一些进阶的部分逐步呈现出来。

相关文章

网友评论

      本文标题:Android启动速度优化二测量和优化View的加载时间

      本文链接:https://www.haomeiwen.com/subject/swgjohtx.html