Android图形显示系统——上层显示2:硬件加速实现

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: Android界面绘制的硬件加速实现Android的界面绘制的硬件加速采取上下整合的一套流程实现一、代码结构(一)JavaHardwareRenderer->ThreadedRenderer:组织硬件加速渲染的类,下发创建显示列表和回放的指令。 GLES20RecordngCanvas GLES20Canvas HardWareCanvas:与

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的显示列表设计

DisplayList
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改成支持多线程分别使用,需要作不少手术。

如图所示:
Cache
上层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都包含的):
TextCache
代码见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就是存储这个模板的。
Demo

在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清除掉
Cache流程
请注意:
Program是不清的。在内存依然紧张时,会在上层直接摧毁OpenGL上下文。

四、Android硬件加速机制评价

(一)优点

1、完备的GPU绘制流程,在上层API不变的前提下,妥善解决了2D渲染的性能问题
2、延迟渲染合并了大量的渲染指令,drawCall调用少效率高,且有一定的防止过度绘制的功能
3、有一层一层回收缓存的机制
4、相当好的基于OpenGLES 2D 的引擎范本,很多代码(比如:纹理上传、PBO、曲纹细分)很有参考价值。

(二)槽点

1、上下层耦合关系严重,对上依赖于Java层的合理调用,对下依赖于Skia,不容易提供单独的基于硬件加速的2D渲染引擎,容易出现内存/资源泄露
2、资源在CPU和GPU中均做Cache,占用内存较多:显示列表中的图片资源和纹理图片同时存在,字体三重缓存
3、延迟渲染机制做得还是不够好,消除过度绘制的能力有限,而且每帧都要算一次延迟渲染信息。

相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
目录
相关文章
|
2月前
|
JavaScript 前端开发 Java
[Android][Framework]系统jar包,sdk的制作及引用
[Android][Framework]系统jar包,sdk的制作及引用
47 0
|
1天前
|
IDE Android开发 iOS开发
探索安卓与iOS系统的技术差异:开发者的视角
本文深入分析了安卓(Android)与苹果iOS两大移动操作系统在技术架构、开发环境、用户体验和市场策略方面的主要差异。通过对比这两种系统的不同特点,旨在为移动应用开发者提供有价值的见解,帮助他们在不同平台上做出更明智的开发决策。
|
1天前
|
Ubuntu Shell API
Ubuntu 64系统编译android arm64-v8a 的openssl静态库libssl.a和libcrypto.a
Ubuntu 64系统编译android arm64-v8a 的openssl静态库libssl.a和libcrypto.a
|
19天前
|
监控 Android开发 iOS开发
深入探索安卓与iOS的系统架构差异:理解两大移动平台的技术根基在移动技术日新月异的今天,安卓和iOS作为市场上最为流行的两个操作系统,各自拥有独特的技术特性和庞大的用户基础。本文将深入探讨这两个平台的系统架构差异,揭示它们如何支撑起各自的生态系统,并影响着全球数亿用户的使用体验。
本文通过对比分析安卓和iOS的系统架构,揭示了这两个平台在设计理念、安全性、用户体验和技术生态上的根本区别。不同于常规的技术综述,本文以深入浅出的方式,带领读者理解这些差异是如何影响应用开发、用户选择和市场趋势的。通过梳理历史脉络和未来展望,本文旨在为开发者、用户以及行业分析师提供有价值的见解,帮助大家更好地把握移动技术发展的脉络。
|
16天前
|
Dart 开发工具 Android开发
在 Android 系统上搭建 Flutter 环境的具体步骤是什么?
在 Android 系统上搭建 Flutter 环境的具体步骤是什么?
|
1月前
|
Android开发 UED 开发者
Android经典实战之WindowManager和创建系统悬浮窗
本文详细介绍了Android系统服务`WindowManager`,包括其主要功能和工作原理,并提供了创建系统悬浮窗的完整步骤。通过示例代码,展示了如何添加权限、请求权限、实现悬浮窗口及最佳实践,帮助开发者轻松掌握悬浮窗开发技巧。
75 1
|
2月前
|
安全 Android开发 iOS开发
安卓与iOS的终极对决:哪个系统更适合你?
在智能手机的世界里,安卓和iOS两大操作系统如同两座巍峨的山峰,各自拥有庞大的用户群体。本文将深入浅出地探讨这两个系统的优缺点,并帮助你找到最适合自己的那一款。让我们一起揭开这场技术盛宴的序幕吧!
|
5天前
|
XML 存储 Java
探索安卓开发之旅:从基础到进阶
【9月更文挑战第37天】安卓开发,一个充满无限可能的领域。它不仅关乎技术的深度与广度,更关乎开发者的成长与突破。本文将带你走进安卓开发的世界,从基础知识的学习到进阶技巧的掌握,一起感受编程的魅力与乐趣。
|
15天前
|
IDE Android开发 iOS开发
探索Android与iOS开发的差异:平台选择对项目成功的影响
【9月更文挑战第27天】在移动应用开发的世界中,Android和iOS是两个主要的操作系统平台。每个系统都有其独特的开发环境、工具和用户群体。本文将深入探讨这两个平台的关键差异点,并分析这些差异如何影响应用的性能、用户体验和最终的市场表现。通过对比分析,我们将揭示选择正确的开发平台对于确保项目成功的重要作用。
|
2天前
|
Android开发
Android开发表情emoji功能开发
本文介绍了一种在Android应用中实现emoji表情功能的方法,通过将图片与表情字符对应,实现在`TextView`中的正常显示。示例代码展示了如何使用自定义适配器加载emoji表情,并在编辑框中输入或删除表情。项目包含完整的源码结构,可作为开发参考。视频演示和源码详情见文章内链接。
16 4
Android开发表情emoji功能开发