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的脚本语言,语序开发者以较少的代码实现功能复杂且性能优越的应用程序。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容