Android显示之应用界面绘制

越到上层,跟业务关联越直接。代码就越繁杂。Android上层显示的代码正是如此。此外,java语言本身繁复的特点(比C语言多了满屏的try-catch,比C++少了析构处理的优雅简洁,和更高级的语言scala、python等就别比了),更加剧了这一现象。


直接去看代码,往往会看得一头雾水,知其然而不知其所以然。在这时候,就要把代码扔掉。细致去理清须要实现什么,怎么实现,画一幅架构设计图出来,然后再跟代码去对照。

Android这部分代码并非圣经,有非常多待商榷的地方。心中要有主见,批判性地看。

因为中间各种事耽搁。加上懒。一直没空写长篇博文。间隔了非常长一段时间,请读者先回想显示概述与下层显示:
http://blog.csdn.net/jxt1234and2010/article/details/44164691
另外,因为Android显示还是有不少人写的。某些模块有写得比較好的文章我就直接上链接。不自己写了,见谅。


下层显示关键词:SurfaceFlinger
上层显示关键词:View

初步章节安排:
1、界面绘制
2、布局计算
3、硬件加速下层实现
4、典型控件
5、资源管理

UI引擎设计原则

易用性

用户是非常懒的,事实上程序猿也一样。 让应用开发人员直接使用OpenGL去开发界面,无异于让他们赤手空拳打坦克。即便是使用图形引擎的接口,也已经相当繁琐了。
最理想的情形,是由编辑器搞定界面,所见即所得。配配參数就ok。如Unity3D。


一般都会提供足够多的默认控件,但假设应用有更绚丽的效果要求,也会提供接口实现。

高效性

作为UI引擎,掌控着足够完整的渲染流程,优化空间是相当大的。相对而言。难度也更大。

这个高效反映在双方面,一是图形引擎的高效,一是脏区域识别的高效。

图形引擎的高效

第一个重要的点是下层图形引擎的选用
图形引擎的高效反映在两个方面:单体性能和复合性能。单体性能即渲染单个物体的性能,复合性能则是指在多个物体一起渲染的性能(多个物体一起渲染,有一些优化手段。比方作遮挡推断,消除非必要渲染。又比方作区域分划。多线程绘制各区域上的物体)。


图形引擎能够基于CPU渲染,也能够基于GPU渲染。

就一般的UI渲染而言,CPU图形引擎优化得足够,倒也能满足要求,不会比GPU引擎差多少。

识别脏区域

与游戏界面的实时变换不同,对普通应用UI界面的渲染而言。大部分情况下一个页面的大部分面积处于不变状态。变化的区域又称脏区域。如何尽可能多地识别不变的部分,并作渲染规避,是UI引擎须要完毕的非常重要的工作。

比較理想的UI引擎的设计结构例如以下图:
UI引擎
应用开发人员能够在三个层次上去实现UI效果。

从上往下。自由度越来越高。开发难度也会越来越大。

Android的设计

UI引擎Android
Android并没有开发新的界面语言。而是採用xml+java的形式。由xml文件确定大致布局,java代码中做控制和微调。

Android没有明白的UI解析引擎,UI解析反映在View、Layout等类的实现中。


应用开发人员使用View的API(UI接口)、Canvas的API(引擎API)进行开发。

View

Android的控件和布局管理都抽象为View。

部分View用于布局解析(各种Layout),部分View用于管理(复合View),部分View是实际的控件(TextView、ImageView、WebView等)。
详细的渲染流程全然取决于应用所选择的View的子类。


全部View组成一个树,布局时逐层创建树节点,渲染时逐级渲染。当调用invalidate刷新View时。由下往上逐层上报dirty区域。


详细可看这篇文章。写得比較清楚:
http://blog.csdn.net/xu_fu/article/details/7829721
一个View不管其渲染流程如何,都必须保证其绘制内容固定在屏幕的指定范围。这是Android上层显示的设计原则。对于使用系统的图形引擎的应用。这能够通过在大图层上划分一块区域,设置裁剪范围而实现。但假设不使用系统图形引擎,就仅仅好新建一个图层,并将主图层相应位置挖洞。
在View的invalidate函数中。将须要重绘的View作标志。

