UI卡顿与布局优化

一、理论基础

从生物学角度来讲:

  • 12fps大概是手动快速翻书的帧率,人眼可以明显察觉每一页之间的过渡。
  • 24fps是早期电视画面的刷新频率,人眼感知的是连续性的动作,帧与帧之间的过渡已经模糊化,但人眼与大脑依然可以感知其流畅度。
  • 60fps是人眼与大脑能感知的最大刷新频率,超过60fps的刷新频率对于人眼来说已经没有太大意义。

对于手机来说,UI流畅度优化的目标是实现60fps的刷新频率,也就是每一帧的总渲染时间不能超过1/60≈16.667ms。

正是基于此,Android手机每隔16ms发出VSYNC信号,触发对UI进行渲染。如果整个过程保证在16ms以内则会是一个流畅的画面,否则就只能等待下一个VSYNC信号,也就是在32ms内显示的都是同一个画面,帧率掉到了约30fps左右,人眼很容易察觉到卡顿。

UI卡顿原理

UI卡顿的原因是无法在16ms内完成渲染,而造成这个结果的原因一般是:UI设计或开发的过于复杂臃肿,执行了没必要的measure、layout等。

那应该从哪里入手解决呢?下面这张图可以作为很好的指导:

UI优化指导图

Android的渲染是个复杂的过程,我们大致分为两步:

  1. CPU端通过measure、layout等操作制作出display list渲染列表
  2. GPU端对display list光栅化处理

由此可见,UI绘制过程的优化分两个方向:

问题 举例 检测工具 方案
layouts & invalidations 不必要嵌套

Hierarchy Viewer

轻量化布局

Lint、Profile GPU rendering

overdraw 重复背景

去掉重复背景

ClipRect

调试GPU过度绘制

 


 

二、冗余布局

(1)Lint

关于Lint的使用方法可以参考另一篇文章:Lint

使用Lint随手检查了一下手头的项目,发现真的有布局相关问题:

"Node can be replaced by a TextView with compound drawables"是因为如下布局可以只用一个TextView代替:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:layout_gravity="center_vertical"
    android:orientation="vertical">

    <TextView
        android:id="@+id/colonTop"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="@dimen/keyguard_clock_colon_size"
        android:textColor="@color/clock_num_text"
        android:text="@string/num_colon"/>

    <ImageView
        android:id="@+id/colonBottom"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/dot"
        android:visibility="gone"/>

</LinearLayout>

"Useless parent layout"是典型的不必要嵌套造成的,下面布局中LinearLayout和FrameLayout可以去掉一层:

<?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"
    android:clickable="true"
    android:orientation="vertical"
    android:padding="20.0dip" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:background="@drawable/not_wifi_dialog_bg"
        android:orientation="vertical" >

        <TextView
            android:id="@+id/title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:layout_marginTop="25dp"
            android:text="@string/gallery_dialog_title"
            android:textColor="#ff000000"
            android:textSize="18sp" />

(2)Hierarchy Viewer

 使用HierarchyViewer工具可以更直观的识别渲染性能低的部分。

Hierarchy viewer

 

比如,下面的RelativeLayout就是不必要的:

不必要的ViewGroup

需要注意的是:

  • 红色一般是性能低可能出问题的地方,但也不是一定有问题,视业务复杂度和子view多少而定;
  • 绿色也不一定就没问题,比如上面Lint查出的第一个问题,虽然布局简单是绿色的但依然可以优化。

多个工具结合使用,才能达到更好的优化效果!


 

三、过度绘制(OverDraw)

OverDraw是个常见的UI性能问题,可以使用Lint、Profile GPU rendering和GPU调试过度绘制等工具来检测。

(1)Lint

Lint也可以检测到过度绘制

这是因为application主题和xml根元素都设置了背景造成了重复绘制, 解决方法是去掉xml背景,为application设置指定的style:

<application
    android:icon="@drawable/ic_logo"
    android:label="@string/app_name"
    android:theme="@style/MyTheme" >
<style name="MyTheme" parent="android:Theme">
    <item name="android:background">@drawable/main</item>
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowFullscreen">true</item>
</style>

(2)调试GPU过度绘制

检测过度绘制最常用、最直观的方法是打开“开发者选项”中的“调试GPU过度绘制”。

对比下图判断过度绘制的倍数,如果红色过多,则需要优化:

过度绘制

造成过度绘制的常见情况大致分为两种:

  1. 背景重复绘制
  2. 多个View互相重叠遮挡

a、背景重复

关于背景重复的问题,可对照过度绘制倍数图层层筛查布局文件,一般不难找到。如下,根布局和ListView设置了同样颜色的背景:

<?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:background="@android:color/white"
              android:orientation="vertical">

    <ListView
        android:id="@+id/id_listview_chats"
        android:layout_width="match_parent"
        android:background="@android:color/white"
        android:layout_height="wrap_content"/>
</LinearLayout>

b、clipRect和clipPath

关于多个View互相重叠遮挡,会造成重叠部分过度绘制,可以使用ClipRect或ClipPath方法裁切画布,避免绘制被遮挡部分。

