作者:史健平(楚奕)

上篇回顾:《 淘宝小部件:全新的凋谢卡片技术!》、《淘宝小部件在 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 篇挪动技术实际&干货给你思考!