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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 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盲盒。
目录
相关文章
|
19天前
|
缓存 Java Shell
Android 系统缓存扫描与清理方法分析
Android 系统缓存从原理探索到实现。
43 15
Android 系统缓存扫描与清理方法分析
|
10天前
|
算法 JavaScript Android开发
|
12天前
|
安全 搜索推荐 Android开发
揭秘安卓与iOS系统的差异:技术深度对比
【10月更文挑战第27天】 本文深入探讨了安卓(Android)与iOS两大移动操作系统的技术特点和用户体验差异。通过对比两者的系统架构、应用生态、用户界面、安全性等方面,揭示了为何这两种系统能够在市场中各占一席之地,并为用户提供不同的选择。文章旨在为读者提供一个全面的视角,理解两种系统的优势与局限,从而更好地根据自己的需求做出选择。
28 2
|
20天前
|
安全 搜索推荐 Android开发
揭秘iOS与Android系统的差异:一场技术与哲学的较量
在当今数字化时代,智能手机操作系统的选择成为了用户个性化表达和技术偏好的重要标志。iOS和Android,作为市场上两大主流操作系统,它们之间的竞争不仅仅是技术的比拼,更是设计理念、用户体验和生态系统构建的全面较量。本文将深入探讨iOS与Android在系统架构、应用生态、用户界面及安全性等方面的本质区别,揭示这两种系统背后的哲学思想和市场策略,帮助读者更全面地理解两者的优劣,从而做出更适合自己的选择。
|
11天前
|
安全 搜索推荐 程序员
深入探索Android系统的碎片化问题及其解决方案
在移动操作系统的世界中,Android以其开放性和灵活性赢得了广泛的市场份额。然而,这种开放性也带来了一个众所周知的问题——系统碎片化。本文旨在探讨Android系统碎片化的现状、成因以及可能的解决方案,为开发者和用户提供一种全新的视角来理解这一现象。通过分析不同版本的Android系统分布、硬件多样性以及更新机制的影响,我们提出了一系列针对性的策略,旨在减少碎片化带来的影响,提升用户体验。
|
11天前
|
安全 Android开发 iOS开发
深入探索iOS与Android系统的差异性及优化策略
在当今数字化时代,移动操作系统的竞争尤为激烈,其中iOS和Android作为市场上的两大巨头,各自拥有庞大的用户基础和独特的技术特点。本文旨在通过对比分析iOS与Android的核心差异,探讨各自的优势与局限,并提出针对性的优化策略,以期为用户提供更优质的使用体验和为开发者提供有价值的参考。
|
13天前
|
安全 Android开发 iOS开发
安卓系统与iOS系统的比较####
【10月更文挑战第26天】 本文将深入探讨安卓(Android)和iOS这两大主流移动操作系统的各自特点、优势与不足。通过对比分析,帮助读者更好地理解两者在用户体验、应用生态、系统安全等方面的差异,从而为消费者在选择智能手机时提供参考依据。无论你是技术爱好者还是普通用户,这篇文章都将为你揭示两大系统背后的故事和技术细节。 ####
32 0
|
前端开发 Android开发
Android图形绘制基础(二)
mainActivity如下: package com.cn; import android.os.Bundle; import android.
774 0
|
前端开发 Android开发
Android图形绘制基础(一)
mainActiviry如下: package com.cn; import android.app.Activity; import android.
943 0
|
1天前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。