并将其区域与上一级View的脏区域作合并,终于反映到ViewRootImpl的mDirty中来。


invalidate顺着View树脉络,一层一层往上刷新。
Invalidate
invalidate之后,该View即须要绘制。即是dirty的。

Canvas

Canvas是Android系统提供的图形引擎API,因为早期Android的图形渲染由Skia完毕,Canvas接口也与Skia的API非常像。
绝大部分控件使用Canvas的API进行界面渲染,如TextView、ImageView及用户自己定义,重载onDraw(Canvas canvas)的View。
比較特殊的是WebView。它不使用Canvas的API渲染,而是由Canvas获取Surface信息后,走web引擎渲染。

绘制主线

触发

众所周知,ViewRootImpl类的performTraversals方法。是全部界面布局、绘制的入口。但这种方法是怎么触发的呢?
在应用初起、View更新(触发invalidate)、动画、创建新Surface等情形下,会通过 scheduleTraversals 方法,向 Choreographer 类注冊一个回调,Choreographer 类是用来接受vsync信号的,这样,在LCD发出vsync信号之后(也即新一帧开启),该回调被运行,即doTraversal -> performTraversals。
详情參见:
http://blog.csdn.net/farmer_cc/article/details/18619429

注:
1、performTraversals的调用是应用级的。也就是说,有可能会有多个应用去调这个函数。

主流程

流程
1、计算总大小,创建一个Surface用于存储渲染结果。
2、进行布局測量。算出每一个View的范围。
3、进行layout,实例化全部子View。
4、一切就绪,运行渲染。
详细的看这篇文章吧:
http://blog.csdn.net/aaa2832/article/details/7849400
由这条绘制主线我们能够看出,跟View相关的一切操作,布局,初始化,渲染,全部在一个线程(事实上是主线程)完毕,假设在这个过程中,其它线程改动了View的属性值,便会造成布局计算后的结果与后面实际渲染的需求不一致。
Android里面对此的解决方式是限制,即众所周知的仅仅能在主线程更新UI。

渲染流程

软件渲染

drawSoftware
简洁明快的流程:
1、调 surface.lockCanvas,取得渲染入口Canvas。
2、从顶层View開始,按树递归调用View的draw方法。在draw方法中。全部View中的onDraw实现被调用。


3、调 surface.unlockCanvasAndPost
第1步相应的下层逻辑还是有点复杂的:
(1)dequeueBuffer获取一块新GraphicBuffer。
(2)将新GraphicBuffer锁定(lock),指明为CPU所訪问。
(3)优化:假设存在上一帧所渲染的GraphicBuffer,且长宽与当前窗体一致,那么复制上一帧非dirty区域的内容到新一帧。假设不存在。将dirty区域设为全屏(即全部区域都要渲染)。
(4)将GraphicBuffer映射为一个SkBitmap,相应创建一个SkCanvas与之绑定,SkCanvas设置裁剪区域为第(3)步得到的dirty区域。
(5)SkCanvas包装为上层的Canvas传回。
第3步相应的下层逻辑就是 queueBuffer。
请注意,不是仅仅须要绘制dirty的View的,因为View有可能会重叠,发生透明度混合,重叠部分影响到非dirty的View时,也应该绘制。Android并没有计算哪些View须要重绘。就笼统地让全部View运行onDraw方法。

软件渲染流程中,布局、渲染、事件响应全部集中在主线程,比較easy造成堵塞。

硬件渲染

为何要有硬件渲染这套流程。而不是仅改造图形引擎为用gpu的呢?
这是因为直接按软件渲染那套流程走下来,是不适合用gpu渲染的,强行换用OpenGL实现,效率会低得可怜。
硬件加速中draw的实如今ThreadedRenderer.java之中(这是5.0的,不同版本号可能有不同,重点看原理)。
1、把创建好的Surface扔给硬件加速的Renderer,供其初始化(eglCreateWindowSurface要用)。
2、更新显示列表(updateRootDisplayList):创建一个记录命令的Canvas,将View中对Canvas的draw操作变成记录命令,非dirty的View不须要又一次记录。
3、运行渲染(nSyncAndDrawFrame)。这一步是放渲染线程里面发一个任务,让其做一次绘制。一般不须要等渲染线程绘制完毕。


