作者:史健平(楚奕)

上篇回顾:《淘宝小部件:全新的开放卡片技术!》、《淘宝小部件在 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平台等不同视角分别描绘下整个渲染流程。

1 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或者光栅化线程被使用。

2 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。

3 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 篇移动技术实践&干货给你思考!

淘宝小部件 Canvas 渲染流程与原理全解析相关推荐

  1. 淘宝小部件:全新的开放卡片技术

    私域,即品牌自运营的空间,可以帮助品牌持续运营自己的消费者. 淘宝也在快速调整私域的布局:淘宝也有非常多的私域产品,譬如店铺.客服.消息等.在这些场景中,品牌商家需要利用创意.内容和服务留住消费者群体 ...

  2. 淘宝小部件在 2021 双十一中的规模化应用

    上篇回顾:淘宝小部件:全新的开放卡片技术 2021 双十一,淘宝全新的开放形态「小部件」大促中首次亮相,并且支撑开放业务初步规模化:说起小部件,我们还是先回顾一下淘宝小程序开放的前世今生. 小程序开放 ...

  3. 淘宝小部件:全新的开放卡片技术!

    简介: 淘宝的开放技术目前主要有两种形态,第一种是小程序,第二种是今天的主角小部件.它是基于小程序技术体系,面向标准化.轻量化.高性能的开放卡片场景.本文我们将通过技术设计策略.核心技术设施.业务场景 ...

  4. 淘宝小程序还可以这么玩!私域互动实践总结

    简介:小程序创意互动项目,作为淘宝购物小程序全面提升消费体验和满足商家个性化运营的重点项目之一,从 2020 年 3 月底正式 KO 到今年双11,已经经历了多次迭代.在购物小程序技术.产品.运营团队 ...

  5. 淘宝小游戏了解一下?技术引擎让你喜提2000条锦鲤

    今天的淘宝是月活过6亿的超级APP,它承载的不仅仅是购物的能力,实际上在淘宝有非常丰富的内容生态和内容体验.比如说以淘宝头条为代表的资讯类模块,以淘宝直播为代表的直播模块.对于游戏这种内容形态,会在淘 ...

  6. 淘宝小程序体验优化:数据分析和优化实践

    作者:莫绪旻(向屿) 淘宝小程序已经走过若干个双十一,淘宝开放业务有序铺开.体验优化是个老生常谈的话题,如何让小程序跑得又稳又快,成了我们最大的挑战之一. 写在前面 如何定义好的体验 过去我们定义这个 ...

  7. 淘宝小游戏背后的质量保障方案

    2022年4月,淘宝开启了小程序游戏项目,旨在从互动公域和店铺私域引入了大量的三方游戏服务商入淘 ,初步构建淘宝游戏的三方生态.对于开放质量团队来说,"游戏生态管控 & 游戏容器测试 ...

  8. 淘宝小程序(商家应用)开发提前需要了解的一些概念

    规定:淘宝小程序即淘宝商家应用.下文中只称作淘宝小程序,二者概念一致. 一:概念和业务场景 概念: 因为淘宝小程序是从支付宝小程序迁移过来的.用的同一套框架.所以你需要先通读一遍商家应用文档(淘宝小程 ...

  9. 淘宝小程序游戏迁移Laya引擎

    淘宝小程序游戏迁移Laya引擎 1. 目录结构 bin -- 当前项目的输出文件 laya -- 存放UI项目 assets -- 图片,音频资源目录 pages -- .scene 场景文件 key ...

最新文章

  1. 编码原则:不变量/前置条件/后置条件
  2. 5分钟了解CDN 加速原理 | +新书推荐
  3. 暗通道先验去雾实现过程分析
  4. 盘点2010年十大出版事件 文著协“宣战”百度文库
  5. P2257-YY的GCD【莫比乌斯反演】
  6. flask web开发的相关博文学习
  7. 关于vector的size()的使用问题
  8. pcl画圆球_PCL之轨迹绘制(二)
  9. 4. linux调用文件计算阶乘前5项和_【题解循环】1091:求阶乘的和
  10. python画圣诞树代码解读_实战 | 教你用Python画各种版本的圣诞树
  11. Ubuntu18.04找不到wifi适配器解决办法以及怎么上网
  12. python时间控件readonly属性_Selenium2+python自动化25-js处理日历控件(修改readonly属性)转自-上海悠悠...
  13. 中芯国际2020年第三季度收入76.38亿元,同比增长31.7%
  14. android代码 灰色,一行代码实现界面全灰(android,web,flutter)
  15. Mixamo使用笔记
  16. Android BaseQuickAdapter万能适配器
  17. iTunes 播放音乐的歌词的软件:DynamicLyrics
  18. python判断是否包含某数字_python如何判断数组里是否有某个数字
  19. Gson JsonParser
  20. 设置服务器可以多人同时远程访问

热门文章

  1. 大数据专业人士的前5大Hadoop课程
  2. FY-4A/LPW产品4km经纬度查找表生成代码-风云四号
  3. Spring Boot Validation提示信息国际化配置
  4. 云栖小镇通勤车_探访6号线科海路站:打通云栖小镇的交通闭塞,吸引更多年轻人就业安家...
  5. mysql listagg within_Oracle的 listagg() WITHIN GROUP ()函数使用
  6. Oracle 中 LISTAGG 函数的介绍以及使用
  7. 学平面设计能做什么?有什么好处?
  8. 你需要知道的 15 个很棒的 CSS 动画库
  9. iOS开发入门——模拟器方向判断及基本功能
  10. 在 iOS 模拟器中调试 Web 页面