Android APP性能优化之 ---- 布局优化(一)

布局优化的核心思想是优化布局嵌套层级(层级越少,View绘制时越快)

一、Android系统屏幕UI刷新机制

首先需要明白一个概念,如果我们想要屏幕流畅的运行,就必须保证UI全部的测量、布局和绘制的时间在16ms内 为什么是16ms? 因为人眼与大脑之间的协作无法感知超过60fps的画面更新,而16ms也就是每秒刷新60fps 16ms=1000/60Hz,也就是说超过16ms用户就会感知到卡顿.

熟悉屏幕UI刷新机制,首先需要了解下刷新率和帧率:

刷新率(Refresh Rate): 指屏幕在一秒内刷新屏幕的次数,例如60HZ

帧率(Frame Rate): 指GPU在一秒内操作画面的帧数,例如30fps,60fps

在一个典型的显示系统中,一般包括CPU、GPU、display(可以理解为屏幕或显示器)三个部分,CPU负责计算数据,把计算好数据交给GPU,GPU会对图形数据进行渲染,渲染好后放到buffer里存起来,然后display负责把buffer里的数据呈现到屏幕上.

显示过程简单的说就是 CPU/GPU准备好数据,存入buffer,display每隔一段时间去buffer里取数据,然后显示出来.但是刷新频率和帧率并不是总能保持相同的节奏.

针对上述情况,Android系统中引入了VSYNC的机制(Vertical Synchronization 垂直同步,我们可以理解为帧同步),
它为了保证GPU生成帧的速度和display刷新的速度保持一致,Android系统会每16ms就会发出一次VSYNC信号触发UI渲染更新.
VSYNC最重要的作用是防止出现画面撕裂(screen tearing).所谓画面撕裂 是指一个画面上出现了两帧画面的内容(如下图).
为什么会出现这种情况呢?这种情况一般是因为显卡输出帧的速度高于显示器的刷新速度,导致显示器并不能及时处理输出的帧,
而最终出现了多个帧的画面都留在了显示器上的问题.

画面撕裂

画面撕裂也就是帧率超过刷新率情况(图一):

我们看下图一,在没有VSync的情况下屏幕刷新第二帧画面的时候 GPU生成了2 3两个画面 导致画面撕裂现象,如果引入VSYNC机制后,要求绘制只能在收到VSYNC信号之后才能进行,因此杜绝了GPU一直不停的进行绘制,帧的生成速度高于屏幕的刷新速度,导致生成的帧不能被正常显示,只能丢弃,这样就出现了丢帧的情况

其实android设备更多的情况是帧率小于刷新频率情况(图二):

我们看下图二,GPU生成帧的速度从60fps突然掉到60fps以下,就会出现某些帧显示的画面内容就会与上一帧的画面相同.这样一来,用户在两个16ms看到的是同一帧画面,因此会给用户卡顿不流畅的感受. 图一: 更多的情况是GPU生成的帧率小于刷新频率情况(图二):

帧率从60fps突然掉到60fps以下,就会出现某些帧显示的画面内容就会与上一帧的画面相同.因此给用户卡顿不流畅的感受.

二、布局的选择

首先FrameLayout能实现的优先使用FrameLayout,因为Framelayout是最简单高效的ViewGroup(为什么它是最高效的呢?最简单的办法就是我们可以通过查看它源码的行数,FrameLayout,源代码行数是最少的,代码逻辑也是最简单的)

其次优先选择RelativeLayout,因为RelativeLayout可以简单实现LinearLayout嵌套才能实现的布局(等下还会介绍一个ConstraintLayout,它可以在不嵌套布局的情况下更简单的完成更多复杂的布局)

最后是当在RelativeLayout和LinearLayout在不嵌套情况下同时能够满足需求时,优先选择LinearLayout,因为RelativeLayout功能相对复杂,同时会有重复绘制的情况

什么是重复绘制?
重复测量视图并不一定是因为错误。RelativeLayout就需要经常对它的子视图测量两次,以确
保所有子视图被放置在了正确的位置。如果 LinearLayout 的子视图设置了 layout weight属
性,那么 LinearLayou也需要测量两次以确定子视图的确切尺寸。如果是嵌套的LinearLayout
或者是RelativeLayout,测量的次数会呈指数增长(两层嵌套会进行4次测量,3层嵌套会进行
8 次测量,等等)。
  • 避免过度绘制(Overdraw)

Overdraw: 指屏幕上某一个像素点在同一帧的时间内被绘制了多次.

在多层次的UI结构中,如果不可见的UI也在做绘制的操作,就会导致某些像素区域被绘制了多次,浪费大量的CPU以及GPU资源,我们可以在手机的设置—->开发者选项—->打开"调试GPU过度绘制" 查看Overdraw过度绘制情况.

如图 通过4种颜色展示不同程度的Overdraw的情况:

暗红: overdraw 4倍.像素绘制了五次或者更多.必须得优化了
淡红: overdraw 3倍.像素绘制了四次.小范围是可以接受,可以试着去优化.
绿色: overdraw 2倍.像素绘制了三次.中等大小的绿色区域是可以接受的
蓝色: overdraw 1倍.像素绘制了两次.大片的蓝色是可以接受的
没有颜色: 没有overdraw.像素只绘制了一次