详细实如今 DrawTask的drawFrame函数。兴许章节详述:
frameworks/base/libs/hwui/renderthread/DrawFrameTask.cpp

从设计而言,硬件加速的渲染流程要比软件渲染流程好一些。显示列表的存在,给复合优化带来可能。即使不用gpu加速,也都有优势。

关于硬件加速几个常见问题和误区:
1、为何开启硬件加速要额外的内存?
非常多文章里面将其误觉得是开启OpenGL所须要的额外内存。事实上不然。OpenGL上下文的内存消耗不会达到MB级,这个额外内存是hwui引擎所须要的缓存。大头是字体。

详细大小能够通过设置系统属性改动。通过 adb shell getprop,可查看相关的属性(ro.hwui开头)。


HWUI
hwui内部机理是将文字解析到一个大的texture上,渲染详细文本时计算相应文字范围。取此texture中的一部分。

因此有一个宽/高的设置,不像skia里面是一维的大小。
关于为什么要有字体缓存。能够看一下这篇文章:
http://mobile.51cto.com/abased-442805.htm
另外。在Android系统内存不足时,会去部分回收这个Cache。
2、显示列表机制是否显著提升了UI渲染性能?
显著提升渲染性能靠的是GPU,显示列表机制是将GPU用上的一种方法。
因为Android早期API全部基于CPU渲染,因此在UI渲染时全部资源(最主要还是图像Bitmap)都在CPU所能訪问的内存中。GPU渲染时,必须要把相应的资源拷贝到显存中。这一个复制的过程。自然不希望每一帧时都做一遍。


保存全部命令及相应资源到一个显示列表上。然后回放,是一个可取的方案,其最大的优点是应用开发人员仍然能够按原先的API进行开发,仅仅须要打开一个开关就能用到硬件加速。
3、硬件加速能否够使全部的界面绘制都用上GPU?
答案是否。

请看以下的“非主线渲染”。

非主线渲染

就View层级设计而言。Android希望一个应用仅仅有一个图层,并在这个图层上布局全部的控件,并且应用不用感知这个图层的内存所在。最多调Canvas接口就可以,系统帮忙搞定图形渲染、Buffer循环、送显合成等繁琐事务。


但非常可惜,这样的方案不能满足全部需求:
1、对视频、照相等应用而言,它们须要直接訪问物理内存(主要是硬件解码器和ISP等须要),把它们的显示放到一个图层的部分区域,不太现实。
2、全部UI操作和绘制集中在主线程,即使是硬件加速,也须要在主线程创建显示列表,做动画时,easy堵塞事件响应。


3、这样的方案下,应用开发人员无法自己定义渲染流程,直接使用OpenGL等图形API进行开发。这样意味着使用不了游戏引擎。

SurfaceView应运而生,它的原理,就是打洞覆盖:另起一个图层(即新建一个Surface),并把主图层的相应区域置为透明,然后渲染就发生在新图层中,终于显示效果自然是依赖SurfaceFlinger的叠加。
使用方法參考:
http://blog.csdn.net/ithomer/article/details/7280968
当中,SurfaceHolder往下会相应着一个Buffer循环队列,这个是物理共享内存的抽象,因此能够做为视频、相机预览流的指定输入。
网上的教程中。SurfaceView的使用方法都在还有一个线程中,先lockCanvas。调用Canvas的接口绘制画面之后,调unlockCanvasAndPost。这样的方式,便是典型的调CPU引擎-Skia渲染的方式。
虽然应用开发人员能够用SurfaceView直接开发基于OpenGL渲染的程序(SurfaceHolder能够用于创建OpenGL上下文)。Google还是非常仁慈地提供了GLSurfaceView,这个类帮开发人员创建好了上下文和相应的渲染线程,开发人员能够直接在回调函数中使用OpenGL,简单非常多。

