Android 性能优化之界面优化

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/jun5753/article/details/93894943

前言

Android的性能优化是一个持续的过程,以发现问题、解决问题或者是组织Code Review为推动力去实施。性能优化涉及到的方面很多,比如启动优化、卡顿优化、内存优化、界面布局优化、稳定性优化、耗电优化、安装包大小优化等等。性能优化是每个开发者都需要关注的功课,本文从界面布局优化做一个总结。

在查阅大量相关资料后,对界面优化的在此做个总结。本文会介绍一下卡顿产生原因、什么是过度绘制和渲染机制,然后介绍如何定位问题和解决问题,最后会总结出在实际开发过程中的使用建议。如有不足之处,欢迎提出宝贵建议。

一、卡顿原因

一个App的用户体验好不好,是否流畅不卡顿是一个很直观的感受。导致Android卡顿场景的原因有很多,比如界面绘制、应用启动、页面跳转、事件响应等等。

这几种卡顿场景的根本原因可以分为两大类:

  • 界面绘制。主要原因是绘制的层级深、页面复杂、刷新不合理,由于这些原因导致卡顿的场景更多出现在 UI 和启动后的初始界面以及跳转到页面的绘制上。
  • 数据处理。导致这种卡顿场景的原因是数据处理量太大,一般分为三种情况,一是数据在处理 UI 线程,二是数据处理占用 CPU 高,导致主线程拿不到时间片,三是内存增加导致 GC 频繁,从而引起卡顿。

引起卡顿的原因很多,但不管怎么样的原因和场景,最终都是通过设备屏幕上显示来达到用户,归根到底就是显示有问题,所以,要解决卡顿,就要先了解 Android 系统的显示原理。

二、Android 系统的显示原理

Android 显示过程可以简单概括为:Android 应用程序把经过测量、布局、绘制后的 surface 缓存数据,通过 SurfaceFlinger把数据渲染到显示屏幕上, 通过 Android 的刷新机制来刷新数据。也就是说应用层负责绘制,系统层负责渲染,通过进程间通信把应用层需要绘制的数据传递到系统层服务,系统层服务通过刷新机制把数据更新到屏幕上。

我们都知道在 Android 的每个 View 绘制中有三个核心步骤:MeasureLayoutDraw。具体实现是从 ViewRootImp类的performTraversals() 方法开始执行,MeasureLayout都是通过递归来获取 View的大小和位置,并且以深度作为优先级,可以看出层级越深、元素越多、耗时也就越长。

三、什么是渲染机制

渲染操作通常依赖于两个核心组件:CPU与GPU。CPU负责包括Measure,Layout,Record,Execute的计算操作,GPU负责Rasterization(栅格化)操作。CPU通常存在的问题的原因是存在非必需的视图组件,它不仅仅会带来重复的计算操作,而且还会占用额外的GPU资源。
渲染问题

从上图可以得出结论:

  • CPU产生的问题:不必要的布局和失效(Layouts、Invalidations)
  • GPU产生的问题:过度绘制(overdraw)

小结: 了解渲染机制,有助于我们了解卡顿的最终原因,方便找到解决问题的方向。

为了能够使得App流畅,我们需要在每帧16ms以内处理完所有的CPU与GPU的计算,绘制,渲染等等操作。

接下来介绍什么是“过度绘制”,避免了过度绘制,界面优化也就做到了。

四、什么是过度绘制(定位问题)

过度绘制(Overdraw)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的 UI 结构里面,如果不可见的 UI 也在做绘制的操作,会导致某些像素区域被绘制了多次,同时也会浪费大量的 CPU 以及 GPU 资源。

在这里插入图片描述

在Android开发人员选项中,找到“调试GPU过度绘制”,开启显示之后,手机会显示出蓝色、绿色、红色等色块。
在这里插入图片描述

其中蓝色、绿色、红色显示的就是过度绘制的区域。

在官网的 Debug GPU Overdraw Walkthrough 说明中对过度重绘做了简单的介绍,其中屏幕上显示不同色块的具体含义如下所示:

overdraw

每个颜色的说明如下:

  • 原色:没有过度绘制(正常的绘制,只绘制了1次)
  • 蓝色:1 次过度绘制 (超过1次绘制是会显示)
  • 绿色:2 次过度绘制
  • 粉色:3 次过度绘制
  • 红色:4 次及以上过度绘制

我们优化的目标是,减少红色,看到更多的蓝色。

五、优化方法(解决问题)

优化原则:减少布局层级、减少过度绘制、布局复用

下面结合项目中的实际使用情况做的优化,同时也在Code Review后发现的做一个总结,Code Review时结合Android studio中的工具检测到的一些布局优化建议提示。(注:记一次CodeReview实例,请点击前往)

