在从Android 6.0源码的角度剖析View的绘制原理一文中,我们了解到View的绘制流程有三个步骤,即measure(
测量
)、layout(布局
)和draw(绘制
),它们主要运行在系统应用框架层,而真正将数据渲染到屏幕上的则是系统Native层的SurfaceFlinger服务
来完成的。本文将暂不对SurfaceFlinger服务
的执行流程进行剖析,而是从更加底层的角度来了解APP的渲染机制,同时引出在渲染过程中遇到的性能问题,以及如何去发现、优化它们。
对于开发者来说,APP的界面主要是由XML布局文件
来表现的,每个XML布局文件
中又包括了很多视图组件,比如ImageView、Button、TextView等等,它们分别表示了图片、按钮、字符串等不同的信息,那么这些复杂的XML布局文件和标记语言,又是如何转化成为用户能够看得懂的图像的呢?答案是:格栅化(Rasterization)
!所谓格栅化,就是将例如字符串
、按钮
、路径
或者形状
等的一些高级对象,拆分到不同的像素屏幕上进行显示。格栅化是一个非常费时的操作,CPU作为中央处理器本身任务就比较繁重,因此人们引入了GPU(图像处理器)
这块特殊的硬件来加快格栅化操作(硬件加速)。格栅化操作大体如下:
接下来,我们就来详细了解XML布局文件中的UI组件是如何被格栅化并渲染显示在屏幕上的?在APP绘制渲染过程中,与绘制渲染有关的硬件主要有CPU和GPU(图形处理器),其中,CPU负责把UI组件计算成Polygons(多边形)
或Texture(纹理)
,而GPU负责格栅化和渲染工作。CPU和GPU通过图形驱动层
进行连接,这个图形驱动层
维护了一个Display List
队列,它们分别充当生产者
和消费者
,协作完成具体的绘制渲染过程。绘制渲染过程如下图所示:
首先
,CPU
会对UI组件(View)进行Measure
、Layout
、Record
、Execute
的数据计算工作,以将其计算成的Polygons(多边形)
或Texture(纹理)
,它们是GPU能够识别的对象,而承载这些信息的是一个被称为Display List
的结构体,它持有所有交给GPU绘制到屏幕上的数据信息,包含GPU要绘制的全部对象的信息列表和执行绘制操作的OpenGL命令列表。在某个View第一次需要被渲染的时候,Display List
会因此被创建,当这个View需要显示到屏幕上时,GPU接收到绘制指令后会执行该Display List
。每个View拥有自己的Display List
,Display Lis
本身也构成一个树状的结构,跟View Hierachy(视图树)
保持一致。
其次
,由于每次从CPU计算得到的Polygons
或Texture
提交(转移)GPU是一件比较麻烦且耗时的事情(注:实际提交的是Display List
),因此Android系统又引入了OpenGL ES,该库API允许将那些需要渲染的Polygons
或Texture
存储(Hold)在GPU存储器(显存)中,当下一次需要渲染的时候只需要在GPU存储器里引用它,然后告诉OpenGL如何绘制就可以了,而不再需要经过CPU上传。需要注意的是,假如View的绘制内容发生变化,那么GPU Memory存储的Display List
就无法再继续使用,这时就需要CPU重新计算创建Display List
并重新执行指令更新到屏幕。
最后,当GPU完成对纹理的格栅化、渲染后,它会将渲染的结果放入帧缓冲区
,视频控制器会按照VSync(垂直同步)
信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。
前面说到,当GPU渲染完成后会将渲染的结果存储到帧缓冲区
,视频控制器会按照VSync(垂直同步)
信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。那么问题来了,什么是VSYNC信号?在讲解VSYNC之前,我们需要了解两个相关的概念:Refresh Rate
和Frame Rate
。
Refresh Rate(刷新频率)
表示屏幕在一秒内被刷新的次数,取决于硬件的固定参数,目前大部分手机这个固定参数(屏幕刷新频率)为60Hz
。也就是说,Android系统每隔约16ms(1000ms/60=16.66ms)
就会重新刷新一次界面(Activity),因此我们有16ms的时间去完成每帧的绘制、渲染的逻辑操作。
Frame Rate(帧率)
表示GPU在一秒内能够渲染的帧数,通常为60fps
。为什么是60fps
?这是因为人眼与大脑之间的协作无法感知超过60fps
的画面更新。简单的说,人眼看到的动画其实都是一帧帧连续播放的静态图片,当播放的速度达到1秒针24帧(24fps
)时,对于人眼感知来说就是连续的线性运动。当然低于30fps在某些场景仍然无法顺畅表现绚丽的画面,此时就需要60fps
来达到想要的效果。
现在我们再来理解下VSYNC信号:**屏幕的刷新过程是每一行从左到右(水平刷新,Horizontal Scanning
),从上到下(垂直刷新,Verical Scanning)。当整个屏幕刷新完毕,即一个垂直刷新周期完成,会有短暂的空白期,此时Android系统就会发出VSYNC信号,触发下一帧对UI的渲染、显示。VSYNC是一种定时中断,一旦收到VSYNC信号CPU就开始处理帧数据,通常Android系统每隔16ms发出VSYNC信号,这个周期时间由屏幕刷新频率决定。**通常来说,帧率超过刷新频率只是一种理想的状态,在超过60fps
的情况下,GPU所产生的帧数据会因为等待VSYNC
的刷新信息而被Hold住,这样能够保持每次屏幕刷新都有实际的新的数据可以显示。但是基本上我们遇到的是帧率<=屏幕刷新频率
的情况,尤其是在帧率小于刷新频率时,会出现待VSYNC
信号到来时,屏幕没有可以刷新的数据,即帧缓冲区还是之前的那帧图像,这就会导致屏幕显示的该帧画面内容仍然是上一帧的画面,而这种现象我们就称之为"掉帧
",对于用户来说,就是界面出现了卡顿现象
,即运行不流畅。
由此我们可以得出,Android系统之所以会出现卡顿现象
是因为的某些操作耗费了帧间隔时间(16ms
),导致其他类似计算、渲染等操作的可用时间就会变少,在16ms
无法完成正常的绘制、渲染工作。当下一个VSYNC信号到来时,Android系统尝试在屏幕上绘制新的一帧,但是新的一帧还没准备好,就会导致无法进行正常渲染,画面就不会刷新,用户看到的还是上一帧的画面,从而发生了丢帧。产生卡顿的原因有很多,主要有以下几点:
布局Layout过于复杂,无法在16ms内完成渲染;
同一时间动画执行的次数过多,导致CPU或GPU负载过重;
View过度绘制,导致某些像素在同一帧时间内被绘制多次
在UI线程中做了很多耗时的操作;
GC回收时暂停时间过长或者频繁GC产生大量的暂停时间(内存抖动);
在1.2小节我们谈到,大量不停的GC操作会显著占用帧间隔时间(16ms
),如果在帧间隔时间里面做了过多的GC操作,那么自然其他类似计算,渲染等操作的可用时间就变得少了,就容易出现丢帧现象,而导致这种频繁GC的原因之一就是“内存抖动(Memory Churn)
”。内存抖动是因为大量的对象被创建又在短时间内马上被释放,瞬间产生大量的对象会严重占用年轻代(Yong Generation)
的内存区域,当达到阀值时剩余空间不够的时候,也会触发GC。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。
从上一小节的分析可知,Android系统的渲染机制主要体现在CPU的绘制和GPU的格栅化、渲染两个阶段,其中,主要比较耗时的在CPU的绘制过程,即将UI对象转换为一系列的多边形和纹理,和从CPU上传Display List数据到GPU过程。如果CPU/GPU 负载过量或者上传数据次数过多,就会导致 16ms 内没有绘制、渲染完成出现渲染性能问题。因此,对于渲染性能方面的的优化,我们将从这两个方面进行剖析,即减轻CPU/GPU的负载量和减少占用额外的GPU资源。下面我们就介绍几个工具来帮助我们发现、解决一些渲染性能优化问题,主要有Profile GPU Rendering
、SysTrace
以及TraceView
等。
所谓过度绘制,指的是在屏幕上某个像素在同一帧时间内被绘制多次,从而浪费了CPU和GPU的资源
。举个例子来说,假如我们要粉刷房子的墙壁,一开始刷绿色,接着又开始刷黄色,这样黄色就将绿色盖住了,为此第一次的粉刷就白干了。产生过度绘制主要有两个原因:
在XML布局中,控件有重叠且都有设置背景
;View的onDraw在同一区域绘制多次;
Show GPU overdraw
是Android系统提供的一个查看界面过度绘制
的工具,我们可以在手机设置里的开发者选项中开启它,即开发者选项->调试GPU过度绘制
。Show GPU overdraw效果:
开启调试GPU过度绘制后,我们的界面就会出现各种颜色,它们的具体含义如下:
白色
:没有过度绘制,即每个像素点在屏幕上只绘制了一次;蓝色
:一次过度绘制,即每个像素点在屏幕上绘制了两次;绿色
:二次过度绘制,即每个像素点在屏幕上绘制了三次;粉红色
:三次过度绘制,即每个像素点在屏幕上绘制了四次;红色
:四次以上过度绘制;为此我们可以得出,一个合格的界面应该以白色和蓝色为主,绿色以上的区域不要超过整体的三分之一,总之颜色越浅越好。
Profile GPU Rendering是Android4.1系统引入用于展示GPU渲染每帧时各个阶段所消耗的时间,以便于我们可以直接观察到渲染该帧时各个阶段的耗时情况,即有没有超过16ms。
我们可以在手机设置里开发者选项中打开它(开发者选项->GPU显示配置文件/GPU呈现模式分析->在屏幕上显示为条形图
),接着手机屏幕的底部就会出现彩色的柱状。效果如下图所示:
图中绿色横轴代表帧时间,彩色的柱状(纵轴)表示某一帧的耗时。绿色的横线为警戒线,超过这条线则意味着该帧绘制渲染的时间超过了16ms,我们应尽量保证垂直的彩色柱状图保持在绿线下面,任何时候一帧超过绿线,我们的app将会丢掉一帧。每一个彩色柱状图代表一帧的渲染,越长的垂直柱状图表示这一帧需要渲染的时间越长。当APP在运行时,我们会看到手机底部的柱状图会从左到右动态地显示,随着需要渲染的帧数越来越多,他们回堆积在一起,这样就可以观察到这段时间帧率的变化。下图为柱状图中不同颜色代表的意义:
颜色意义解释:
橘色
:代表处理的时间。代表CPU在等待GPU完成工作的时间,如果过高表示GPU很繁忙;
红色
:代表执行的时间。Android 的2D渲染器向OpenGL发出命令绘制或重绘display lists 花费的时间,柱子的高度等于所有Display list绘制时间的总和。如果红色柱状图很高,可能由于重新提交视图而导致,还有复杂的自定义View也会导致红的柱状图变高;
浅蓝色
:向CPU传输Bitmap花费的时间,过高代表了加载了大量图形;
深蓝色
:代表绘制的时间。也就是需要多长时间去创建和更新Dispaly List。过高代表在onDraw中花费过多时间,可能是自定义画图操作太多或执行了其它操作;
淡绿色
:代表了onLayout和onMeasure方法消耗的总时间,这段很高代表遍历整个view树结构花费了太多时间;
绿色
:代表为该帧内所有animator求值(属性动画中代表通过估值器计算属性的具体值)所花费的时间.如果这部分过高,代表自定义animator性能不佳或者更新view属性引发了某些意外操作;
深绿色
:代表app在用户输入事件回调中花费的时间,这部分过高可能意味着app处理用户输入事件时间过长,建议将操作分流到工作线程;
墨绿色
:代表在连续两帧间的时间间隔,可能是因为子线程执行时间过长抢占了UI线程被cpu执行的机会。
为此,我们可以得出在Profile GPU Rendering中,假如红色/橘色
占比较大,说明可能出现重复布局
的情况,可以从否减少视图层级、减少无用的背景图、减轻自定义控件复杂度
等方面去优化;假如蓝色/浅蓝/各种绿色
占比较大,应该从耗时操作
这方面去优化。当然,Profile GPU Rendering可以找到渲染有问题的界面,但是想要修复的话,只依赖Profile GPU Rendering是不够的,我们可以通过如下两款工具来定位问题,即TraceView
和Hierarchy Viewer
,前者能够详细分析问题原因;后者能够查看布局的层次和每个View所花费的时间。Systrace
总之,我们应尽量从以下几个方面避免过度绘制。
移除无用的背景图;
减少视图层级,尽量使用扁平化布局,比如Relativeayout;
减轻自定义控件复杂度,重叠区域可以使用canvas.clipRect
方法指定绘制区域;` 前面我们谈到,Android系统之所以会出现卡顿现象
是因为的某些操作耗费了帧间隔时间(16ms
),导致其他类似计算、渲染等操作的可用时间就会变少,在16ms
无法完成正常的绘制、渲染工作。当下一个VSYNC信号到来时,Android系统尝试在屏幕上绘制新的一帧,但是新的一帧还没准备好,就会导致无法进行正常渲染,画面就不会刷新,用户看到的还是上一帧的画面,从而发生了丢帧,这就是"卡顿现象"。下面我们介绍两款分析界面卡顿的利器,即SysTrace
和TraceView
,其中,SysTrace
为卡顿原因指明大体方向,TraceView
则是找到是什么让CPU繁忙、某些方法的调用次数等具体信息。
SysTrace
是Android 4.1中新增的性能数据采样和分析工具,它可以帮助我们收集Android关键子系统的运行信息,如SurfaceFlinger、WMS等Framework部分关键模块、服务、View体系系统等。Systrace
的功能包括跟踪系统的I/O操作、内核工作队列、CPU负载以及Android各个子系统的运行状态。**对于UI显示性能,比如动画播放不流畅、渲染卡顿等问题提供了分析数据。**我们可以在AS的Android Device Monitor(DDMS)开启Systrace
,需要注意的是,AS 3.0以后谷歌已将DDMS从AS面板上移除,但我们仍然可以打开sdk目录/tools/monitor.bat
使用它。SysTrace
使用方法如下:
双击sdk目录/tools/monitor.bat
打开DDMS,单击DDMS面板上的Systrace按钮;
进入抓取设置界面后设置跟踪的时长,以及trace.html文件输出路径等内容;
操作APP我们怀疑卡顿的地方,对该过程进行跟踪;
待跟踪结束后就会在指定路径生产trace.html文件,接下来用Chrome浏览器打开它进行分析。
Alerts
、Frames
和Kernel CPU
,下面我们就详细分析如何使用它们来定位问题。 Alerts部分是Systrace自动分析trace中的事件,并能自动高亮某个性能问题作为一个Alerts(警告),建议调试人员下一步怎么做。可以通过点击顶部浅蓝色圆圈查看某一个警告,或者点击右侧的Alerts
选项列出所有警告信息,这些信息将会按类别列出,比如上图中列出了Scheduling delay
。选中一条Alert详情如下:
Scheduling delay
(调度延迟)的意思就是一个线程在处理一块运算的时候,在很长一段时间都没有被分配到CPU上面做运算,从而导致这个线程在很长一段时间都没有完成工作。从上图可以看出,选中的这帧只运行了2.343ms
,而有101.459ms
是在休眠,这就意味着这一帧的渲染过程非常慢。当然仅仅通过Alerts的提示仍然无法找到渲染慢的原因,接下来就需要借助Frames部分进一步定位。
Frames部分给出的是应该的帧数,每一帧就是一个F圆圈
,F圆圈
有三种颜色:绿色、黄色和红色,其中,绿色表示该帧渲染流畅(即没有超过16.66ms,满足每秒60帧稳定所需的帧),黄色和红色表示该帧的渲染时间超过了16.66ms,这就意味着黄色和红色代表的帧存在渲染性能问题(红色比黄色更严重)。我们点击红色的F圆圈
,在面板的底部会给出具体的Alert信息(同Alert部分),然后我们再按下电脑键盘上的M键
可以看到被高亮的部分就是这帧精确的时间耗时。应用区域的Frames展示如下:
由于Android 5.0以上系统UI渲染的工作是在UI Thread
和RenderThread
完成的,我们使用键盘的W键
对这两个区域放大,然后选择两个线程中最长的一块区域(表示某个函数方法)观察那些View在被填充过程中耗时比较严重。我们点击DrawFrame
这个方法,在面板的底部可以得到以下信息:
从上图可以看出,Wall Duration
代表着这一块区域的开始到结束的耗时,为204.877ms;CPU Duration
代表实际CPU在处理这一块区域的耗时,即分配CPU的时长0.899ms。很显然,这两个时间差距非常大,CPU只消耗了不到1ms的时间来运算这块区域。这就意味着可能其他线程/进程长期占用CPU,导致RenderThread无法进行正常运算出现渲染异常。接下来,我们就通过分析Kernel CPU部分进一步确定是哪个线程/进程长期占用CPU。
在Kernel CPU区域,每一行代表一个CPU核心和它执行任务的时间片,放大后会看到每个色块代表一个执行的进程,色块的长度代表其执行时间。如下图所示:
在之前选中的帧区域,我们很容易看到一个很长的绿色块,位于CPU2上,这个绿色块CPU执行的进程为HeapTaskDaemon,从进程名来看很容易猜出这个进程就是我们的GC守护进程。这就意味着,在这一帧内执行了长时间的垃圾回收操作,由于系统执行GC操作时会将正在执行的其他进行挂起,GC进程会独占CPU,长时间的GC操作或频繁GC就会导致其他进程长时间分配不到CPU时间片,这就是导致渲染变慢的根本原因。我们点击HeapTaskDaemon颜色块,可以看到具体的GC时间,如下图所示:
操作快捷键
- W、S:放大、缩小
- A、D:向左、向右移动
需要注意的是,由于Systrace是以系统的角度返回一些信息,只能提供一个大概且深度有限,我们可以用它来进行粗略的检查,以便获得具体分析的方向。如果需要进一步得到是哪块代码引起的CPU繁忙、某些方法的调用次数等,就需要借助TraceView这个工具进行。
TraceView
是Android SDK中自带的数据采集和分析工具,与Systrace
不同的是,它是从代码层面来分析性能问题,且针对的是每个方法来分析,比如当我们发现应用出现卡顿的时候,就可以通过TraceVIew
来分析出现卡顿时在方法的调用上有没有很耗时的操作。开启TraceView
方法与Systrace
一样,都是在DDMS
中启动。在使用TraceView
时,我们需要着重关注以下两个方面:
TraceView使用方法:
sdk目录/tools/monitor.bat
打开DDMS,单击DDMS面板上左上角带小红点按钮,文字提示为start Method Profiling
,然后按钮左上方会出现一个灰色的正方形,文字提示为stop Method Profiling
,此时按钮变为停止采集功能;Profiling Options
配置采样频率,默认为1000微秒;stop Method Profiling
)结束采集。TraceView
的面板分上下两个部分:时间线面板
以每个线程为一行,右边是该线程在整个过程中方法执行的情况;数据分析面板
是以表格的形式展示所有线程的方法的各项数据指标;时间线面板:
时间线面板以每个线程为一行,右边是该线程在整个过程中方法执行的情况,比如上图中显示的main线程就是Android应用的主线程,当然也会存在其他线程,可能会因操作不同而发生改变。每个线程的右边对应的是该线程中每个方法的执行信息,左边为第一个方法执行开始,最右边为最后一个方法执行结束,其中的每一个小立柱就代表一次方法的调用,你可以把鼠标放到立柱上,就会显示该方法调用的详细信息。
如上图所示,墨绿色的立柱表示FibonacciActivity.computeFubonacci()
方法的执行情况,可以看出这个方法执行的时间很长,尤其是对于处于主线程中,这是非常不正常的现象。如果你想在分析面板中详细查看该方法,可以双击该立柱,数据分析面板自动跳转到该方法。
数据分析面板:
数据分析面板右侧为时间线面板中每个立柱表示的方法,当我点击墨绿色立柱后,FibonacciActivity.computeFubonacci()
方法被高亮,展开该方法我们可以看到Parents
和Children
,其中,Parents
表示谁调用了computeFubonacci()方法,Children
表示computeFubonacci()方法调用了哪些方法。数据分析面板的左侧展示的是每个方法(一行)执行的耗时,我们着重看下computeFubonacci()方法Incl Real Time
值为1595.360ms,这显然是不正常的。另外,再看下Calls+Recur Calls/Total
的值显示computeFubonacci()方法数为1,但是被递归调用了1491次。因此,我们就可以得出之前的APP操作之所以会出现卡顿,是因为在主线程的FibonacciActivity中递归调用了computeFubonacci()方法,导致CPU被长期占用,从而导致渲染线程无法获得CPU资源出现无法正常渲染的性能问题。
每一列数据代表的含义如下表所示:
名称 | 意义 |
---|---|
Name | 方法的详细信息,包括包名和参数信息 |
Incl Cpu Time | Cpu执行该方法该方法及其子方法所花费的时间 |
Incl Cpu Time % | Cpu执行该方法该方法及其子方法所花费占Cpu总执行时间的百分比 |
Excl Cpu Time | Cpu执行该方法所话费的时间 |
Excl Cpu Time % | Cpu执行该方法所话费的时间占Cpu总时间的百分比 |
Incl Real Time | 该方法及其子方法执行所话费的实际时间 |
Incl Real Time % | 上述时间占总的运行时间的百分比 |
Excl Real Time % | 该方法自身的实际允许时间 |
Excl Real Time | 上述时间占总的允许时间的百分比 |
Calls+Recur Calls/Total | 调用次数+递归次数 |
Calls/Total | 调用次数和总次数的占比 |
Cpu Time/Call | Cpu执行时间和调用次数的百分比,代表该函数消耗cpu的平均时间 |
Real Time/Call | 实际时间于调用次数的百分比,该表该函数平均执行时间 |
/** UI卡顿现象
* @Auther: Jiangdg
* @Date: 2019/11/18 15:06
* @Description: 使用斐波那契数列人为制造卡顿现象
*/
public class FibonacciActivity extends AppCompatActivity {
private static final String LOG_TAG = "FibonacciActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fibonacci);
Button mbtn = (Button) findViewById(R.id.caching_do_fib_stuff);
mbtn.setText("计算斐波那契数列");
mbtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(LOG_TAG, String.valueOf(computeFibonacci(40)));
}
});
WebView webView = (WebView) findViewById(R.id.webview);
webView.getSettings().setUseWideViewPort(true);
webView.getSettings().setLoadWithOverviewMode(true);
webView.loadUrl("file:///android_asset/shiver_me_timbers.gif");
}
public int computeFibonacci(int positionInFibSequence) {
//0 1 1 2 3 5 8
if (positionInFibSequence <= 2) {
return 1;
} else {
return computeFibonacci(positionInFibSequence - 1)
+ computeFibonacci(positionInFibSequence - 2);
}
}
}