美文网首页android进阶指南android
Android性能优化盘点 - 布局优化

Android性能优化盘点 - 布局优化

作者: Stan_Z | 来源:发表于2020-03-22 23:56 被阅读0次

    继上一篇卡顿优化后,开始盘点卡顿/丢帧的第一个小分支:布局优化。还是老规矩,先列大纲:

    布局优化盘点大纲
    一、基础知识
    1.1 布局加载流程
    布局加载流程简单示意图
    1.2 布局绘制相关流程
    触发addView流程:
    performTraversals流程:
    measure、layout、draw流程:

    注:图片来源于工匠若水
    二、优化工具

    首先简单介绍下绘制优化相关的工具,这里systrace和traceView依然好使,按绘制流程阶段发现绘制耗时函数。这部分同卡顿篇原理一致就不赘述了。

    2.1 Lint

    静态代码检测工具,通过对代码进行静态分析,可以帮助开发者发现代码质量问题和提出一些改进建议。AS中目前大概有200个左右的lint检查,当然有特殊需求的可以自定义:【我的Android进阶之旅】Android自定义Lint实践

    这里简单看下布局相关的两个检查项:

    点击Analyze的Inspect Code触发Lint检测

    2.2 show GPU overdraw & GPU rendering

    Settings/开发者选项/调试GPU过度绘制

    Settings/开发者选项/HWUI呈现模式分析

    1)在屏幕上显示为条形图:

    2)adb shell dumpsys gfxinfo

    2.3 Layout Inspector

    AS: Tools > Android > Layout Inspector 选择对应进程

    左侧看视图层级结构,右侧看具体属性和赋值内容。

    三、监控
    3.1 布局整体耗时监控:

    可以使用AspectJ做面向aop的非侵入性的监控。

    工程主gradle:

    classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0’
    

    项目gradle:

    apply plugin: 'android-aspectjx’
    
    implementation 'org.aspectj:aspectjrt:1.8.+’
    

    针对Activity.setContentView监控简单示例:

    @Aspect
    public class PerformanceAop {
        public static final String TAG = "aop";
       @Around("execution(* android.app.Activity.setContentView(..))")
        public void getSetContentViewTime(ProceedingJoinPoint joinPoint) {
            Signature signature = joinPoint.getSignature();
           String name = signature.toShortString();
           long time = System.currentTimeMillis();
           try {
                joinPoint.proceed();
           } catch (Throwable throwable) {
                throwable.printStackTrace();
           }
            Log.i(TAG, name + " cost " + (System.currentTimeMillis() - time));
       }
    }
    
    3.2 单个视图创建耗时监控:

    Factory2、Factory本质上他俩就是创建View的一个hook,可以通过这个回调来监控单个View创建耗时情况。

    注:Factory2继承自Factory,Factory2比Factory的onCreateView方法多一个parent的参数,即当前创建View的父View。

    简单示例:

    LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
       @Nullable
       @Override
       public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
           //1.配合getDelegate().createView来做高版本控件的兼容适配。
           //2.单个View创建耗时统计。
           long time = System.currentTimeMillis();
           View view = getDelegate().createView(parent, name, context, attrs);
           Log.i("TAG", name + "  cost: " + (System.currentTimeMillis() - time));
           return view;
       }
    
       @Nullable
       @Override
       public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
           return null;
       }
    });
    

    这里有一点要注意:setFactory2必须在super.onCreate(savedInstanceState)之前,不然会报如下错误:

    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.stan.topnews/com.stan.topnews.app.MainActivity}: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater
            at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3314)
            at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3453)
    

    打印结果:

    2020-03-11 16:43:07.389 17078-17078/com.stan.topnews I/Perf: Connecting to perf service.
    2020-03-11 16:43:07.567 17078-17078/com.stan.topnews I/perf: LinearLayout  cost: 13
    2020-03-11 16:43:07.569 17078-17078/com.stan.topnews I/perf: ViewStub  cost: 0
    2020-03-11 16:43:07.634 17078-17078/com.stan.topnews I/perf: TextView  cost: 16
    2020-03-11 16:43:07.637 17078-17078/com.stan.topnews I/perf: TextView  cost: 3
    ...
    
    3.3 布局绘制监控

    这里用到的还是FPS,就监控一个doFrame。

    简单示例:

    private long mStartFrameTime = 0;
    private int mFrameCount = 0;
    /**
    * 单次计算FPS使用160毫秒
    */
    private static final long MONITOR_INTERVAL = 160L;
    private static final long MONITOR_INTERVAL_NANOS = MONITOR_INTERVAL * 1000L * 1000L;
    /**
    * 设置计算fps的单位时间间隔1000ms,即fps/s
    */
    private static final long MAX_INTERVAL = 1000L;
    private void getFPS() {
       if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
           return;
       }
    
       getWindow().getDecorView().getViewTreeObserver().addOnDrawListener(new ViewTreeObserver.OnDrawListener() {
           @Override
           public void onDraw() {
               Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
                   @Override
                   public void doFrame(long frameTimeNanos) {
                       if (mStartFrameTime == 0) {
                           mStartFrameTime = frameTimeNanos;
                       }
                       long interval = frameTimeNanos - mStartFrameTime;
                       if (interval > MONITOR_INTERVAL_NANOS) {
                           double fps = (((double) (mFrameCount * 1000L * 1000L)) / interval) * MAX_INTERVAL;
                           Log.i(TAG, "fps:" + fps);
                           mFrameCount = 0;
                           mStartFrameTime = 0;
                       } else {
                           ++mFrameCount;
                       }
                   }
               });
           }
       });
    }
    

    FPS相关成熟三方库:

    matrix微信的卡顿检测方案,采用的ASM插桩的方式,支持fps和堆栈获取的定位,但是需要自己根据asm插桩的方法id来自己分析堆栈,定位精确度高,性能消耗小,比较可惜的是目前没有界面展示,对代码有一定的侵入性。如果线上使用可以考虑。

    fpsviewer 利用Choreographer.FrameCallback来监控卡顿和Fps的计算,异步线程进行周期采样,当前的帧耗时超过自定义的阈值时,将帧进行分析保存,不影响正常流程的进行,待需要的时候进行展示,定位。

    四、布局加载优化

    前面简单了解了布局加载流程,

    性能瓶颈在于LayoutInflater.inflater过程,主要包括如下两点:

    • xmlPullParser IO操作,布局越复杂,IO耗时越长。
    • createView 反射,View越多,反射调用次数越多,耗时越长,但是这必须达到一定量级才会有明显影响。Java反射到底慢在哪?

    那么很容易想到两个解决办法:要么把IO和反射交由子线程来处理,要么通过动态加载视图把IO和反射规避掉。那么市面上有没有相关的成熟方案呢?当然是有的,下面来简单看一看:

    AsyncLayoutInflater

    AsyncLayoutInflater是google提供的方案,让LayoutInflater.inflater过程通过子线程来做:

         new AsyncLayoutInflater(AsyncLayoutActivity.this)
                   .inflate(R.layout.async_layout, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
                       @Override
                       public void onInflateFinished(View view, int resid, ViewGroup parent) {
                           setContentView(view);
                       }
                   });
    

    实现也很简单:handle+thread+queue+inflater。可以理解为具有loop能力的子线程来实现的耗时部分异步处理。

    这里有两点局限性:

    • 不能设置LayoutInflater.Factory/Factory2
    • 线程安全问题

    详细源码分析和自定义AsyncLayoutInflater解决局限性问题可以参考如下文章,我就不重复造轮子了:
    Android AsyncLayoutInflater 源码解析
    Android AsyncLayoutInflater 限制及改进

    X2C

    动态加载视图,这样能避免IO和反射,但是这样缺点是可读性差、可维护性差,因此掌阅团队开发的X2C做了鱼和熊掌都兼得的方案:X2C,它原理是采用APT(Annotation Processor Tool)+ JavaPoet技术来完成编译期间视图xml布局生成java代码,这样布局依然是用xml来写,编译期X2C会将xml转化为动态加载视图的java代码。

    这里个人理解可能存在的局限性:

    • 失去系统兼容AppCompat
    • 是不是能全面支持所有布局属性及自定义属性
    • 如果视图全部用X2C来处理,会造成代码冗余。
    五、布局绘制优化

    这部分是由ViewRootImpl触发的performTraversals,它主要包含:measure(确定ViewGroup以及View的大小) layout(ViewGroup决定View的摆放位置) draw(绘制视图)三个部分。另外,绘制好的DisplayListOp tree最终需要经过OpenGL命令转换交由GPU渲染,如果同一个像素点被多次重复绘制,势必也是造成浪费以及GPU任务变重。

    因此布局绘制最终优化方向就是如下两个:

    5.1 优化布局层级及其复杂度

    measure、layout、draw这三个过程都包含的自顶向下的view tree遍历耗时,它是由视图层级太深会造成耗时,另外也要避免类似RealtiveLayout嵌套造成的多次触发measure、layout的问题。最后onDraw在频繁刷新时可能多次被触发,因此onDraw不能做耗时操作,同时不能有内存抖动隐患等。

    优化思路:

    • 减少View树层级
    • 布局尽量宽而浅,避免窄而深
    • ConstraintLayout 实现几乎完全扁平化布局,同时具备RelativeLayout和LinearLayout特性,在构建复杂布局时性能更高。
    • 不嵌套使用RelativeLayout
    • 不在嵌套LinearLayout中使用weight
    • merge标签使用:减少一个根ViewGroup层级
    • ViewStub 延迟化加载标签,当布局整体被inflater,ViewStub也会被解析但是其内存占用非常低,它在使用前是作为占位符存在,对ViewStub的inflater操作只能进行一次,也就是只能被替换1次。
    5.2 避免过度绘制

    一个像素最好只被绘制一次。

    优化思路:

    • 去掉多余的background,减少复杂shape的使用
    • 避免层级叠加
    • 自定义View使用clipRect屏蔽被遮盖View绘制
    5.3 视图与数据绑定耗时

    由于网络请求或者复杂数据处理逻辑耗时导致与视图绑定不及时。这里可以从优化数据处理的维度来解决。

    六、Litho介绍

    Litho是 FaceBook 2017年上半年开源的声明式UI渲染框架。

    主要针对RecyclerView复杂滑动列表做了以下几点优化:

    • 视图的细粒度复用,可以减少一定程度的内存占用。
    • 异步计算布局,把测量和布局放到异步线程进行。
    • 扁平化视图,把复杂的布局拍成极致的扁平效果,优化复杂列表滑动时由布局计算导致的卡顿问题。

    这里具体实战可以了解下Litho在美团动态化方案MTFlexbox中的实践

    其他

    本篇文章对布局优化做了一个全局的简单梳理,也提供一些常规的优化思路以及目前市面上比较成熟的三方库。最终所有的优化点都需要落地到具体的技术点上,因此这里再简单例举一些个人认为值得去研究和学习的若干技术点:

    当然有更好的文章也可以推荐给我学习学习。

    相关文章

      网友评论

        本文标题:Android性能优化盘点 - 布局优化

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