clipRect

除了最后一张图片,前面的图片只需要绘制漏出的部分就可以:

  protected void onDraw(Canvas canvas)
    {

        super.onDraw(canvas);

        canvas.save();
        canvas.translate(20, 20);
        for (int i = 0; i < mCards.length; i++)
        {
            canvas.translate(120, 0);
            canvas.save();
            if (i < mCards.length - 1)
            {
               //只需要绘制120宽度的部分
                canvas.clipRect(0, 0, 120, mCards[i].getHeight());
            }
            canvas.drawBitmap(mCards[i], 0, 0, null);
            canvas.restore();
        }
        canvas.restore();

    }

四、include、merge和viewStub

(1)include

include的用法类似c、c++中的头文件,将一些需要重复使用的布局——比如多个界面的顶部布局是一样的——独立成一个xml,在使用的时候,通过<include>标签导入。

<?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:orientation="vertical" >  

    <include  
        android:id="@+id/my_title_ly"  
        android:layout_width="match_parent"  
        android:layout_height="wrap_content"  
        layout="@layout/my_title_layout" />  

    <!-- ... -->
</LinearLayout>

注意事项:使用<include>标签时,外部id会覆盖内部根元素id。比如,上面my_title_ly会覆盖my_title_layout根元素id。

(2)Merge

The tag helps eliminate redundant view groups in your view hierarchy when including one layout within another. For example, if your main layout is a vertical LinearLayout in which two consecutive views can be re-used in multiple layouts, then the re-usable layout in which you place the two views requires its own root view. However, using another LinearLayout as the root for the re-usable layout would result in a vertical LinearLayout inside a vertical LinearLayout. The nested LinearLayout serves no real purpose other than to slow down your UI performance.

merge的作用是减少include布局时的层级。比如,上面include的布局文件my_title_layout是多个子元素的合集,必然要用一个ViewGroup来组织:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    android:layout_width="fill_parent"  
    android:layout_height="fill_parent">  

    <ImageView    
        android:layout_width="fill_parent"   
        android:layout_height="fill_parent"   
        android:scaleType="center"  
        android:src="@drawable/golden_gate" />  

    <TextView  
        android:layout_width="wrap_content"   
        android:layout_height="wrap_content"   
        android:text="Golden Gate" />  

</FrameLayout> 

这就导致最终布局多了一个FrameLayout,更有效的写法是使用<merge>标签:

<merge xmlns:android="http://schemas.android.com/apk/res/android" >  

    <ImageView    
        android:layout_width="fill_parent"   
        android:layout_height="fill_parent"   
        android:scaleType="center"  
        android:src="@drawable/golden_gate" />  

    <TextView  
        android:layout_width="wrap_content"   
        android:layout_height="wrap_content"   
        android:text="Golden Gate" />  

</merge> 

实际上,<merge>标签的作用是将其内部元素直接解析到它的parent布局中,这样就少了一层ViewGroup。

(3)ViewStub

ViewStub is a lightweight view with no dimension and doesn’t draw anything or participate in the layout. As such, it’s cheap to inflate and cheap to leave in a view hierarchy. Each ViewStub simply needs to include the android:layout attribute to specify the layout to inflate.

对于一些只在特定情境下才需要显示的布局(比如,首次进入时的欢迎界面),可以使用viewStub代替,具体用法如下:

 

<ViewStub  
    android:id="@+id/stub_id"  
    android:inflatedId="@+id/stub_inflated_id"  
    android:layout="@layout/my_stub_layout"  
    android:layout_width="wrap_content"  
    android:layout_height="wrap_content"  
    android:layout_gravity="bottom" /> 

 

ViewStub实际上是一个高度为0的View,只有当调用其setVisibility或inflate函数时才会将其要装载的布局加载进来,从而实现了懒加载。

注意事项:与<include>标签类似,使用<ViewStub>标签时,inflatedId如果不为空,则会覆盖装载layout的根元素布局。

 


五、总结

UI布局优化是一个繁琐细致的工作,可以首先通过 "Profile GPU rendering" 和 "GPU调试过度绘制" 等工具做一个整体的检测,然后具体问题具体分析。

"Profile GPU rendering"用法:

profile

其中,绿线是16ms线,全部渲染低于此线才能保证界面刷新为60fps。

  • 蓝色部分记录了这一帧对所有需要更新的view完成这两步花费的时间,当它很高的时候,说明有很多view突然无效(invalidate)了,或者是有几个自定义view在onDraw函数中做了特别复杂的绘制逻辑。
  • 红色部分代表执行时间,也就是Android 2D渲染引擎(OpenGL)执行display list的时间,如果这一段比较高,复杂的view都可能是罪魁祸首。
  • 橙色部分代表处理时间,就是CPU告诉GPU渲染已经完成的时间,这个时间是阻塞的,如果这里比较高,通常是由于很多复杂的view绘制导致GPU做的任务太多了。

 

posted @ 2020-05-20 14:37  西贝雪  阅读(288)  评论(0编辑  收藏  举报