1.移除默认的 Window 背景

一般应用默认继承的主题都会有一个默认的 windowBackground ,比如默认的AppTheme主题:

   <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!--TODO:移除主题背景色,减少一次界面过度绘制-->
        <item name="android:windowBackground">@android:color/white</item>
    </style>

但是一般界面都会自己设置界面的背景颜色或者列表页则由 item 的背景来决定,所以默认的 Window 背景基本用不上,如果不移除就会导致所有界面都多 1 次绘制。

可以在应用的主题中添加如下的一行属性来移除默认的 Window 背景:

<item name="android:windowBackground">@android:color/transparent</item>
<!-- 或者 -->
<item name="android:windowBackground">@null</item>

或者在 BaseActivityonCreate() 方法中使用下面的代码移除:

getWindow().setBackgroundDrawable(null);
// 或者
getWindow().setBackgroundDrawableResource(android.R.color.transparent);

移除默认的 Window 背景的工作在项目初期做最好,因为有可能有的界面未设置背景色,这就会导致该界面显示成黑色的背景,如下图所示,如果是后期移除的,就需要检查移除默认 Window 背景之后的界面是否显示正常。

在这里插入图片描述
在这里插入图片描述
原先的系统主题是白色,移除后就变为黑色,此时渲染的颜色也变了,减少一次过度绘制。只是这时需要在子布局中添加相应的背景色即可。

2.移除XML布局文件中不必需的背景

例如在布局文件中嵌套了RecyclerView,注意在item中需要用到背景色时再考虑添加背景色,这样可以减少一次过度绘制。简单的布局出现颜色上出现了过度绘制,可以先好排查是否在xml中或者代码中调用了多余的绘制。

3.自定义控件使用 clipRect() 和 quickReject() 优化

当某些控件不可见时,如果还继续绘制更新该控件,就会导致过度绘制。但是通过 Canvas clipRect() 方法可以设置需要绘制的区域,当某个控件或者 View 的部分区域不可见时,就可以减少过度绘制。

先看一下 clipRect() 方法的说明:

Intersect the current clip with the specified rectangle, which is expressed in local coordinates.

顾名思义就是给 Canvas 设置一个裁剪区,只有在这个裁剪矩形区域内的才会被绘制,区域之外的都不绘制。

这个项目中暂时没有用到,先记录于此,后期用到了再完善使用注意细节。

可以参考一下其他人总结的使用方式:Android性能优化之渲染篇

4.使用合理高效的布局

(1)RecyclerView中的item的分割线处理

一般的写法如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingBottom="8dp"
    android:background="@color/divider_gray">

    <LinearLayout
        android:padding="@dimen/mid_dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:background="@color/white">

        <ImageView
            android:id="@+id/iv_app_icon"
            android:layout_width="40dp"
            android:layout_height="40dp"
            tools:src="@mipmap/ic_launcher"/>

        <TextView
            android:id="@+id/tv_app_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:layout_marginLeft="@dimen/mid_dp"
            android:textColor="@color/text_gray_main"
            android:textSize="@dimen/mid_sp"
            tools:text="test"/>
    </LinearLayout>

</LinearLayout>

这种改变布局实现分割线的方式虽然很快捷方便,但是存在不少问题的:

(1)加深了布局层级,和之前的布局相比多了一级

在这里插入图片描述

(2)多了 2 次过度绘制

解决方式有两种:

  1. 一种是使用 RelativeLayout 将分割线添加在 item 的布局中,但是这样会导致布局复杂度增加,同时因为 RelativeLayout 布局的两次测量,也会延长 View 测量的时间,在解决这种需求时并不是一个好的方式。
  2. 另一种是使用 RecyclerViewaddItemDecoration(ItemDecoration decor) 方法添加分割线,这种方式在你自定义好一个分割线 ItemDecoration 时是很方便的,网上有很多关于这方面的例子(如果你使用 ListView 的话,则使用 setDivider(Drawable divider) 方法)。

我们采用第二种解决方法,优化前后的对比如下:

在这里插入图片描述

优化后的布局 ImageView 和 item 背景区域均比优化前少了 2 次过度重绘,布局层级也没增加,需求也实现了。

(2)使用TextView本身的属性同时显示图片和文字
<TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/batman"
        android:drawableLeft="@drawable/batman"
        android:drawableStart="@drawable/batman"
        android:drawablePadding="5dp">
</TextView>

在界面中有图片和文字的布局时,不一定要用LinearLayout或RelativeLayout来嵌套,直接用TextVeiw也能实现,这样减少一层嵌套,也更优雅。

(3)用LinearLayout自带的分割线

