Android界面绘制的硬件加速实现
Android的界面绘制的硬件加速采取上下整合的一套流程实现
一、代码结构
(一)Java
HardwareRenderer->ThreadedRenderer:组织硬件加速渲染的类,下发创建显示列表和回放的指令。
GLES20RecordngCanvas GLES20Canvas HardWareCanvas:与Canvas平级的UI渲染引擎支持,但这个Canvas只能存储命令到显示列表中,并在ThreadedRenderer中的渲染线程辅助下运行。
RenderNode:所有View对应一个构成一个RenderNode
RenderNodeAnimator:动画用
HardwareLayer:调saveLayer时产生,缓存绘制内容为一个Layer
(二)Hwui
DisplayListRenderer:对应于HardWareCanvas,创建显示列表的类
RenderNode:一个渲染节点,包含绘制命令和相关资源
BaseRenderNodeAnimator:动画用
RenderProxy:由于OpenGL上下文是线程私有的,需要使用到OpenGL的操作都必须在同一线程。这个类的作用就是按Commander模式做一个中转,把事务转移到持有上下文的线程中执行。
Layer:对应于上层的HardwareLayer,实际上是缓存绘制过的内容到一张纹理上,至于如何缓存,有fbo方式和copytex方式
OpenGLRenderer:这个类并不直接被上层调用,但它是执行实际渲染任务的入口类,定位性能问题一般直接从这个类看起。
PS:建议仔细看看RenderProxy.cpp里面的Commander模式实现方法,确实相当之精妙简洁,不过感觉C11有匿名函数后不需要这么麻烦了。
二、Hwui引擎设计
(一)显示列表
1、显示列表设计难题
很多介绍显示列表机制的文章都是一带而过,仿佛得到一个显示列表并回放是很简单的事情。但真正动手写时,就会发现有很多问题:
1、如何存储每个API及相关参数?为每个API创建一个类,回放时调类方法?还是把API及参数作一个编码,然后回放时解码,用虚拟机的方式执行?用前者实现比较简单,扩展相对容易,但每个API建一个类,需要非常大的代码量(越多的代码意味着越容易出错);用后者,需要构建编码解码的逻辑,总体代码量较少,但是虚拟机处理中switch case代码冗长,且不容易作扩展。
2、资源怎么处理?这个是最为棘手的,如果拷贝资源,会大幅降低效率,不可取,但如果不拷贝,上层在传入资源后马上修改这些资源,回放时结果会是错误的(如传入Bitmap A之后,调用drawBitmap之后,马上修改A的内容,回放时A就是修改后的)。原则上自然是不拷贝,但如何约束上层行为呢?
2、Hwui的显示列表设计
DrawOp即产生渲染效果的算符,StateOp为产生状态变更的算符,须与后续的DrawOp配合使用。
之所以采用独立成类的设计方式,是为了满足批处理优化的需要。
Resouces保留在对应的ResouceCaches中。
(1)DrawOp
DrawOp为渲染算符,分的子类较多,主要是以下几顶:
callDrawGLFunction->DrawFunctorOp:
用于WebKit/chromium的硬件加速渲染,WebKit/chromium浏览器内核中将基于opengl的渲染代码封装为函数Functor,传入hwui引擎中执行。
DrawSomeTextOp:
绘制文本的算符,归结为一个算符的原因是文本解析的步骤是统一的
DrawColorOp:
将区域刷成指定颜色的算符
DrawBoundedOp:
drawRect、drawBitmap及一般的drawPath均继承于此算符,其特点是渲染存在边界。可以设法判断是否覆盖
DrawLayerOp:
绘制Layer
DrawShadowOp:
绘制阴影
(2)StateOp
保存/恢复状态/建层:SaveOp/RestoreToCountOp/SaveLayerOp
矩阵变换相关:TranslateOp/RotateOp/SkewOp/SetMatrixOp/ConcatMatrixOp
设置裁剪区域的算符:ClipOp/ClipRectOp/ClipPathOp/ClipRegionOp
设置Paint的采样模式:ResetPaintFilterOp/SetupPaintFilterOp
(二)渲染缓存
Hwui的缓存是比较复杂的,一方面,由于采用基于显示列表的异步渲染机制,用于渲染的资源本身需要在列表中缓存。另一方面,由于GPU/显卡渲染的异构性,其所需要的资源必须要由显示列表中的资源上传或映射而来,上传的资源和映射关系本身构成显存的缓存。
Caches 作为单例,存储了所有的渲染缓存,主要内容如下:
TextureCache textureCache;
LayerCache layerCache;
RenderBufferCache renderBufferCache;
GradientCache gradientCache;
ProgramCache programCache;
PathCache pathCache;
PatchCache patchCache;
TessellationCache tessellationCache;
TextDropShadowCache dropShadowCache;
FboCache fboCache;
ResourceCache resourceCache;
这种单例设计模式自然完全没有考虑同一进程中可能有多个线程使用Hwui的情况,因此如果要将Hwui改成支持多线程分别使用,需要作不少手术。
如图所示:
上层Canvas的API中所夹带的资源,创建显示列表时在ResourceCache中缓存一次(Bitmap仅引用,其余的全部拷贝),在回放显示列表时再继续构建各自对应的Cache。
Caches中的所有缓存,除resourceCache之外的不妨统称为EngineCache。这个缓存关系就是:
API(Java Virtual Machine)——ResourceCache——EngineCache
ResourceCache
缓存匹配的查询机制都是依靠指针,由于Path、Paint等资源中会夹带Effect、Shader等特效,当应用层修改这些东西后,由于指针没变,缓存无法感知其变化而更新。
因此在Skia里面为SkPath、SkPaint加入了generationId,当它们附带的特效发生改变时,这个id同时修改,依此来校验API-ResourceCache,ResourceCache—EngineCache是否一致,若不一致自然是要重新再拷贝一遍/重新生成一次Cache。
由于ResourceCache不复制Bitmap,必须要防止在渲染过程中上层把Bitmap给释放/修改掉。
但它只防止了释放,并没有阻止修改的实现,因此这个只能靠应用开发者自觉。
代码见 frameworks/base/core/jni/android/graphics/Bitmap.cpp
static jboolean Bitmap_recycle(JNIEnv* env, jobject, jlong bitmapHandle) {
SkBitmap* bitmap = reinterpret_cast<SkBitmap*>(bitmapHandle);
#ifdef USE_OPENGL_RENDERER
if (android::uirenderer::Caches::hasInstance()) {
bool result;
result = android::uirenderer::Caches::getInstance().resourceCache.recycle(bitmap);
return result ? JNI_TRUE : JNI_FALSE;
}
#endif // USE_OPENGL_RENDERER
bitmap->setPixels(NULL, NULL);
return JNI_TRUE;
}
ResourceCache不包含Bitmap(虽然会阻止上层回收),占用内存还是很少的,缓存大头还在 EngineCache
Paint/Shader——ProgramCache
Hwui用的是2.0以上的OpenGLES版本,着色器的构建是很重要的部分。不过,2D绘图的着色器相对也较简单。
这里的设计思想是先翻译 SkPaint 及其中的 SkShader为 ProgramDescription 结构,然后由 ProgramCache 去根据这个结构,选择合适的着色器语言片断,拼装起来,组成 GLProgram
SkShader 是一个父类,包含 Bitmap shader,Gradient shader 等好几类,因此这里对每一类都要有对应的函数去解析。
主要函数:
SkiaShader::describe
ProgramCache::generateVertexShader
ProgramCache::generateFragmentShader
至于ProgramCache的着色器代码怎么写的,用的正交投影还是透视投影,纹理贴图怎么实现等,这里就不详述了。
图片——TextureCache
(1)普通图片——SkBitmap
代码参考TextureCache::get 和 TextureCache::generateTexture
基础的纹理上传,不多述。
(2)资源图集——AssetAtlas
这个是Android 4.3起引入的机制,将预加载所得的图片,先整合到一张 GraphicBuffer 上,转变为一张EGLImage。然后各应用在使用硬件加速渲染UI时,将此EGLImage映射为自身的OpenGL纹理,从而免去这部分资源纹理上传的过程,且由于应用间共享纹理,节省了内存。
详细看老罗的博客吧,虽然个人感觉把这一个简单的功能讲太细了:
http://blog.csdn.net/luoshengyang/article/details/45831269
文字——FontRenderer
文字/文本绘制对任何一个2D渲染引擎来说,都是一个棘手的事。主要是因为文本解析本身需要大量的时间,肯定需要缓存,但使用缓存的话,由于各个文字在各种字体下的解析结果都不一样,全缓存进来内存耗费极高,是不可能的。
没有什么完美的方案去设计一个文本缓存机制,正好比没有绝对正确的企业管理模式。
Hwui中是这么处理的:
缓存的设计(这个是每一个FontRenderer都包含的):
代码见FontRenderer::initTextTexture
对Skia解析出来的字形SkGlyph,会按PixelBuffer 由小到大逐次去找一个对应位置,然后复制上去,如果是有变换需要(mGammaTable存在),则在这个过程顺便把gammatable变换做了。
在后面渲染时,PixelBuffer会上传为Texture,然后GPU就可以使用字形渲染的结果了。
至于 mGammaTable,可详细看 GammaFontRenderer 和 Lookup3GammaFontRenderer 类。
PixelBuffer 根据 设备支持的OpenGL ES 版本和属性配置(ro.hwui.use_gpu_pixel_buffers)选用CpuPixelBuffer或GpuPixelBuffer(需要3.0以上版本和属性开关开启)。CpuPixelBuffer就是malloc出来的内存,GpuPixelBuffer是PBO。OpenGLES 3.0 标准有PBO映射为CPU内存的API(glMapBufferRange),会提升缓存过程中上传的效率。
(注:GpuPixelBuffer这一段代码也是使用PBO的好教材,需要了解PBO如何使用的可以参考下)。
在渲染时先根据已经缓存好的字形位置,算出纹理采样的坐标,塞进对应cache的vbo,然后遍历所有的cacheTexture,渲染包含有待渲染文字的cache即可。
代码见 FontRenderer::issueDrawCommand
延迟渲染模式下,不管是多少个字,始终是根据cache数来调drawCall,这样,drawCall的调用次数就比较少了。
出于内存优化的考虑,中间一层 PixelBuffer 是可以不要的,但相应地就要在外面把 gammaTable 映射做掉,逻辑会复杂一些。
路径Path和TessellationCache
Hwui引擎中实现drawPath时,没有自己去计算路径点,而是调用skia的drawPath接口绘制一张A8的模板,然后按模板把Shader混合进去。对应的PathCache就是存储这个模板的。
在Android 4.0时,绘制圆角矩形、圆形等特殊形状时,是按drawPath的方式,生成模板再混合,这种方式需要占用内存,且不是很效率,因此后面Hwui中加入了处理形体的功能,这就是曲纹细分器Tessellator,它通过解析SkPath,生成一系列顶点来描述形体。
目前主要支持凸形状(详见PathTessellator的实现),目前细分过程仍然是靠CPU实现的,在未来手机上的GPU支持曲纹细分的Shader后,可以把这部分工作转移到GPU上。
TessellationCache就是曲纹细分器生成的vbo(vetex buffer object),相对于模板(一张A8纹理)而言节省不少内存,且执行时一般效率更高(曲纹细分方式由于顶点数多,Vertex Shader负荷较大,但相对于模板方式,Fragment Shader负荷较小,内存带宽占用较少)。
Layer——LayerCache
(略,以后有空再补)
三、基本流程
总体流程
关于显示列表的创建过程可以参考老罗博客:
http://blog.csdn.net/luoshengyang/article/details/45943255
延迟渲染
延迟渲染是在回放显示列表时,先做一步预处理(defer),然后再执行处理后的命令(flush)。
status_t OpenGLRenderer::drawRenderNode(RenderNode* renderNode, Rect& dirty, int32_t replayFlags) {
/*.......*/
DeferredDisplayList deferredList(*currentClipRect(), avoidOverdraw);
DeferStateStruct deferStruct(deferredList, *this, replayFlags);
renderNode->defer(deferStruct, 0);
/*.......*/
return deferredList.flush(*this, dirty) | status;
}
/*.......*/
}
看这段回放的主代码可以知道,延迟渲染是先创建一个延迟渲染列表,然后把显示列表中的命令全部往里面加进去(这个过程中做预处理),然后交由延迟渲染列表去回放(flush)。
作用
预处理的作用主要是:
(1)合并渲染,减少drawCall调用
(2)避免部分的过度绘制
过度绘制/OverDraw是指同一个像素被渲染多次的情形。解决OverDraw的方法要使用命令列表(显示列表),对列表中每个绘制命令计算其涵盖区域。然后是计算重复渲染的区域,设法将这个区域上面的绘制命令合并
Defer信息获取
DrawOp算子需要实现onDefer这个方法,为 DeferredDisplayList 提供两个信息:DeferInfo和DeferredDisplayState。
DeferInfo反映这个DrawOp算子本身的性质(能否合并,是否透明,归属哪一类),DeferredDisplayState则是结合算子所处的矩阵变换状态,反映该算子在最终显示屏的地位(渲染边界、矩阵变换)
Defer的作用体现
Hwui中,避免过度绘制的条件很苛刻,需要完全不透明且完全覆盖,因此Defer作用主要体现在合并渲染上了。
支持合并渲染的DrawOp需要实现一个特殊的multiDraw函数,用以将同类一系列DrawOp的渲染在同一函数完成。
目前所看到合并渲染仅限于绘制AssetAtlas资源的操作合并与多次绘制文字绘制的合并。
资源回收
当应用内存不足时,会尽量去回收内存,其中Hwui所占的Cache在回收的范围之内,最终调用Caches::flush回收,有三种模式:
kFlushMode_Layers:
清除LayerCache和RenderBufferCache
kFlushMode_Moderate:
除上面外,清除部分字体缓存、图片纹理、路径纹理
kFlushMode_Full:
字体缓存全清,再把fbo、dither清除掉
请注意:
Program是不清的。在内存依然紧张时,会在上层直接摧毁OpenGL上下文。
四、Android硬件加速机制评价
(一)优点
1、完备的GPU绘制流程,在上层API不变的前提下,妥善解决了2D渲染的性能问题
2、延迟渲染合并了大量的渲染指令,drawCall调用少效率高,且有一定的防止过度绘制的功能
3、有一层一层回收缓存的机制
4、相当好的基于OpenGLES 2D 的引擎范本,很多代码(比如:纹理上传、PBO、曲纹细分)很有参考价值。
(二)槽点
1、上下层耦合关系严重,对上依赖于Java层的合理调用,对下依赖于Skia,不容易提供单独的基于硬件加速的2D渲染引擎,容易出现内存/资源泄露
2、资源在CPU和GPU中均做Cache,占用内存较多:显示列表中的图片资源和纹理图片同时存在,字体三重缓存
3、延迟渲染机制做得还是不够好,消除过度绘制的能力有限,而且每帧都要算一次延迟渲染信息。