• home > OS > Android > Develop >

    安卓性能优化:布局优化分析与调优

    Author:zhoulujun Date:

    布局优化总体来说,尽量减少布局层级和复杂度!如果同一个位置上面叠加了多个层级,该像素点就会被绘制多次。冗余的渲染会浪费了大量的GPU和CPU资源,因而增加了绘制时长,从而出现卡顿现象。

    先安利一下《Android XML/iOS-StoryBoard/HTML5 排布布局分析与对比—style的异同》,接下来主要以 《『Android性能优化手册』布局分析与调优 》作为主线修改与增补

    对于GUI的开发来说,因为页面布局造成页面卡顿、交互差,对于新手还是经常发生的。当页面设计复杂起来,层级越来越深,页面会变得越来越卡顿,这在web html 与css非常常见,在Android,因为页面毕竟不大,出现的概率少些,但是也得去避免。

    无论是浏览器对html与css 的处理还是Android原生的页面渲染,都是以层叠来实现页面的展示,在Android中,一个Activity绑定着一个Window,Window又管理着页面的根ViewGroup,然后ViewGroup中包含着View,层层包裹,就如同Photoshop中的图层:

    Android层叠布局控制

    因此如果同一个位置上面叠加了多个层级,该像素点就会被绘制多次。冗余的渲染会浪费了大量的GPU和CPU资源,因而增加了绘制时长,从而出现卡顿现象。

    而人眼与大脑之间的协作无法感知超过60fps的画面更新,也就是1秒内如果必须展示60帧,才能看起来流畅,1s=1000ms,因此,平均每16ms就要绘制一帧,如果布局层级很深,渲染时长超过16ms,就会看起来稍显卡顿。


    如何分析当前页面绘制情况

    debug调试GPU过度绘制-检测页面渲染层级

    Android系统支持我们查看页面绘制情况的功能,在手机的设置-开发者选项中有一个调试GPU绘制的开关,打开之后,会发现手机界面上出现了很多颜色区域:

    使用GPU过度绘制检测页面渲染层级显示GPU过度绘制

    GPU过度绘制一共有以下几种颜色:

    • 原色:没有过度绘制

    • 蓝色:1 次过度绘制

    • 绿色:2 次过度绘制

    • 粉色:3 次过度绘制

    • 红色:4 次及以上过度绘制

    平常开发的界面中,应该尽可能地将过度绘制控制为 2 次(绿色)及其以下

    使用Layout Inspector查看布局层级

    可以在新版本的AndroidStudio中,菜单栏的Tools里面找到Layout Inspector:

    然后选择所要分析的进程,比如选择你自己正在运行中的应用,然后跳转到你要分析的页面,确认之后会在项目目录下生成一个captuers文件夹,Layout Inspector会根据当前手机正在显示的页面生成一个文件存放在这个目录下:

    4.png

    • 左边:页面的层级,从最顶层的DecorView开始,其下所有当前页面存在的View都会显示在这里

    • 中间:当前页面的预览

    • 右边:每个View的布局属性,包括宽高、padding等等。通过Layout Inspector能清晰地看到页面的层次结构,比如说布局文件中某一处重复包裹了两层View,或者不小心在自定义ViewGroup的时候多加了一层根View,在这里都能看得出来。

    布局优化

    总体来说,尽量减少布局层级和复杂度

    1. 尽量不要嵌套使用RelativeLayout.

    2. 尽量不要在嵌套的LinearLayout中都使用weight属性.

    3. Layout的选择, 以尽量减少View树的层级为主.

    4. 去除不必要的父布局.

    5. 善用TextView的Drawable减少布局层级

    6. 如果H Viewer查看层级超过5层, 你就需要考虑优化下布局了~



    下面看具体的实施

    移除叠加的背景

    我们注册Activity时一般都会为它设置主题,主题一般都会有默认背景 windowBackground,比如下面这种:

    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="windowBackground">@color/colorPrimary<
        /item>
    </style>

    它会为我们的页面设置一个背景,但有些时候,布局文件里面的根布局也会设置一个背景,这个时候window的background用户完全看不到,实际上没有作用,这种情况下我们可以移除它的背景:

    <item name="android:windowBackground">@null</item>

    刚才是针对window的背景做了处理,同理,布局嵌套中也有可能出现这种情况,比如说一个LinearLayout里包裹了两个子View,而且这两个子View刚好占满了LinearLayout的全部空间,那LinearLayout同样就没必要设置背景了。

    合理使用布局设计

    我们平时都是用五大布局组合成页面的结构,相同的效果,可以用不同的ViewGroup来组合实现,但是:

    RelativeLayout底层会测绘两次,而LinearLayout和FrameLayout只会绘制一次(详见《 RelativeLayout和LinearLayout及FrameLayout性能分析》)。因此性能上不如LinearLayout和FrameLayout。

    RelativeLayout也有它的优点,利用它的各种相对属性可以减少我们的页面层级,所以总的来说就是:

    如果能减少页面层级可以考虑采用RelativeLayout,如果是相同层级的情况下,优先考虑采用FrameLayout和LinearLayout

    在LinearLayout中,采用它的layout_weight来为子View设置显示的比例,但是layout_weight同样会触发LinearLayout测量两遍,所以慎用

    个约束布局——ConstraintLayout,它的出现主要是为了解决布局嵌套过多的问题,以灵活的方式定位和调整小部件。它与 RelativeLayout 一样有相对的属性,但性能上比RelativeLayout更胜一筹。

    采用布局标签减少布局嵌套

    Android中提供了几种布局标签——merge、include、ViewStub 能够为我们减少很多不必要的嵌套

    merge与include用法

    merge和include 和PHP、JSP里面的非常像,也可以理解为Vue、或者react的组件。无非就是把可复用的页面布局封装成为一个组件,然后再使用的地方复用。

    include只是换了种方式包裹布局,使用merge标签可以帮我们忽略掉我们的子布局的根View,相当于直接将子布局添加到我们主布局下

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <include
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            layout="@layout/layout_item"/>
    
    </FrameLayout>

    组件模块

    <?xml version="1.0" encoding="utf-8"?>
    <merge xmlns:android="http://schemas.android.com/apk/res/android">
        <ImageView
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:background="@color/colorPrimary"/>
            android:text="Common Card"/>
    </merge>

    merge标签有很多要注意的地方:

    • merge标签必须使用在根布局(这也正是为何推荐搭配include使用的原因)

    • merge会帮我们忽略掉根View,因此根View的布局属性也全都会失效,会直接采用主布局中其父View的布局属性

    • merge标签本质上不是一个View,对它设置的任何布局属性都是没有意义的,并且在通过LayoutInflate.inflate()方法获取它的时候,第二个参数必须指定一个父容器,且第三个参数必须为true,也就是必须为merge下的视图指定一个父亲节点。

    ViewStub的用法

    ViewStub可以用来包裹布局,被包裹的布局在页面加载时是没有被加载出来的,只有调用了viewStub.inflate()或者viewStub.setVisible()时,才会被加载出来,也就是类似一种懒加载的机制,很适用于用来包裹我们的一些缺省布局,比如无网络提示、加载错误提示等等,或者一些不需要页面一启动就显示出来的View,因为这些布局不一定会展示给用户,如果全部写在layout文件里面的话,页面加载的时候无论可不可见实际上都是会加载出来的(注意区分加载和可见的概念)。

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <ViewStub
            android:id="@+id/no_net_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout="@layout/layout_no_net"/>
        <TextView
            android:id="@+id/loading_tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="正在加载中..."/>
    </FrameLayout>

    ViewStub组件

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="Check Your Internet Connection"/>
    </LinearLayout>

    其实这里都是还是GUI组件复用的思路。

    • include ViewStub:重用布局.

    • merge:解决include或自定义组合ViewGroup导致的冗余层级问题. 例如本例中的RepoItemView的布局文件实际可以用一个<merge>标签来减少一级.

    ListView优化

    1. contentView复用

    2. 引入holder来避免重复的findViewById.

    3. 分页加载


    参考文章:

    『Android性能优化手册』布局分析与调优 https://www.jianshu.com/p/809f95341695

    Android App优化之Layout怎么摆 https://www.jianshu.com/p/4943dae4c333




    转载本站文章《安卓性能优化:布局优化分析与调优》,
    请注明出处:https://www.zhoulujun.cn/html/OS/Android/AndroidDevelop/2020_0924_8614.html