分割线在App经常会用到的,使用频率高到让你惊讶。但是LinearLayout有一个属性可以帮你添加分割线。下面的例子中,LinearLayout包含2个TextView和基于他们中间的分割线。

LinearLayout自带的分割线
1.创建分割线(用shape实现)

下面是一个简单的shape divider_horizontal.xml用来当做分割线。

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <size android:width="@dimen/divider_width"/>
    <solid android:color="@color/colorPrimaryDark"/>

</shape>

2.将分割线放到布局属性divider

<?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="wrap_content"
        android:orientation="horizontal"
        android:divider="@drawable/divider_horizontal"  //添加分割线图片
        android:dividerPadding="5dp"  //设置padding
        android:showDividers="middle"> //居中显示


    <TextView android:layout_width="0dp"
              android:layout_weight="1"
              android:layout_height="wrap_content"
              android:gravity="center"
              android:text="@string/batman"/>

    <TextView android:layout_width="0dp"
              android:layout_height="wrap_content"
              android:layout_weight="1"
              android:gravity="center"
              android:text="@string/superman"/>
</LinearLayout>

上面用到了三个xml属性:

  • divider -用来定义一个drawable或者color作为分割线

  • showDividers -设置分隔线的显示位置,有四个flag,分别是:begining(开始位置),end(结束位置),middle(中间,最常见的),none(不显示,也是默认值)

  • dividerPadding -给divider添加padding

    注:RadioGroup继续自LinearLayout,同时也具有上述属性。

    <RadioGroup  
    	android:id="@+id/rgSize"  
    	android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
    	android:divider="@drawable/shape_space"  
    	android:showDividers="middle"  
    	android:orientation="horizontal" >  
    </RadioGroup>  
    

    shape_space.xml

    <?xml version="1.0" encoding="utf-8"?>  
    <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">  
      
        <solid android:color="@android:color/transparent" />  
      
        <stroke  
            android:width="0dp"  
            android:color="@android:color/transparent" />  
      
        <size  
            android:height="8dp"  
            android:width="8dp" />  
      
    </shape> 
    

    实现效果为:
    RadioGroup

六、总结

最后总结一下在实际开发中 ,在写布局界面时的建议:

  1. 使用合适的布局

    三种常见的ViewGroup的绘制速度:FrameLayout> LinerLayout> RelativeLayout

    • ConstraintLayout是一个更高性能的消灭布局层级的神器

    • RelativeLayout会让子View调用2次onMeasureLinearLayout 在有weight时,也会调用子View2次onMeasure

    • RelativeLayout的子View如果高度和RelativeLayout不同,则会引发效率问题,当子View很复杂时,这个问题会更加严重。如果可以,尽量使用padding代替margin。

    • 在不影响层级深度的情况下,使用LinearLayoutFrameLayout而不是RelativeLayout

    • RecycleView中item 一般用ConstraintLayout或直接使用控件来布局,以业务需求为准。

    • 简单布局一般用FrameLayout来布局,同时结合include、merge来使用。布局文件都要有根节点,但android中的布局嵌套过多会造成性能问题,于是在使用include嵌套的时候我们可以使用merge作为根节点,这样可以减少布局嵌套,提高显示速率。

    小结:使用布局优先级:FrameLayout>ConstraintLayout>LinearLayout>RelativeLayout,结合效率和需求实现。

  2. 尽量减少使用wrap_content,推荐使用mathch_parent或固定尺寸配合gravity="center"

    因为 在测量过程中,match_parent和固定宽高度对应EXACTLY,而wrap_content对应AT_MOST,这两者对比AT_MOST耗时较多。

  3. 在需要的地方添加渲染背景,外层不渲染,在内层需要的地方渲染。

  4. 文本控件,需要考虑文本过长时的省略策略

  5. 切图至少提供两套,xhdpixxhdpi

  6. 消除布局警告,同时删除控件中的无用属性

  7. 对于只有在某些条件下才展示出来的组件,建议使用viewStub包裹起来,include 某布局如果其根布局和引入他的父布局一致,建议使用merge包裹起来,如果你担心preview效果问题,这里完全没有必要,可以tools:showIn=""属性,这样就可以正常展示preview了。

  8. 欢迎补充。

参考资料:

1.Android性能优化之布局优化

2.Android 高效布局的几点建议

3.(译)写出高效清晰Layout布局文件的一些技巧

4.Android 过度绘制优化(推荐阅读)

5.Android开发之merge结合include优化布局

6.Android性能优化之渲染篇

7.如何优化你的布局层级结构之RelativeLayout和LinearLayout及FrameLayout性能分析

8.LinearLayout增加divider分割线

9.Android APP 性能优化的一些思考

猜你喜欢

转载自blog.csdn.net/jun5753/article/details/93894943