请注意:
1、SurfaceView不会自己主动起一个单独的线程去渲染,仅仅是这个View上面的渲染能够在随意线程完毕。开发人员执意在主线程去渲染这个View。也是能够的。就像曾经QQ某一版的引导页一样,CPU差一点的机器滑都滑不动(净给我们这些做系统优化的出难题)。
2、SurfaceView虽然能够把渲染流程移到还有一个线程运行,但它的存在同一时候添加了SurfaceFlinger的合成负担(图层数添加),不要以为这就是一个非常高效的View。假设是出于提升性能的目的而使用。请细致权衡一下得失。
3、硬件加速属性不影响SurfaceView的渲染方式,lockCanvas必定得到用CPU绘制的Canvas。要在SurfaceView中用上GPU渲染,仅仅好自已建上下文或用GLSurfaceView,接入3D引擎。

补充,2015.8.14之后。Android提供了一个lockHardwareCanvas方法。用此方法能够得到硬件加速的Canvas。Android 6.0上已经能够使用。这但是个大福音
4、SurfaceView系列的渲染流程不在performTraversals主线中,因此一般也不受vsync限制(当然,能够设计流程使之受限),也不会像主线渲染必须由invalidate触发。只是,假设渲染太快,在下层显示的窗体管理模块,能够使之堵塞在申请Buffer的步骤上。

Android的设计吐槽

Android的发展也有些年头了。图形显示部分更是一改再改,差点儿面目全非。总算是满足了手机硬件发展的需求,实现了一个比較高效。对开发人员相对友好的界面绘制系统,相对于其它系统来说,事实上也算优秀了。然而,作为一个逐渐演进的复杂系统,背负着不少历史的包袱。总会有各种各样的不合理。这里就来吐槽一下:
1、主线程单一管理界面
个人觉得的最大槽点,没有之中的一个。全部UI操作集中到一个线程后无法并行,而measure/layout/draw都是耗时大户。

在应用启动、屏幕旋转、列表滑动等场景。屡屡出现性能问题。ART模式开启。加快了java代码运行效率后,好了一些。但指标仍然不好看。

2、脏区域识别之后并没有充分地优化
软件渲染时。虽然限制了渲染区域,但全部View的onDraw方法一个不丢的运行了一遍。


硬件渲染时,避免了没刷新的View调onDraw方法更新显示列表。但显示列表中的命令仍然一个不落的在全屏幕上运行了一遍。


一个比較easy想到的优化方案就是为主流程中的View建立一个R-Tree索引,invalidate这一接口改动为能够传入一个矩形范围R,更新时。利用R-Tree索引找出包括R的全部叶子View,令这些View在R范围重绘一次就可以。

这个槽点事实上影响倒不是非常大,大部分情况下View不多。且假设出现性能问题。基本上都是一半以上的屏幕刷新。

3、图层分配方案比較浪费内存和内存传输带宽(DDR带宽)
下图是对小米平板上相机应用 dumpsys SurfaceFlinger的一个结果
小米相机
由上图能够看出。SurfaceView的Layer(相机的预览Surface)和com.android.camera的Layer(主渲染流程的Surface)是一样大的,都差点儿相同占了全屏。
但实际上。com.android.camera仅仅有几个图标,这个Layer绝大部分是透明的。考虑到TrippleBuffer机制,按透明部分约为1024*2048的大小算。就浪费了1024*2048*4*3=24M的内存。
并且在SurfaceFlinger作合成时,透明部分也要參与。按最省内存传输带宽的在线合成(仅仅须要一读)方式,预览帧按30fps算。透明部分所须要的DDR带宽就是8M*30/s = 240M/s。

一般手机上的DDR带宽才800M/s(高端手机应该有1600),这就占用了差点儿1/3。

posted on 2017-07-07 17:13  yutingliuyl  阅读(3384)  评论(0编辑  收藏  举报