共计 6205 个字符,预计需要花费 16 分钟才能阅读完成。
作者:史健平(楚奕)
上篇回顾:《 淘宝小部件:全新的凋谢卡片技术!》、《淘宝小部件在 2021 双十一中的规模化利用》
本文次要从技术视角论述 Canvas 在小部件下的渲染原理。
在进入注释之前须要先解释下什么是【小部件】,小部件是淘宝模块 / 卡片级的凋谢解决方案,其次要面向私域提供类小程序的规范 & 统一化的生产、凋谢、经营等能力,它有着多种业务状态,如商品卡片、权利卡片以及互动卡片等等,ISV 开发的小部件能够以极低成本部署到店铺、详情、订阅等业务场景,极大进步了经营 & 散发效率。
从端上技术视角看,小部件首先是一个业务容器,它的特点是DSL 标准化、跨平台渲染、跨场景流通:
- DSL 标准化是指小部件齐全兼容小程序的 DSL(不仅仅是 DSL,还包含原子 API 能力、生产链路等等),开发者不须要额定学习即可疾速上手;
- 跨平台渲染顾名思义,小部件内核 (基于 weex2.0) 通过相似 flutter 自绘的计划能够在 Android、iOS 等不同操作系统上渲染出完全一致的成果,开发者不须要关怀兼容性问题;
- 最初跨场景流通是指小部件容器能够『嵌入』到多种技术栈的其余业务容器中,比方 Native、WebView、小程序等等,以此做到对开发者屏蔽底层容器差别并达到一次开发,多处运行的成果。
独一无二,Canvas 在小部件下的技术计划与小部件容器嵌入其余业务容器的技术计划竟然有不少相似之处,那么下边笔者就从 Canvas 渲染方面开展来讲一讲。
原理揭秘
端侧整体技术架构
小部件技术侧的整体架构如下图所示,宏观看可分为 “ 壳 ” 与 “ 核 ” 两层。
“ 壳 ” 即小部件容器,次要包含 DSL、小部件 JSFramework、原子 API 以及扩大模块比方 Canvas。
“ 核 ” 为小部件的内核,基于全新的 weex2.0。在 weex1.0 中咱们应用类 RN 的原生渲染计划,而到了 weex2.0 与时俱进降级到了类 Flutter 的自绘渲染计划,因而 weex2.0 承当了小部件 JS 执行、渲染、事件等外围职责,并细分为 JS 脚本引擎、Framework 与渲染引擎三模块。JS 引擎在 Android 侧应用轻量的 QuickJS,iOS 侧应用 JavaScriptCore,并且反对通过 JSI 编写与脚本引擎无关的 Bindings;Framework 层提供了与浏览器统一的 CSSOM 和 DOM 能力,此外还有 C ++ MVVM 框架以及一些 WebAPI 等等(Console、setTimeout、…);最初是外部称之为 Unicorn 的渲染引擎,次要提供布局、绘制、合成、光栅化等渲染相干能力,Framework 与渲染引擎层均应用 C ++ 开发,并对平台进行了相干形象,以便更好的反对跨平台。
值得一提的是,unicorn 渲染引擎内置了 PlatformView 能力,它容许在 weex 渲染的 Surface 上嵌入另一 Surface,该 Surface 的内容齐全由 PlatformView 开发者提供,通过这种扩大能力,Camera、Video 等组件得以低成本接入,Canvas 也正是基于此能力将小程序下的 Native Canvas(外部称之为 FCanvas)疾速迁徙到小部件容器。
多视角看渲染流程
更多细节还能够参考笔者先前的文章《跨平台 Web Canvas 渲染引擎架构的设计与思考(内含实现计划)》
到了本文的重点,首先仍然从宏观角度看下 Canvas 大体的渲染流程,请看上面图示,咱们从右到左看。
对开发者而言,间接接触到的是 Canvas API,包含 w3c 制订 Canvas2D API 以及 khronos group 制订的 WebGL API,它们别离通过 canvas.getContext(‘2d’)和 canvas.getContext(‘webgl’) 取得,这些 JS API 会通过 JSBinding 的形式绑定到 Native C++ 的实现,2D 基于 Skia 实现而 WebGL 则间接调用 OpenGLES 接口。图形 API 须要绑定平台窗体环境即 Surface,在 Android 侧能够是 SurfaceView 或是 TextureView。
再往左是小部件容器层。对 weex 而言,渲染合成的根本单位是 LayerTree,它形容了页面层级构造并记录了每个节点绘制命令,Canvas 就是这颗 LayerTree 中的一个 Layer — PlatformViewLayer(此 Layer 定义了 Canvas 的地位及大小信息),LayerTree 通过 unicorn 光栅化模块合成到 weex 的 Surface 上,最终 weex 和 Canvas 的 Surface 均参加 Android 渲染管线渲染并由 SurfaceFlinger 合成器光栅化到 Display 上显示。
以上是宏观的渲染链路,下边笔者试着从 Canvas/Weex/Android 平台等不同视角别离描述下整个渲染流程。
Canvas 本身视角
从 Canvas 本身视角看,能够临时疏忽平台与容器局部,要害之处有两点,一是 Rendering Surface 的创立,二是 Rendering Pipeline 流程。以下通过时序图的形式展现了这一过程,其中共波及四个线程,Platform 线程(即平台 UI 线程)、JS 线程、光栅化线程、IO 线程。
- Rendering Surface Setup: 当收到上游创立 PlatformView 的音讯时,会先异步在 JS 线程绑定 Canvas API,随后在 Platform 线程创立 TextureView/SurfaceView。当收到 SurfaceCreated 信号时,会在 Raster 线程提前初始化 EGL 环境并与 Surface 绑定,此时 Rendering Surafce 创立实现,告诉 JS 线程环境 Ready,能够进行渲染了。与 2D 不同的是,如果是 WebGL Context,Rendering Surace 默认会在 JS 线程创立(未开启 Command Buffer 状况下);
- Rendering Pipeline Overview: 开发者收到该 Ready 事件后,能够拿到 Canvas 句柄进而通过 getContextAPI 抉择 2d 或者 WebGL Rendering Context。对于 2d 来说,开发者在 JS 线程调用渲染 API 时,仅仅是记录了渲染指令,并未进行渲染,真正的渲染产生在光栅化线程,而对于 WebGL 来说,默认会间接在 JS 线程调用 GL 图形 API。不过无论是 2d 还是 WebGL 渲染均是由平台 VSYNC 信号驱动的,收到 VSYNC 信号后,会发送 RequestAnimationFrame 音讯到 JS 线程,随后真正开始一帧的渲染。对于 2D 来说会在光栅化线程回放先前的渲染指令,提交实在渲染命令到 GPU,并 swapbuffer 送显,而 WebGL 则间接在 JS 线程 swapbuffer 送显。如果须要渲染图片,则会在 IO 线程下载并进行图片解码最终在 JS 或者光栅化线程被应用。
Weex 引擎视角
从 Weex 引擎视角看,Canvas 属于扩大组件,Weex 甚至都感知不到 Canvas 的存在,它只晓得以后页面有一块区域是通过 PlatformView 形式嵌入的,具体是什么内容它并不关怀,所有的 PlatformView 组件的渲染流程都是统一的。
上面这张图左半局部形容了 Weex2.0 渲染链路的外围流程: 小部件 JS 代码通过脚本引擎执行,通过 weex CallNative 万能 Binding 接口将小部件 DOM 构造转为一系列 Weex 渲染指令(如 AddElement 创立节点、UpdateAttrs 更新节点属性等等),随后 Unicorn 基于渲染指令还原为一颗动态的节点树(Node Tree),该树记录了父子关系、节点本身款式 & 属性等信息。动态节点树会在 Unicorn UI 线程进一步生成 RenderObject 渲染树,渲染树通过布局、绘制等流程生成多张 Layer 组合成为 LayerTree 图层构造,通过引擎的 BuildScene 接口将 LayerTree 发送给光栅化模块进行合成,最终渲染到 Surface 上并通过 SwapBuffer 送显。
右半局部是 Canvas 的渲染流程,大体流程上边 Canvas 视角已介绍过,不再赘述,这里关注 Canvas 的嵌入计划,Canvas 是通过 PlatformView 机制嵌入的,其在 Unicorn 中会生成对应的 Layer,参加后续合成,不过 PlatformView 有多种实现计划,每种计划的流程天壤之别,下边开展讲一下。
Weex2.0 在 Android 平台提供了多种 PlatformView 嵌入的技术计划,这里介绍下其中两种:VirtualDisplay 与 Hybrid Composing,除此之外还有自研的挖洞计划。
VirtualDisplay
此模式下 PlatformView 内容最终会转为一张内部纹理参加 Unicorn 的合成流程,具体过程:首先创立 SurfaceTexture,并存储到 Unicorn 引擎侧,随后创立 android.app.Presentation,将 PlatformView(比方 Canvas TextureView)作为 Presentation 的子节点,并渲染到 VirtualDisplay 上。家喻户晓 VirtualDisplay 须要提供一个 Surface 作为 Backend,那么这里的 Surface 就是基于 SurfaceTexture 创立。当 SurfaceTexture 被填充内容后,引擎侧收到告诉并将 SurfaceTexture 转 OES 纹理,参加到 Unicorn 光栅化流程,最终与其余 Layer 一起合成到 Unicorn 对应的 SurfaceView or TextureView 上。
此模式性能尚可,然而次要弊病是无奈响应 Touch 事件、失落 a11y 个性以及无奈取得 TextInput 焦点,正是因为这些兼容性问题导致此计划利用场景比拟受限。
Hybrid Composing
在此模式下小部件不再渲染到 SurfaceView or TextureView 上,而是被渲染到一张或者多张由 android.media.ImageReader 关联的 Surface 上。Unicorn 基于 ImageReader 封装了一个 Android 自定义 View,并应用 ImageReader 生产的 Image 对象作为数据源,一直将其转为 Bitmap 参加到 Android 原生渲染流程。那么,为啥有可能是多个 ImageReader?因为有布局层叠的可能性,PlatformView 上边和下边均有可能有 DOM 节点。与之对应的是,PlatformView 本身 (比方 Canvas) 也不再转为纹理而是作为一般 View 同样参加 Android 平台的渲染流程。
Hybrid Composing 模式解决了 VirtualDisplay 模式的大部分兼容性问题,然而也带来了新的问题,此模式次要弊病有两点,一是须要合并线程,启用 PlatformView 后,Raster 线程的工作会抛至 Android 主线程执行,增大了主线程压力;二是基于 ImageReader 封装的 Android 原生 View(即下文提到的 UnicornImageView)须要一直创立 Bitmap 并绘制,特地是在 Android 10 以前须要通过软件拷贝的形式生成 Bitmap,对性能有肯定影响。
综合来看 Hyrbid Composing 兼容性更好,因而目前引擎默认应用该模式实现 PlatformView。
Android 平台视角
上面笔者试着进一步以 Android 平台视角从新扫视下这一过程(以 Weex + Hybrid Composing PlatformView 模式为例)。
上边提到,Hybrid Composing 模式下小部件被渲染到一张或多张 Unicorn ImageView,依照 Z -index 从上到下排列是 UnicornImageView(Overlay) -> FCanvasTextureView -> UnicornImageView(Background) -> DecorView,那么从 Android 平台视角看,视图构造如上图所示。Android 根视图 DecorView 下嵌套 weex 根视图(UnicornView),其中又蕴含多个 UnicornImageView 和一个 FCanvasPlatformView (TextureView)。
从平台视角看,咱们甚至不须要关怀 UnicornImageView 和 FCanvas 的内容,只须要晓得它们都是继承自 android.view.View 并且都遵循 Android 原生的渲染流程。原生渲染是由 VSYNC 信号进行驱动,通过 ViewRootImpl#PerformTraversal 顶级函数触发测量 (Measure)、布局(Layout)、绘制(Draw) 流程,以绘制为例,音讯首先散发到根视图 DecorView,并自顶向下散发 (dispatchDraw) 顺次回调每个 View 的 onDraw 函数。
- 对于 FCanvas PlatformView 来说,它是一个 TextureView,其本质上是一个 SurfaceTexture,当 SurfaceTexture 发现新的内容填充其外部缓冲区后,会触发 frameAvailable 回调,告诉视图 invalidate,随后在 Android 渲染线程通过 updateTexImage 将 SurfaceTexture 转为纹理并交由零碎合成;
- 对于 UnicornImageView 来说,它是一个自定义 View,其本质上是对 ImageReader 的封装,当 ImageReader 关联的 Surface 外部缓冲区被填充内容后,能够通过 acquireLatestImage 取得最新帧数据,在 UnicornImageView#onDraw 中,正是将最新的帧数据转为 Bitmap 并交给 android.graphics.Canvas 渲染。
而 Android 本身的 View Hierarchy 也关联着一块 Surface,通常称之为 Window Surface。上述 View Hierarchy 经由绘制流程之后,会生成 DisplayList,并在 Android 渲染线程经由 HWUI 模块解析 DisplayList 生成理论图形渲染指令交由 GPU 进行硬件渲染,最终内容均绘制到上述 Window Surface,而后与其余 Surface 一起 (比方状态栏、SurfaceView 等) 通过零碎 SurfaceFlinger 合成到 FrameBuffer 并最终显示到设施上,以上就是 Android 平台视角下的渲染流程。
总结与瞻望
通过上边多个视角的剖析,置信读者对渲染流程已有初步的理解,这里稍稍总结一下,Canvas 作为小部件外围能力,通过 weex 内核 PlatformView 扩大机制反对,这种松耦合、可插拔的架构模式一方面使得我的项目能够麻利迭代,让 Canvas 能够在新场景疾速落地赋能业务,而另一方面也让零碎更加灵便和可扩大。
但与此同时,读者也能够看到 PlatformView 本身其实也存在一些性能缺点,而性能优化正是咱们后续演进的指标之一,下一步咱们会尝试将 Canvas 与 Weex 内核渲染管线深度交融,让 Canvas 与 Weex 内核共享 Surface,不再通过 PlatformView 扩大的形式嵌入,此外对于互动小部件来说将来咱们会提供更精简的渲染链路,敬请期待。
关注【阿里巴巴挪动技术】微信公众号,每周 3 篇挪动技术实际 & 干货给你思考!