Android的显示过程可以简单概括为:Android应用程序把经过测量、布局、绘制后的surface缓存数据、通过SurfaceFlinger把数据渲染到显示屏幕上,通过Android的刷新机制来刷新数据。也就是说应用层负责绘制,系统层负责渲染,通过进程间通信把应用层需要绘制的数据传递到系统层服务,系统层服务通过刷新机制把数据更新到屏幕。
在Android的每个View都会经过Measure和Layout来确定当前需要绘制的View所在的大小和位置,然后,再通过Draw绘制到surface上。在Android系统中整体的绘制源码是在ViewRootImpl类的performTraversals()方法,通过这个方法可以看出Measure和Layout都是递归来获取View的大小和位置,并且以深度作为优先级。显然,层级越深,元素越多,耗时就越长。
对于绘制,Android支持两种绘制方式:
硬件加速从Android 3.0开始支持,它在UI显示和绘制效率方面远高于软件绘制。但它的局限如下:
将数据渲染到屏幕上是通过系统级进程中的SurfaceFlinger服务来实现的,它的主要工作流程如下:
其中,SurfaceFlinger系统进程和应用进程使用了匿名共享内存SharedClient,并且,每一个应用和SurfaceFlinger之间都会创建一个SharedClient,在每个SharedClient中,最多可以创建31个SharedBufferStack,每一个SharedBufferStack对应一个Surface,即一个window。(其中包含了两个(小于4.1版本)或者三个(4.1及以上版本)缓冲区)
因此,从上可知,一个Android应用程序最多可以包含31个窗口。最后,显示的整体流程如下:
绘制的过程首先是 CPU准备数据,通过Driver层把数据交给CPU渲染,其中CPU主要负责Measure、Layout、Record、Execute的数据计算工作,GPU负责Rasterization(栅格化)、渲染。因为图形API不允许CPU直接和GPU通信,所以要通过一个图形驱动的中间层来进行连接,在图形驱动里面维护了一个队列,CPU把display list(待显示的数据列表)添加到队列中,GPU从这个队列中取出数据进行绘制,最终才在显示屏上显示出来。
Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅画面所需的60FPS。
在4.1版本的Project Butter中对Android Display系统进行了重构,引入了三个核心元素:VSYNC(Vertical Synchronization)、Triple Buffer(三级缓冲)、Choreographer。其中作为Project Buffer核心的VSYNC,即垂直同步可认为是一种定时中断。而Choreographer起调度的作用,将绘制工作统一到VSYNC的某个时间点上,使应用的绘制工作有序。
目的是解决刷新不同步的问题。
在Tripe Buffer出现之前,Android的显示系统采用的是双缓冲技术。
在Linux上通常使用 Framebuffer 来做显示输出,当用户进程更新Framebuffer中的数据后,显示驱动会把FrameBuffer中每个像素点的值更新到屏幕,但是如果上一帧数据还没显示完,Framebuffer中的数据又更新了,就会带来残影的问题,用户会觉得有闪烁感,所以采用了双缓冲技术。
双缓冲意味着要使用两个缓冲区(在上文提及的SharedBufferStack中),其中一个称为Front Buffer,另一个称为Back Buffer。UI总是先在Back Buffer中绘制,然后再和Front Buffer交换,渲染到显示设备中。即只有当另一个buffer的数据准备好后,才会通过io_ctl系统调用来通知显示设备切换Buffer。
因为只有两个Buffer;所以4.1版本后,出现了第三个缓冲区:Triple Buffer。它利用CPU/GPU的空闲等待时间提前准备好数据,并不一定会使用。
除非必要,大部分情况下只是用到双缓冲。而且,缓冲区并不是越多越好,要做到平衡到最佳效果。
因为 VSync 中断处理的线程优先级一定要最高,否则即使接收到VSync中断,不能及时处理,也是徒劳无功。
当收到VSYNC信号时,调用用户设置的回调函数。回调类型的优先级从高到低为CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL。
Android常用的绘制优化工具一般有如下几种:
这里我们来讲解后面三种分析工具。
它是Android手机上自带的一个辅助工具,打开Profile GPU Rendering后可以看到实时刷新的彩色图,其中每一根竖线表示一帧,由多个颜色组成。
在Android M版本之前,每一条柱状图都由红、黄、蓝、紫组成,分别对应每一帧在不同阶段的实际耗时不同颜色的解释如下:
并且,从Android M开始变成了渲染八步骤:
表示GPU处理任务的时间。
进行2D渲染显示列表的时间,越高表示需要绘制的视图越多。
准备有待绘制的图片所耗费的时间,越高表示图片数量越多或图片越大。
测量和绘制视图所需的时间,越高表示视图越多或onDraw方法有耗时操作。
onMeasure与onLayout所花费的时间。
执行动画所需要花费的时间。越高表示使用了非官方动画工具或执行中有读写操作。
系统处理输入事件所耗费的时间。
主线程执行了太多任务,导致UI渲染跟不上vSync的信号而出现掉帧。
此外,可通过如下 adb命令将具体的渲染耗时输出到日志中来分析:
adb shell dumpsys gfxinfo com.**.**
复制代码
它主要用来分析函数的调用过程,可以对Android的应用程序以及Framework层代码进行性能分析。
使用TraceView查看耗时,主要关注Calls + Recur Calls / Total和(该方法调用次数+递归次数)和Cpu Time / Call(该方法耗时)这两个值,然后优化这些方法的逻辑和调用次数,减少耗时。
RealTime(实际时长)的实际执行时间要比CPU Time要长,因为它包括了CPU的上下文切换、阻塞、GC等时间消耗。
Systrace是Android 4.1及以上版本提供的性能数据采样和分析工具,它的主要作用可以归结为如下两点:
使用事项如下:
一般我们使用命令行来得到输出的html表单,在4.3版本及以上可以省略设置跟踪类别标签来获取默认值。命令如下:
cd android-sdk/platform-tools/systrace
python systrace.py --time=10 -o mynewtrace.html sched gfx view wm
复制代码
其中,常用的几个参数命令如下:
其余标签用法请参见此处。
此外,我们可以使用代码插桩的方式,在Android 4.3及以上版本可以使用Trace类的Trace.beginSection()与Trace.endSection()方法来进行追踪。其中需要注意:
使用Chrome打开文件后,其中和UI绘制关系最密切的是Alerts和Frame两个数据:
最后,这里再列出在Systrace便于操作的快捷键:
RelativeLayout也存在性能低的问题,原因是RelativeLayout会对子View做两次测量。但如果在LinearLayout中有weight属性,也需要进行两次测量,但是因为没有更多的依赖关系,所以仍然会比RelativeLayout的效率高。
由于Android的碎片化程度很高,所以使用RelativeLayout能使构建的布局适应性更强。
merge的原理:在Android布局的源码中,如果是Merge标签,那么直接将其中的子元素添加到Merge标签Parent中。
ViewStub是一个轻量级的View,它是一个看不见的,并且不占布局位置,占用资源非常小的视图对象。可以为ViewStub指定一个布局,加载布局时,只有ViewStub会被初始化,然后当ViewStub被设置为可见时,或是调用了ViewStub.inflate()时,ViewStub所指向的布局才会被加载和实例化,然后ViewStub的布局属性都会传给它指向的布局。
注意:
Android的布局复用可以通过 include 标签来实现。
最后,下面列出了我平常做布局优化时的一些小技巧:
导致过度绘制的主要原因一般有如下两点:
打开手机开发者选项中的Show GPU Overdraw选项,会有不同的颜色来表示过度绘制次数,依次是无、蓝、绿、淡红、深红,分别对应0-4次过度绘制。
比如:在获取Avatar的图像之后,把ImageView的Background设置为Transparent,只有当图像没有获取到时,才设置对应的Background占位图片。
通过canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制。并且,它还可以节约CPU和GPU资源,在clipRect区域之外的绘制指令都不会被执行。
在绘制一个单元之前,首先判断该单元的区域是否在Canvas的剪切域内。若不在,直接返回,避免CPU和GPU的计算和渲染工作。
如通过监听ListView的onScrollStateChanged事件,在滚动时暂停图片下载线程工作,结束后再开始,可以提高ListView的滚动平滑度,RecyclerView同理。
如自定义View一般采用invalidate方法刷新,可以使用以下重载方法指定要刷新的区域:
提升动画性能主要从以下三个纬度着手:
消耗资源最多,效果最差,能不用就不用。
使用补间动画实现导致View重绘非常频繁,更新DisplayList的次数过多,且有以下缺点:
相比于补间动画,属性动画重绘明显会少很多,应优先使用。
核心类:DisplayList,每一个View对应一个。
在打开硬件渲染后绘制View时,其中执行绘制的draw()方法会把所有绘制命令记录到一个新的显示列表(DisplayList),这个显示列表包含了输出的View层级的绘制代码,但并不是加入到显示列表就立刻执行,当这个ViewTree的DisplayList全都记录完毕后,由OpenGLRender负责将Root View中的DisplayList渲染到屏幕上。而invalidate()方法只是在显示列表中记录和更新显示层级,去标记不需要绘制的View。
如果应用程序中只使用了标准View或者Drawable,就可以为整个系统打开硬件加速的全局设置。
此时,会使用硬件纹理操作对一个View进行动画绘制,如果不调用invalidate()方法,就可以减少对View自身频繁的重绘。同时Android 3.0的属性动画也减小了重绘,当View通过硬件层返回时,最终所有的层叠画面显示到屏幕,View的属性同时被处理好,因此只要设置这些属性,就可以明显提高绘制的效率,它们不需要View重绘,设置属性后,View会自动刷新。因此,属性动画中绘制的递归次数比补间动画少很多。
在Android 3.0前,使用View的绘制缓冲或Canvas.saveLayer()函数对离屏缓冲进行渲染。Android 3.0后则使用View.setLayerType(type, paint)方法代替,type可以为以下三种Layer类型之一:
1、将要执行动画的View的LayerType设置为LAYER_TYPE_HARDWARE。
2、计算动画View的属性等信息,更新View的属性。
3、若动画结束,将LayerType设置为NONE。
目前比较流行的方案都是利用了Looper中的Printer来实现监控。
利用主线程的消息队列处理机制,通过自定义Printer,然后在Printer中获取到两次被调用的时间差,这个时间差就是执行时间。如果该时间超过设定的卡顿阈值(如1000ms)时,主线程卡顿发生,并抛出各种有用信息,供开发者分析。(此外,也可以在UI线程以外开启一个异步线程,定时向UI线程发送一个任务,并记下发送时间。任务的内容是将执行时间同步到发送线程,如果UI线程被阻塞,那么发送过去的任务不能被准时执行。但此方法会增加系统开销,不可取)
发生卡顿时需要捕获如下四类信息,以提高定位卡顿问题的效率与精度。
这里的信息建议抽样上报或者可以先将其保存到本地,在合适的时机以及达到一定的量时,再压缩上报到服务器,供开发者分析。具体监控代码实现可以参考BlockCanary开源项目的代码。
至此,这里我们分析一下绘制优化应经历的几个过程:
应用之所以会出现卡顿,除了绘制方面的问题,还有一个影响因素就是内存,不合理地使用内存不仅会导致卡顿,还会对耗电和应用的稳定性造成很大影响。下一篇性能优化文章,笔者将对Android中的内存优化进行全面的讲解,若读者觉得哪里有写的不好的地方或有误的地方希望多多进行批评指正,愿我们共同进步和成长!
整理了一份721页,包含性能优化所有知识点的pdf学习笔记,内容涵盖设计思想与代码质量优化、程序性能优化、开发效率优化、其中详细讲解了启动优化、布局优化、内存优化、屏幕适配、OOM问题等方面,并还包含很多大厂的项目实战案例解析。