全栈开发知识库

布局优化

布局优化的核心问题就是要解决因布局渲染性能不佳而导致应用卡顿的问题。

绘制原理

Android的绘制主要是借助CPU和GPU结合刷新机制来共同完成。

  • CPU负责计算显示内容,包括Measure、Layout等操作,在UI绘制上的缺陷在于容易显示重复的视图组件,这样不仅带来重复的计算操作,而且会占用额外的GPU资源。
  • GPU负责光栅化,将UI元素绘制到屏幕上。

例如,文字首先要经过CPU换算成纹理,然后再传递给GPU进行渲染。而图片是先经过CPU计算,然后加载到内存中,最后再传给GPU进行渲染。

耗时原因

分析完布局的加载流程之后,我们发现有如下四点可能会导致布局卡顿:

  1. 首先,系统会将我们的Xml文件通过IO的方式映射的方式加载到我们的内存当中,而IO的过程可能会导致卡顿。
  2. 其次,布局加载的过程是一个反射的过程,而反射的过程也会可能会导致卡顿。
  3. 同时,这个布局的层级如果比较深,那么进行布局遍历的过程就会比较耗时。
  4. 最后,不合理的嵌套RelativeLayout布局也会导致重绘的次数过多。

优化方式

  • 去除不必要的嵌套和节点 这是最基本的一条,但也是最不好做到的一条,往往不注意的时候难免会一些嵌套等。

    • 首次不需要的节点设置为GONE或使用ViewStud.
    • 使用Relativelayout代替LinearLayout.
      平时写布局的时候要多注意,写完后可以通过Hierarchy Viewer或在手机上通过开发者选项中的显示布局边界来查看是否有不必要的嵌套。
  • 使用include
    include可以用于将布局中一些公共的部分提取出来。在需要的时候使用即可,比喻一些页面统一的loading页。
    include标签的layout属性指定所要包含的布局文件,我们也可以通过android:id或者一些其他的属性来覆盖被引入布局的根节点所对应 的属性值。
    xml <include layout="@layout/loading" android:id="@+id/loading_main" /> loading.xml内容为:
    ```xml

    <ProgressBar
        android:id="@+id/pb_loadiing"
        android:layout_width="28dip"
        android:layout_height="28dip"
        android:layout_centerInParent="true"
        android:indeterminateDrawable="@drawable/progressbar_anim_drawable" />
    
    <TextView
        android:layout_below="@id/pb_loadiing"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="10dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="loading..."
        android:textSize="15sp" />
    

    ```

  • 使用<merge>标签
    merge可以有效的解决布局的层级关系。我们通过一个例子来说明一下:
    ```xml

    <ImageView
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
    
        android:scaleType="center"
        android:src="@drawable/ic_launcher" />
    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="20dip"
        android:textSize="22sp"
        android:textColor="#990000"
        android:layout_gravity="center_horizontal|bottom"
        android:text="TEST" />
    

    ```

    我们在一个页面中显示该部分内容,运行后观察Hierarchy Viewer。
    image
    我们会发现除了我们布局最外层还会有一层FrameLayout,这是因为Activity内容视图的parent view就是一个FrameLayout,所以对于我们来说无意中已经多了一层毫无意义的布局。
    接下来merge的功能就能发挥了,修改代码如下。
    ```xml

    <ImageView
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:scaleType="center"
        android:src="@drawable/ic_launcher" />
    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="20dip"
        android:textSize="22sp"
        android:textColor="#990000"
        android:layout_gravity="center_horizontal|bottom"
        android:text="TEST" />
    

    `` 接下来我们在用Hierarchy Viewer观察就会发现完美的去掉了一层FrameLayout![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/merge_2.png) 当然上面我们使用merge是因为跟布局正好是FrameLayout并且没有backgroud和padding等这些属性。如果根本局是LinearLayout等,就没法直接使用merge了。 在include的时候很容易造成布局层级嵌套过多,结合merge`使用能有效解决这个问题。

  • 使用ViewStub
    ViewStub标签与include一样可以用来引入一个外部布局,但是Viewstub引入的布局默认不会解析与显示,宽高为0,View也为null,这样就会在解析layout时节省cpu和内存。简单的理解就是ViewStub是 include加上GONE.ViewStub常用来引入那些默认不会显示,只在特殊情况下显示的布局,如进度布局、网络失败显示的刷新布局、信息出错出现的提示布局等. ```xml

    ……
    <ViewStub
        android:id="@+id/network_unreachble"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout="@layout/network_unreachble" />
    

    在代码中通过`(ViewStub)findViewById(id)`找到`ViewStub`,使用`inflate()`展开`ViewStub`,然后得到子`View`,如下:java private View mNetErrorView;

    private void showNetError() { if (mNetErrorView != null) { mNetErrorView.setVisibility(View.VISIBLE); return; }

    ViewStub stub = (ViewStub)findViewById(R.id.network_unreachble);
    // 解析并且显示该部分,返回值就是解析后的该`View`
    mNetErrorView = stub.inflate();
    Button networkSetting = (Button)mNetErrorView.findViewById(R.id.bt_network);
    

    }

    private void showNormal() { if (mNetErrorView != null) { mNetErrorView.setVisibility(View.GONE); } } 或者也可以通过第二种方式:java View viewStub = findViewById(R.id.network_unreachble); // ViewStub被展开后的布局所替换 viewStub.setVisibility(View.VISIBLE);
    // 获取展开后的布局 mNetErrorView = findViewById(R.id.network_unreachble); ```

  • 减少不必要的Inflate
    如上一步中stub.infalte()后将该View进行记录或者是ListView中item inflate的时候。

  • 使用ConstraintLayout降低布局嵌套层级

    • 实现几乎完全扁平化的布局
    • 构建复杂布局性能更高
    • 具有RelativeLayout和LinearLayout的特性
  • 使用AsyncLayoutInflater异步加载对应的布局

    • 工作线程加载布局
    • 回调主线程
    • 节省主线程时间

    AsyncLayoutInflater是通过侧面缓解的方式去缓解布局加载过程中的卡顿,但是它依然存在一些问题:

    • 1、不能设置LayoutInflater.Factory,需要通过自定义AsyncLayoutInflater的方式解决,由于它是一个final,所以需要将代码直接拷处进行修改。
    • 2、因为是异步加载,所以需要注意在布局加载过程中不能有依赖于主线程的操作。

  • 邮箱 :charon.chui@gmail.com
  • Good Luck!
本页由 Markdown 自动渲染 · 共 628 篇笔记