Android的性能优化是一个持续的过程,以发现问题、解决问题或者是组织Code Review为推动力去实施。性能优化涉及到的方面很多,比如启动优化、卡顿优化、内存优化、界面布局优化、稳定性优化、耗电优化、安装包大小优化等等。性能优化是每个开发者都需要关注的功课,本文从界面布局优化做一个总结。
在查阅大量相关资料后,对界面优化的在此做个总结。本文会介绍一下卡顿产生原因、什么是过度绘制和渲染机制,然后介绍如何定位问题和解决问题,最后会总结出在实际开发过程中的使用建议。如有不足之处,欢迎提出宝贵建议。
一个App的用户体验好不好,是否流畅不卡顿是一个很直观的感受。导致Android卡顿场景的原因有很多,比如界面绘制、应用启动、页面跳转、事件响应等等。
这几种卡顿场景的根本原因可以分为两大类:
引起卡顿的原因很多,但不管怎么样的原因和场景,最终都是通过设备屏幕上显示来达到用户,归根到底就是显示有问题,所以,要解决卡顿,就要先了解 Android 系统的显示原理。
Android 显示过程可以简单概括为:Android 应用程序把经过测量、布局、绘制后的 surface 缓存数据,通过 SurfaceFlinger
把数据渲染到显示屏幕上, 通过 Android 的刷新机制来刷新数据。也就是说应用层负责绘制,系统层负责渲染,通过进程间通信把应用层需要绘制的数据传递到系统层服务,系统层服务通过刷新机制把数据更新到屏幕上。
我们都知道在 Android 的每个 View 绘制中有三个核心步骤:Measure
、Layout
、Draw
。具体实现是从 ViewRootImp
类的performTraversals()
方法开始执行,Measure
和 Layout
都是通过递归来获取 View
的大小和位置,并且以深度作为优先级,可以看出层级越深、元素越多、耗时也就越长。
渲染操作通常依赖于两个核心组件:CPU与GPU。CPU负责包括Measure,Layout,Record,Execute的计算操作,GPU负责Rasterization(栅格化)操作。CPU通常存在的问题的原因是存在非必需的视图组件,它不仅仅会带来重复的计算操作,而且还会占用额外的GPU资源。
从上图可以得出结论:
小结: 了解渲染机制,有助于我们了解卡顿的最终原因,方便找到解决问题的方向。
为了能够使得App流畅,我们需要在每帧16ms以内处理完所有的CPU与GPU的计算,绘制,渲染等等操作。
接下来介绍什么是“过度绘制”,避免了过度绘制,界面优化也就做到了。
过度绘制(Overdraw)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的 UI 结构里面,如果不可见的 UI 也在做绘制的操作,会导致某些像素区域被绘制了多次,同时也会浪费大量的 CPU 以及 GPU 资源。
在Android开发人员选项中,找到“调试GPU过度绘制”,开启显示之后,手机会显示出蓝色、绿色、红色等色块。
其中蓝色、绿色、红色显示的就是过度绘制的区域。
在官网的 Debug GPU Overdraw Walkthrough 说明中对过度重绘做了简单的介绍,其中屏幕上显示不同色块的具体含义如下所示:
每个颜色的说明如下:
我们优化的目标是,减少红色,看到更多的蓝色。
优化原则:减少布局层级、减少过度绘制、布局复用
下面结合项目中的实际使用情况做的优化,同时也在Code Review后发现的做一个总结,Code Review时结合Android studio中的工具检测到的一些布局优化建议提示。(注:记一次CodeReview实例,请点击前往)
一般应用默认继承的主题都会有一个默认的 windowBackground
,比如默认的AppTheme
主题:
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
--TODO:移除主题背景色,减少一次界面过度绘制-->
- "android:windowBackground">@android:color/white
style>
但是一般界面都会自己设置界面的背景颜色或者列表页则由 item 的背景来决定,所以默认的 Window 背景基本用不上,如果不移除就会导致所有界面都多 1 次绘制。
可以在应用的主题中添加如下的一行属性来移除默认的 Window 背景:
<item name="android:windowBackground">@android:color/transparentitem>
<item name="android:windowBackground">@nullitem>
或者在 BaseActivity
的 onCreate()
方法中使用下面的代码移除:
getWindow().setBackgroundDrawable(null);
// 或者
getWindow().setBackgroundDrawableResource(android.R.color.transparent);
移除默认的 Window 背景的工作在项目初期做最好,因为有可能有的界面未设置背景色,这就会导致该界面显示成黑色的背景,如下图所示,如果是后期移除的,就需要检查移除默认 Window 背景之后的界面是否显示正常。
原先的系统主题是白色,移除后就变为黑色,此时渲染的颜色也变了,减少一次过度绘制。只是这时需要在子布局中添加相应的背景色即可。
例如在布局文件中嵌套了RecyclerView,注意在item中需要用到背景色时再考虑添加背景色,这样可以减少一次过度绘制。简单的布局出现颜色上出现了过度绘制,可以先好排查是否在xml中或者代码中调用了多余的绘制。
当某些控件不可见时,如果还继续绘制更新该控件,就会导致过度绘制。但是通过 Canvas clipRect()
方法可以设置需要绘制的区域,当某个控件或者 View 的部分区域不可见时,就可以减少过度绘制。
先看一下 clipRect()
方法的说明:
Intersect the current clip with the specified rectangle, which is expressed in local coordinates.
顾名思义就是给 Canvas 设置一个裁剪区,只有在这个裁剪矩形区域内的才会被绘制,区域之外的都不绘制。
这个项目中暂时没有用到,先记录于此,后期用到了再完善使用注意细节。
可以参考一下其他人总结的使用方式:Android性能优化之渲染篇
一般的写法如下:
<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 次过度绘制
解决方式有两种:
RelativeLayout
将分割线添加在 item 的布局中,但是这样会导致布局复杂度增加,同时因为 RelativeLayout
布局的两次测量,也会延长 View 测量的时间,在解决这种需求时并不是一个好的方式。RecyclerView
的 addItemDecoration(ItemDecoration decor)
方法添加分割线,这种方式在你自定义好一个分割线 ItemDecoration
时是很方便的,网上有很多关于这方面的例子(如果你使用 ListView 的话,则使用 setDivider(Drawable divider)
方法)。我们采用第二种解决方法,优化前后的对比如下:
优化后的布局 ImageView 和 item 背景区域均比优化前少了 2 次过度重绘,布局层级也没增加,需求也实现了。
<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也能实现,这样减少一层嵌套,也更优雅。
分割线在App经常会用到的,使用频率高到让你惊讶。但是LinearLayout有一个属性可以帮你添加分割线。下面的例子中,LinearLayout包含2个TextView和基于他们中间的分割线。
下面是一个简单的shape divider_horizontal.xml用来当做分割线。
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size android:width="@dimen/divider_width"/>
<solid android:color="@color/colorPrimaryDark"/>
shape>
2.将分割线放到布局属性divider
中
//居中显示
<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
中
<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>
最后总结一下在实际开发中 ,在写布局界面时的建议:
使用合适的布局
三种常见的ViewGroup
的绘制速度:FrameLayout
> LinerLayout
> RelativeLayout
。
ConstraintLayout
是一个更高性能的消灭布局层级的神器
RelativeLayout
会让子View调用2次onMeasure
,LinearLayout
在有weight时,也会调用子View
2次onMeasure
RelativeLayout
的子View如果高度和RelativeLayout
不同,则会引发效率问题,当子View很复杂时,这个问题会更加严重。如果可以,尽量使用padding代替margin。
在不影响层级深度的情况下,使用LinearLayout
和FrameLayout
而不是RelativeLayout
。
RecycleView
中item 一般用ConstraintLayout
或直接使用控件来布局,以业务需求为准。
简单布局一般用FrameLayout
来布局,同时结合include、merge来使用。布局文件都要有根节点,但android中的布局嵌套过多会造成性能问题,于是在使用include嵌套的时候我们可以使用merge作为根节点,这样可以减少布局嵌套,提高显示速率。
小结:使用布局优先级:FrameLayout>ConstraintLayout>LinearLayout>RelativeLayout
,结合效率和需求实现。
尽量减少使用wrap_content
,推荐使用mathch_parent
或固定尺寸配合gravity="center"
因为 在测量过程中,match_parent
和固定宽高度对应EXACTLY
,而wrap_content
对应AT_MOST
,这两者对比AT_MOST
耗时较多。
在需要的地方添加渲染背景,外层不渲染,在内层需要的地方渲染。
文本控件,需要考虑文本过长时的省略策略
切图至少提供两套,xhdpi
和xxhdpi
消除布局警告,同时删除控件中的无用属性
对于只有在某些条件下才展示出来的组件,建议使用viewStub
包裹起来,include 某布局如果其根布局和引入他的父布局一致,建议使用merge包裹起来,如果你担心preview效果问题,这里完全没有必要,可以tools:showIn=""
属性,这样就可以正常展示preview
了。
欢迎补充。
参考资料:
1.Android性能优化之布局优化
2.Android 高效布局的几点建议
3.(译)写出高效清晰Layout布局文件的一些技巧
4.Android 过度绘制优化(推荐阅读)
5.Android开发之merge结合include优化布局
6.Android性能优化之渲染篇
7.如何优化你的布局层级结构之RelativeLayout和LinearLayout及FrameLayout性能分析
8.LinearLayout增加divider分割线
9.Android APP 性能优化的一些思考