举一个导致overdraw的场景: 当我们代码中ViewPager和ViewPager中的fragment都设置了背景色,这样会导致背景色在同一个像素点上重复绘制,而针对这种情况,我们可以去掉ViewPager的背景色来避免overdraw

  • ConstraintLayout

ConstraintLayout是Android Studio 2.2中主要的新增功能之一,它可以在不嵌套任何布局的情况下构建复杂的布局.它与RelativeLayout非常相似,所有的view都依赖于相邻控件的相对关系.而ConstraintLayout比RelativeLayout更加灵活,在AndroidStudio中进行拖拽即可完成布局.

以往 我们都是通过嵌套或者使用RelativeLayout来完成复杂的布局,但是通过使用Systrace大量测试表明:嵌套式层次结构和 RelativeLayout(会对其每个子对象重复测量两次)的特性导致性能低下.因此,针对复杂的布局,我们毫无疑问优先选择ConstraintLayout.

ConstraintLayout用法介绍及ConstraintLayout和RelativeLayout测试对比性能优势

三、优化控件的使用

  • include标签

include标签可以在一个布局中引入另外一个布局.如果我们程序的所有界面都有一个公共的部分,这个时候最好的做法就是将这个公共的部分提取到一个独立的布局文件中,然后在每个界面的布局文件中来引用这个公共的布局

作用: 为了提高代码的复用性,减少代码;将布局中公共部分抽取供其他layout使用,但可能会导致多余的布局嵌套

用法如下:

    <!-- 1.定义公共部分布局: include_layout.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:id="@+id/back"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_centerVertical="true"
            android:text="Back" />
        <TextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="Title"
            android:textSize="20sp" />
        <TextView
            android:id="@+id/done"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:text="Finish" />
    </RelativeLayout>

    <!-- 2.使用include_layout.xml -->
    <?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>标签中,我们也可以覆盖layout中定义的所有属性-->
        <include
            layout="@layout/include_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    
        <TextView
            style="@style/textStyle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="HAHA" />
    </LinearLayout>

    <!--定义style-->
    <style name="textStyle">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:textSize">18sp</item>
        <item name="android:textColor">@android:color/black</item>
        <item name="android:gravity">center_vertical</item>
    </style>
 

如上代码针对style的抽取,同样也提高代码的复用性

除了抽取公共部分代码外,include还可以将我们布局代码进行模块化,也就是当我们页面逻辑非常复杂,单纯布局代码就一千多行的时候,不管是谁来维护这样的代码,看着就会头疼,这个时候我们可以将布局进行分类,使用include标签抽取分成不同模块来引入,模块化后的代码更便于提高维护的效率

  • merge标签

merge标签是include标签的辅助扩展,为了防止在引用布局文件时产生多余的布局嵌套

作用: 解决布局层级的优化,减少布局嵌套的层次,提高布局加载的效率

用法如下:

    <!--定义merge_layout.xml-->
    <?xml version="1.0" encoding="utf-8"?>
    <merge xmlns:android="http://schemas.android.com/apk/res/android">
        <!--根标签必须是merge-->
        <Button
            android:id="@+id/ok"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="OK" />
    
        <Button
            android:id="@+id/cancel"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="Cancel" />
    
    </merge>
    
    <!--使用merge_layout.xml-->
    <?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">
        <!--merge标签使用的是父布局的特性
        (也就是这里是垂直的LinearLayout,merge中两个button也就是垂直的LinearLayout属性)-->
        <include layout="@layout/merge_layout" />
    
    </LinearLayout>

使用场景为:当父布局和子布局相同时,可以利用merge标签来减少一层布局嵌套,merge标签使用的是父布局的特性

  • ViewStub标签

ViewStub只有加载该布局的时候才占用资源,INVISIBLE状态是不会绘制出来的(ViewStub虽说也是View的一种,但是它没有大小,没有绘制功能,也不参与布局,资源消耗非常低,将它放置在布局当中基本可以认为是完全不会影响性能的)

用法如下:

    <!--viewstub_layout.xml-->
    <?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">
    
        <Button
            android:id="@+id/more"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="Button2" />
    </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:orientation="vertical">
    
        <Button
            android:id="@+id/button1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="Button1" />
    
        <ViewStub
            android:id="@+id/view_stub"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout="@layout/viewstub_layout" />
    </LinearLayout>

四、原生View控件的优化:

android系统提供的一些原生控件并不是完美的,或者说结合我们实际复杂的开发环境来说,压根就不会有绝对完美的控件,为了提高我们使用控件的效率进而提升程序的质量,因此我们需要针对原生控件做一些局部优化.

  • ListView的优化

复用convertView(重用View可以减少重新分配缓存 避免内存频繁分配/回收),

使用ViewHolder(原因是findViewById方法耗时较大,如果控件个数过多,会严重影响性能,而使用ViewHolder主要是为了可以省去这个时间.通过setTag,getTag直接获取这个View),

以及数据多的情况下进行分批加载等等

  • WebView的优化

