UI优化
UI渲染基础
1、屏幕与适配
通过dp和自适应布局可以基本解决屏幕碎片化的问题,这也是Android推荐使用的屏幕兼容性适配方案,但它存在两个比较大的问题:
- 不一致行,由于dpi与实际的ppi的差异性,导致在相同分辨率的手机上,控件的实际大小会有所不同
- 效率,设计师设计稿都是以px为单位的,开发人员为了UI适配,需要手动通过百分百估算出dp值
除了直接dp适配之外,目前比较常用的适配方案有两种: - 限制符适配方案,主要有宽高限定符与smallestWidth限定符适配方案
- 今日头条适配方案,通过反射修正系统的density值
2、CPU与GPU
UI组件在绘制到屏幕之前,都需要经过Rasterization(栅格化)操作,而栅格化操作有是一个非常耗时的操作,GPU也就是图形处理器,它主要用于处理图形运算,可以帮助我们加快栅格化操作。
3、OpenGL和Vulkan
硬件绘制,我们通过调用OpenGL ES接口利用GPU完成绘制,OpenGL是一个跨平台的图形API,它为2D/3D图形处理硬件制定了标准软件接口,而OpenGL ES是OpenGL的子集,专为嵌入式设备设计。
Android 7.0把OpenGL ES升级到最新的3.2版本同时,还添加了对Vulkan的支持。Vulkan是用于高性能的3D图形的低开销、跨平台API。在改善功耗、多核优化提升绘图调用上有着非常明显的优势。
Android渲染
Android各组件的作用:
- 画笔:Skia或者OpenGL,我们用Skia画笔绘制2D图形,也可以用OpenGL来绘制2D/3D图形
- 画纸:Surface,在Android 中Window是View的容器,每个窗口都会关联一个Surface,而WindowManager则负责管理这些窗口,并把它们的数据传递给SurfaceFlinger
- 画板:Graphic Buffer,用于应用程序图形的绘制,在Android 4.1之前是双缓冲机制,在Android 4.1之后采用三缓冲机制
- 显示:SurfaceFlinger,它将WindowManager提供的所有Surface,通过硬件合成器Hardware Composer合成并输出到显示屏。
1、Android 4.0开启硬件加速
在Android3.0之前,或者没有启用硬件加速时,系统会使软件方式渲染UI,整个流程如上图所示:
- Surface,每个View都由某一个窗口管理,而每个窗口都关联有一个Surface
- Canvas,通过Surface的lock函数获得一个Canvas,Canvas可以简单理解为Skia底层接口的封装
- Graphic Buffer, SurfaceFlinger会帮我们托管一个BufferQueue,我们冲BufferQueue中拿到Graphic Buffer,然后通过Canvas以及Skia将绘制内容栅格化到上面
-
SurfaceFlinger, 通过Swap Buffer 把Front Graphic Buffer 的内容交给SurfaceFlinger,最后硬件合成器Hardware Composer合成并输出到显示屏。
Android 3.0开始支持硬件加速,到Android 4.0 时默认开启硬件加速
硬件加速绘制与软件绘制整个流程差异非常大,最核心就是我们通过GPU完成Graphic Buffer的内容绘制。此外硬件绘制还引入了一个DisplayList的概念,每个View内部都有一个DisplayList,当某个View需要重绘时,将它标记会Dirty。
当需要重绘时,仅仅只需要重绘一个View的DisplayList,而不是像软件绘制那样需要向上递归。这样可以大大减少绘图的操作数量,提高渲染效率。
2、Android 4.1:Project Butter
Project Butter主要包含两个组成部分:一个是VSYNC,一个是Triple Buffering。
VSYNC信号
VSYNC类似于时钟中断,每收到VSYNC中断,CPU会立即准备Buffer数据,由于大部分显示设备刷新频率都是60HZ(一秒刷新60次),也是一帧数据的准备工作要在16ms内完成。
这样应用总是在VSYNC边界上开始绘制,而SurfaceFlinger总是VSYNC边界上进行合成,这样可以消除卡顿,并提升图形的视觉表现。
三级缓冲机制Triple Buffering
在Android 4.1之前都使用双缓冲机制,不同的View或者Activity它们都会共用一个Window,也就是共用一个Surface。而每个Surface都会有一个BufferQueue缓存机制,但是这个队列会由SurfaceFlinger管理,通过匿名共享内存机制与App应用成交互。
整个流程如下:
- 每个Surface对应的BufferQueue内部都有两个Graphic Buffer,一个用于绘制一个用于显示。会把内容先绘制到离屏缓冲区(OffScreen Buffer),在需要显示时,才把离屏缓冲区的内容通过Swap Buffer复制到Front Graphic Buffer中
- 这样SurfaceFlinger就拿到了某个Surface最终要显示的内容,但是同一时间可能会有多个Surface,这里可能是不同应用的Surface,也可能是同一个应用里面类似SurfaceView和TextureView,他们都会有自己单独的Surface
-
这个时候SurfaceFlinger把所有的Surface要显示的内容统一交给Hardware Composer,它会根据位置、Z-Order顺序等信息合成为最终显示的内容,而这个内容交给系统的帧缓冲区Frame Buffer来显示(Frame Buffer是非常底层的,可以理解为屏幕显示的抽象)。
如果只有两个Graphic Buffer缓冲区A和B,如果CPU/GPU绘制时间过程,超过了一个VSYNC信号周期,因为缓冲区B中的数据还没准备好,所以只能继续显示A缓冲区的内容,这样缓冲区A和B都分别被显示设备和GPU占用,CPU无法准备下一帧的数据。
如果再提供一个缓冲区,CPU、GPU和显示设备都能各自使用各自的缓冲区工作,互不应用。简单来说,三缓冲机制就是在双缓冲机制的基础上增加了一个Graphic Buffer,这样可以最大限度的利用空闲时间,带来的坏处是多使用了一个Graphic Buffer所占用的内存。
数据测量
Android 4.1新增了Systrace性能数据采样和分析工具,Tracer for OpenGL ES也是Android 4.1新增的工具,可逐帧、逐函数的记录App用OpenGL ES的绘制过程。它提供了每个OpenGL函数调用的消耗时间,所以很多时候用来做性能分析。在Android 4.2系统增加了检测过度绘制工具。
3、Android 5.0:RenderThread
经过Project Buffer黄油计划之后,Android的渲染性能有了很大的改善,但是有一个问题,虽然我们利用了GPU的图形高性能运算,但是从计算DisplayList,到通过GPU绘制到Frame Buffer,整个计算和绘制都在UI主线程中完成。
UI主线程任务过于繁重,如果整个渲染过比较耗时,可能会造成用户无法响应的操作,进而出现卡顿。GPU对图形的绘制渲染能力更胜一筹,如果使用GPU并在不同的线程绘制渲染图形,那么整个流程会更加顺畅。
所以Android 5.0引入了两个比较大的改变。一是RenderNode,它对DisplayList及一些View的显示属性做了进一步的封装;另一个是RenderThread,所欲的GL命令执行都放到这个线程上,渲染线程在RenderNode中存有渲染帧的所有信息,可以做一些属性动画,这样即便主线程有耗时的操作也可以保证动画流程。
我们可以开启Profile GPU Rendering检查,在Android 6.0之后,会输出下面的计算和绘制每个阶段的耗时:
如果将上面的步骤转化为线程模型,可以得到下面的流水线模型。CPU将数据同步(sync)给GPU之后,一般不会阻塞等待GPU渲染完毕,而是通知结束后返回,而RenderThread承担了比较多的绘制工作,分担了主线程很多压力,提高了UI线程的响应速度。
UI渲染测量
- 测试工具:Profile GPU Rendering 和Show GPU Overdraw
-
问题定位工具:Systrace和Tracer for OpenGL ES
在Android 3.1之后,推荐使用Graphic API Debugger(GAPID)来代替Tracer for OpenGL ES工具,GAPID可以说是升级版,它不仅可以跨平台,而且功能更加强大,支持Vulkan与回放。
1、gfxinfo
gfxinfo可以输出包含各阶段发生的动画及帧相关的性能信息,具体命令如下:
adb shell dumpsys gfxinfo 包名
除了渲染性能之外,gfxinfo还可以拿到渲染相关的内存和View hierachy信息,在Android 6.0之后,gfxinfo命令增加了framestats参数,可以拿到最近120帧每个绘制阶段的耗时信息。通过这个命令可以实现自动化统计应用的帧率,更进一步还可以实现自定义的“Profile GPU Rendering”工具。
adb shell dumpsys gfxinfo 包名 framestats
2、SurfaceFlinger
关于渲染使用的内存情况,可以使用下面的命令拿到SurfaceFlinger相关的信息:
adb shell dumpsys SurfaceFlinger
UI优化的常用手段
要求所有的渲染操作都必须在16ms(=1000ms / 60fps)内完成,UI优化就是拆解熏染的各个阶段的耗时,找到瓶颈点,再加以优化。
1、尽量使用硬件加速
硬件加速绘制的性能是远大于软件绘制,所以要尽可能的采用硬件加速。有些情况不能采用硬件加速,因为硬件加速不支持所有的Canvas API,具体API兼容列表,查看drawing-support,如果使用了不支持的API,系统就需要通过CPU软件模拟绘制,这也是渐变、磨砂、圆角等效果渲染性能比较低的原因。
SVG也是一个非常典型的例子,SVG很多指令硬件加速都不支持,但可以提前将SVG转换成Bitmap保存起来,这样系统就可以更好的使用硬件加速会绘制,同理对于圆角、渐变等场景,也可以改为Bitmap实现。
2、Create View优化
View的创建是在主线程完成的,对于一些负责的界面,耗时会增加,根据View的创建流程有如下优化点:
使用代码创建
XML进行UI编写十分方便且提供可视化预览,但是效率方面大打折扣,建议在对性能要求非常高,且修改不频繁的页面采用代码创建的方式。
异步创建
如果再线程提前创建View,会报如下的错误:
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare() at android.os.Handler.(Handler.java:121)
解决方案是先将线程的Looper的MessageQueue替换成UI线程Looper的Queue,最后在创建完View后把线程的Looper恢复成原来的。
View复用
正常来说,View会随着Activity的销毁而同时销毁,ListView、RecycleView通过View的缓存和重用大大提升渲染性能。因此可以实现一套可以在不同Activity或Fragment使用的View缓存机制。但是这里需要保证所有进入缓存的View不会保留之前的状态。
3、measure/layout优化
- 减少UI布局层次,尽量扁平化,使用<ViewStub><Merg>等优化
- 优化layout开销,尽量不使用RelativeLayout或者基于weighted LinearLayout,他们layout的开销非常巨大,建议使用ConstrainLayout替代。
- 背景优化,尽量不要重复设置背景
PrecomputedText提供了接口,可以异步进行measure和layout,不必再主线程中执行。
UI优化其他手段
1、Litho:异步布局
Litho是Facebook开源的声明式Android UI渲染框架,本身非常强大,内部做了很多优化。
-
异步布局
一般来说Android所有的控件绘制都要遵守measure -> layout -> draw的流水线。
Litho把measure和layout都放到了后台线程,只留下了必须要再主线程完成的draw,大大降低了UI线程的负载。
- 界面扁平化
Litho使用自有的布局引擎(Yoga),在布局阶段可以检测不必要的层级,减少ViewGroups,来实现UI扁平化。 -
优化RecycleView
Litho优化RecycleView中UI组件的缓存和回收方法,原生RecycleView或者ListView是按照viewType来进行缓存和回收,但是如果一个RecycleView/ListView中出现过多的viewType,会使缓存形同虚设,Litho是按照text、image和video独立回收的,可以提高缓存命中率、降低内存使用率、提高滚动帧率。
2、Flutter:自己的布局+渲染引擎
Flutter是谷歌推出的开源移动应用开发框架,可发着可以通过Dart语言开发App,一套代码同时运行在iOS和Android平台。在Android上Flutter完全没有基于系统的渲染引擎,而是把Skia引擎直接集成到App中,并且直接使用了Dart虚拟机。
开发Flutter应用总的来说简化了线程模型,框架给我们抽象出各司其职的Runner,包括UI、GPU、I/O、Platform Runner。Android平台上面没一个引擎实例启动的时候都会为UI Runner、GPU Runner、I/O Runner各自创建一个新的线程,所有Engine实例共享同一个Platform Runner和线程。
- 首先UI Runner会执行root isolate(可以简单理解为main函数,isolate是Dart虚拟机中一种执行并发代码实现,Dart虚拟机实现了Actor的并发模型,与大名鼎鼎的Erlang使用了类似的并发模型。)
- Flutter 引擎得到通知后,会告知系统我们要同步VSYNC
- 得到GPU的VSYNC信号后,对UI Widgets进行Layout并生成一个Layer Tree
- 然后Layer Tree会交给GPU Runner进行合成和栅格化
-
GPU Runner使用Skia库绘制相关图形
Flutter也采用了类似Litho、React属性不可变,单向数据流的方案,这样做的好处是将视图与数据分开。
3、RenderThread与RenderScript
在Android 5.0系统增加了RenderThread对于ViewPropertyAnimator和CircularReveal动画,可以使用RenderThread实现动画的异步渲染。图片的变换涉及大量的计算任务,可以通过RenderScript提高性能,它是Android操作系统提供的一套API,基于异构计算思想,专门用于密集计算。RenderScript提供了三个基本工具:一个硬件无关的通用计算API;一个类似于CUDA、OpenGL和GLSL的计算API;一个类C99的脚本语言,语序开发者以较少的代码实现功能复杂且性能优越的应用程序。