在当下的Android开发中,Webview的身影随处可见,尤其是在Hybrid App(混合模式移动应用)中,更是不可或缺,而Webview的性能却是有待改善的(手机QQ中大概有70%以上的业务都是由H5开发).

Hybrid App(混合模式移动应用)是指介于web-app、native-app这两者之间的app,兼具“Native App良好用户交互体验的优势”和“Web App跨平台开发的优势”。

全局WebView(混合模式移动应用开发中,在客户端刚启动时,就初始化一个全局的WebView待用,并隐藏;当用户访问了WebView时,直接使用这个WebView加载对应网页,并展示.这种方法可以比较有效的减少WebView在App中的首次打开时间,当用户访问页面时,不需要等待初始化WebView的时间)

客户端代理数据请求(在客户端初始化WebView的同时,直接由native开始网络请求数据;当页面初始化完成后,向native获取其代理请求的数据.这个方法虽然不能减小WebView初始化时间,但数据请求和WebView初始化可以并行进行,这样总体的页面加载时间就缩短了,这种方式是参考腾讯所分享的在手机QQ混合开发的做法)

优化网页加载速度(先设置WebView禁止加载图片,再覆写WebViewClient的onPageFinished()方法,页面加载结束后再加载图片)

还有其他各种优化的方式,不再一一列举,总结起来都是围绕两点:

1.在使用前预先初始化好WebView,从而减小耗时
2.在初始化的同时,通过Native来完成一些网络请求等过程,使得WebView初始化不是完全的阻塞后续过程

摘选自美团技术团队 -- WebView性能优化总结:

一个加载网页的过程中,native、网络、后端处理、CPU都会参与,各自都有必要的工作和依赖关系;让他们相互并行处理而不是相互阻塞才可以让网页加载更快:
    WebView初始化慢,可以在初始化同时先请求数据,让后端和网络不要闲着。
    后端处理慢,可以让服务器分trunk输出,在后端计算的同时前端也加载网络静态资源。
    脚本执行慢,就让脚本在最后运行,不阻塞页面解析。
    同时,合理的预加载、预缓存可以让加载速度的瓶颈更小。
    WebView初始化慢,就随时初始化好一个WebView待用。
    DNS和链接慢,想办法复用客户端使用的域名和链接。
    脚本执行慢,可以把框架代码拆分出来,在请求页面之前就执行好。
  • ViewPager的延迟加载

ViewPager有一个 “预加载”的机制,默认会把ViewPager当前位置的左右相邻页面预先初始化(预加载),这样设计是为了ViewPager左右滑动会更加流畅,但如果当前APP页面数量不多,并且每个页面资源占用很大,用户可能只在一个页面使用,不需要切换页面,这时,我们就没必要使用预加载来消耗手机的资源了

怎样去做到延迟加载呢?
    setOffscreenPageLimit(int limit)用来设置ViewPager预加载的数量,默认是1,
    1也就是会预加载左右相邻页面,所以我们设置的值应该小于1,但事实上设置小于1是不生效的.因此这种方式行不通.
    ViewPager中,预加载数量值的变量为DEFAULT_OFFSCREEN_PAGES,这个值是private,因此子类继承ViewPager也是行不通.

以下通过两种方式来实现延迟加载:

1.从Fragment着手,只有Fragment可见的时候才去加载数据

2.自定义一个ViewPager,把原生ViewPager代码全拷过来,修改加载数变量DEFAULT_OFFSCREEN_PAGES值为0; (我们这里使用的是低版本ViewPager代码,Android 4.0的代码,低版本的代码相对更少,逻辑相对更简单)

    // 方式一:
public class LazyFragment extends Fragment {

    private boolean mIsInit;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        mIsInit = true; // View 控件的初始化
        isLoadData();   // 这里还需满足条件1: 视图对用户可见
        return super.onCreateView(inflater, container, savedInstanceState);
    }

    /**
     * 这个方法是通知Fragment的UI是否可见,当参数isVisibleToUser为true的时候,fragment的UI是可见的,为false的时候为不可见
     */
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        isLoadData();   // 还需满足条件2: 视图已经初始化
    }

    private void isLoadData() {
        if (getUserVisibleHint()) {
            if (mIsInit) {
                // 满足以上两个条件才加载数据
            }
        }
    }
}
    // 方式二:
https://github.com/ansen360/Sample/blob/master/app/src/main/java/com/ansen/sample/LazyViewPager.java

4. 其他优化点:

  • 删除控件中无用属性

  • 减少不必要的infalte

  • 布局上的优化。移除 XML 中非必须的背景,移除 Window 默认的背景、按需显示占位背景图片

  • 自定义View优化.避免onDraw方法声明太多变量,使用canvas.clipRect()来帮助系统识别那些可见的区域,只有在这个区域内才会被绘制


相关链接直达:

Android APP性能优化之 ---- 布局优化(一)

Android APP性能优化之 ---- 内存优化(二)

Android APP性能优化之 ---- 代码优化(三)

Android APP性能优化之 ---- 优化监测工具(四)

Android APP性能优化之 ---- APK瘦身 App启动优化

Android内存泄露OOM异常处理优化

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章