淘寶小部件 Canvas 渲染流程與原理全解析 - 新聞資(zī)訊 - 雲南小程序開發|雲南軟件開發|雲南網站(zhàn)建設-西山區知普網絡科技工作室

159-8711-8523

雲南網建設/小程序開發/軟件開發

知識

不管是網站(zhàn),軟件還是小程序,都要直接或間接能為您産生價值,我們在追求其視覺表現的同時,更側重于功能的便捷,營銷的便利,運營的高效,讓網站(zhàn)成為營銷工具,讓軟件能切實提升企業(yè)内部管理水平和(hé)效率。優秀的程序為後期升級提供便捷的支持!

淘寶小部件 Canvas 渲染流程與原理全解析

發表時間:2022-7-26

發布人:葵宇科技

浏覽次數:51

在進入正文(wén)之前需要先解釋下(xià)什麼是【小部件】,小部件是淘寶模塊/卡片級的開放解決方案,其主要面向私域提供類小程序的标準&一緻化的生産、開放、運營等能力,它有着多種業(yè)務形态,如(rú)商(shāng)品卡片、權益卡片以及互動(dòng)卡片等等,ISV開發的小部件可(kě)以以極低成本部署到店鋪、詳情、訂閱等業(yè)務場景,極大提高了運營&分發效率。

從端上技術(shù)視角看,小部件首先是一個(gè)業(yè)務容器(qì),它的特點是DSL标準化、跨平台渲染、跨場景流通(tōng):

  • DSL标準化是指小部件完全兼容小程序的DSL(不僅僅是DSL,還包括原子(zǐ)API能力、生産鍊路(lù)等等),開發者不需要額外學習即可(kě)快速上手;
  • 跨平台渲染顧名思義,小部件内核(基于weex2.0)通(tōng)過類似flutter自繪的方案可(kě)以在Android、iOS等不同操作系統上渲染出完全一緻的效果,開發者不需要關(guān)心兼容性問(wèn)題;
  • 最後跨場景流通(tōng)是指小部件容器(qì)可(kě)以『嵌入』到多種技術(shù)棧的其他業(yè)務容器(qì)中(zhōng),比如(rú)Native、WebView、小程序等等,以此做到對開發者屏蔽底層容器(qì)差異并達到一次開發,多處運行的效果。

無獨有偶,Canvas 在小部件下(xià)的技術(shù)方案與小部件容器(qì)嵌入其他業(yè)務容器(qì)的技術(shù)方案居然有不少(shǎo)相似之處,那麼下(xià)邊筆者就從 Canvas 渲染方面展開來講一講。

原理揭秘

端側整體技術(shù)架構

小部件技術(shù)側的整體架構如(rú)下(xià)圖所示,宏觀看可(kě)分為 "殼" 與 "核" 兩層。

"殼"即小部件容器(qì),主要包括DSL、小部件JSFramework、原子(zǐ)API以及擴展模塊比如(rú)Canvas。

"核"為小部件的内核,基于全新的weex2.0。在weex1.0中(zhōng)我們使用類RN的原生渲染方案,而到了weex2.0與時俱進升級到了類Flutter的自繪渲染方案,因此weex2.0承擔了小部件JS執行、渲染、事件等核心職責,并細分為JS腳本引擎、Framework與渲染引擎三模塊。JS引擎在Android側使用輕量的QuickJS,iOS側使用JavaScriptCore,并且支持通(tōng)過JSI編寫與腳本引擎無關(guān)的Bindings;Framework層提供了與浏覽器(qì)一緻的CSSOM和(hé)DOM能力,此外還有C++ MVVM框架以及一些WebAPI等等(Console、setTimeout、...);最後是内部稱之為Unicorn的渲染引擎,主要提供布局、繪制、合成、光栅化等渲染相關(guān)能力,Framework與渲染引擎層均使用C++開發,并對平台進行了相關(guān)抽象,以便更好的支持跨平台。

值得一提的是,unicorn渲染引擎内置了PlatformView能力,它允許在weex渲染的Surface上嵌入另一Surface,該Surface的内容完全由PlatformView開發者提供,通(tōng)過這種擴展能力,Camera、Video等組件得以低成本接入,Canvas也正是基于此能力将小程序下(xià)的Native Canvas(内部稱之為FCanvas)快速遷移到小部件容器(qì)。

多視角看渲染流程

更多細節還可(kě)以參考筆者先前的文(wén)章《跨平台Web Canvas渲染引擎架構的設計與思考(内含實現方案)》

到了本文(wén)的重點,首先依然從宏觀角度看下(xià)Canvas大體的渲染流程,請看下(xià)面圖示,我們從右到左看。

對開發者而言,直接接觸到的是Canvas API,包括w3c制定Canvas2D API以及khronos group制定的WebGL API,它們分别通(tōng)過canvas.getContext('2d')和(hé) canvas.getContext('webgl') 獲得,這些JS API會通(tōng)過JSBinding的方式綁定到Native C++的實現,2D基于Skia實現而WebGL則直接調用OpenGLES接口。圖形API需要綁定平台窗體環境即Surface,在Android側可(kě)以是SurfaceView或是TextureView。

再往左是小部件容器(qì)層。對weex而言,渲染合成的基本單位是LayerTree,它描述了頁面層級結構并記錄了每個(gè)節點繪制命令,Canvas就是這顆LayerTree中(zhōng)的一個(gè)Layer -- PlatformViewLayer(此Layer定義了Canvas的位置及大小信息),LayerTree通(tōng)過unicorn光栅化模塊合成到weex的Surface上,最終weex和(hé)Canvas的Surface均參與Android渲染管線渲染并由SurfaceFlinger合成器(qì)光栅化到Display上顯示。

以上是宏觀的渲染鍊路(lù),下(xià)邊筆者試着從Canvas/Weex/Android平台等不同視角分别描繪下(xià)整個(gè)渲染流程。

Canvas自身視角

從Canvas自身視角看,可(kě)以暫時忽略平台與容器(qì)部分,關(guān)鍵之處有兩點,一是Rendering Surface的創建,二是Rendering Pipeline流程。以下(xià)通(tōng)過時序圖的方式展示了這一過程,其中(zhōng)共涉及四個(gè)線程,Platform線程(即平台UI線程)、JS線程、光栅化線程、IO線程。

  • Rendering Surface Setup: 當收到上遊創建PlatformView的消息時,會先異步在JS線程綁定Canvas API,随後在Platform線程創建TextureView/SurfaceView。當收到SurfaceCreated信号時,會在Raster線程提前初始化EGL環境并與Surface綁定,此時Rendering Surafce創建完成,通(tōng)知JS線程環境Ready,可(kě)以進行渲染了。與2D不同的是,如(rú)果是WebGL Context,Rendering Surace默認會在JS線程創建(未開啟Command Buffer情況下(xià));
  • Rendering Pipeline Overview: 開發者收到該Ready事件後,可(kě)以拿到Canvas句柄進而通(tōng)過getContextAPI選擇2d或者WebGL Rendering Context。對于2d來說,開發者在JS線程調用渲染API時,僅僅是記錄了渲染指令,并未進行渲染,真正的渲染發生在光栅化線程,而對于WebGL來說,默認會直接在JS線程調用GL圖形API。不過無論是2d還是WebGL渲染均是由平台VSYNC信号驅動(dòng)的,收到VSYNC信号後,會發送RequestAnimationFrame消息到JS線程,随後真正開始一幀的渲染。對于2D來說會在光栅化線程回放先前的渲染指令,提交真實渲染命令到GPU,并swapbuffer送顯,而WebGL則直接在JS線程swapbuffer送顯。如(rú)果需要渲染圖片,則會在IO線程下(xià)載并進行圖片解碼最終在JS或者光栅化線程被使用。

Weex引擎視角

從Weex引擎視角看,Canvas屬于擴展組件,Weex甚至都感知不到Canvas的存在,它隻知道當前頁面有一塊區域是通(tōng)過PlatformView方式嵌入的,具體是什麼内容它并不關(guān)心,所有的PlatformView組件的渲染流程都是一緻的。

下(xià)面這張圖左半部分描述了Weex2.0渲染鍊路(lù)的核心流程: 小部件JS代碼通(tōng)過腳本引擎執行,通(tōng)過weex CallNative萬能Binding接口将小部件DOM結構轉為一系列Weex渲染指令(如(rú)AddElement創建節點、UpdateAttrs更新節點屬性等等),随後Unicorn基于渲染指令還原為一顆靜态的節點樹(shù)(Node Tree),該樹(shù)記錄了父子(zǐ)關(guān)系、節點自身樣式&屬性等信息。靜态節點樹(shù)會在Unicorn UI線程進一步生成RenderObject渲染樹(shù),渲染樹(shù)經過布局、繪制等流程生成多張Layer組合成為LayerTree圖層結構,經過引擎的BuildScene接口将LayerTree發送給光栅化模塊進行合成,最終渲染到Surface上并經過SwapBuffer送顯。

右半部分是Canvas的渲染流程,大體流程上邊Canvas視角已介紹過,不再贅述,這裡關(guān)注Canvas的嵌入方案,Canvas是通(tōng)過PlatformView機制嵌入的,其在Unicorn中(zhōng)會生成對應的Layer,參與後續合成,不過PlatformView有多種實現方案,每種方案的流程大相徑庭,下(xià)邊展開講一下(xià)。

Weex2.0在Android平台提供了多種PlatformView嵌入的技術(shù)方案,這裡介紹下(xià)其中(zhōng)兩種:VirtualDisplay與 Hybrid Composing,除此之外還有自研的挖洞方案。

VirtualDisplay

此模式下(xià)PlatformView内容最終會轉為一張外部紋理參與Unicorn的合成流程,具體過程:首先創建SurfaceTexture,并存儲到Unicorn引擎側,随後創建android.app.Presentation,将PlatformView(比如(rú)Canvas TextureView)作為Presentation的子(zǐ)節點,并渲染到VirtualDisplay上。衆所周知VirtualDisplay需要提供一個(gè)Surface作為Backend,那麼這裡的Surface就是基于SurfaceTexture創建。當SurfaceTexture被填充内容後,引擎側收到通(tōng)知并将SurfaceTexture轉OES紋理,參與到Unicorn光栅化流程,最終與其他Layer一起合成到Unicorn對應的SurfaceView or TextureView上。

此模式性能尚可(kě),但是主要弊端是無法響應Touch事件、丢失a11y特性以及無法獲得TextInput焦點,正是由于這些兼容性問(wèn)題導緻此方案應用場景比較受限。

Hybrid Composing

在此模式下(xià)小部件不再渲染到SurfaceView or TextureView上,而是被渲染到一張或者多張由android.media.ImageReader關(guān)聯的Surface上。Unicorn基于ImageReader封裝了一個(gè)Android自定義View,并使用ImageReader生産的Image對象作為數據源,不斷将其轉為Bitmap參與到Android原生渲染流程。那麼,為啥有可(kě)能是多個(gè)ImageReader?因為有布局層疊的可(kě)能性,PlatformView上邊和(hé)下(xià)邊均有可(kě)能有DOM節點。與之對應的是,PlatformView自身(比如(rú)Canvas)也不再轉為紋理而是作為普通(tōng)View同樣參與Android平台的渲染流程。

Hybrid Composing模式解決了VirtualDisplay模式的大部分兼容性問(wèn)題,然而也帶來了新的問(wèn)題,此模式主要弊端有兩點,一是需要合并線程,啟用PlatformView後,Raster線程的任務會抛至Android主線程執行,增大了主線程壓力;二是基于ImageReader封裝的Android原生View(即下(xià)文(wén)提到的UnicornImageView)需要不斷創建Bitmap并繪制,特别是在Android 10以前需要通(tōng)過軟件拷貝的方式生成Bitmap,對性能有一定影響。

綜合來看Hyrbid Composing兼容性更好,因此目前引擎默認使用該模式實現PlatformView。

Android平台視角

下(xià)面筆者試着進一步以Android平台視角重新審視下(xià)這一過程(以Weex + Hybrid Composing PlatformView模式為例)。

上邊提到,Hybrid Composing模式下(xià)小部件被渲染到一張或多張Unicorn ImageView,按照Z-index從上到下(xià)排列是UnicornImageView(Overlay) -> FCanvasTextureView -> UnicornImageView(Background) -> DecorView,那麼從Android平台視角看,視圖結構如(rú)上圖所示。Android根視圖DecorView下(xià)嵌套weex根視圖(UnicornView),其中(zhōng)又包含多個(gè)UnicornImageView和(hé)一個(gè)FCanvasPlatformView (TextureView)。

從平台視角看,我們甚至不需要關(guān)心UnicornImageView和(hé)FCanvas的内容,隻需要知道它們都是繼承自android.view.View并且都遵循Android原生的渲染流程。原生渲染是由VSYNC信号進行驅動(dòng),通(tōng)過ViewRootImpl#PerformTraversal頂級函數觸發測量(Measure)、布局(Layout)、繪制(Draw)流程,以繪制為例,消息首先分發到根視圖DecorView,并自頂向下(xià)分發(dispatchDraw)依次回調每個(gè)View的onDraw函數。

  • 對于FCanvas PlatformView來說,它是一個(gè)TextureView,其本質上是一個(gè)SurfaceTexture,當SurfaceTexture發現新的内容填充其内部緩沖區後,會觸發frameAvailable回調,通(tōng)知視圖invalidate,随後在Android渲染線程通(tōng)過updateTexImage将SurfaceTexture轉為紋理并交由系統合成;
  • 對于UnicornImageView來說,它是一個(gè)自定義View,其本質上是對ImageReader的封裝,當ImageReader關(guān)聯的Surface内部緩沖區被填充内容後,可(kě)以通(tōng)過acquireLatestImage獲得最新幀數據,在UnicornImageView#onDraw中(zhōng),正是将最新的幀數據轉為Bitmap并交給android.graphics.Canvas渲染。

而Android自身的View Hierarchy也關(guān)聯着一塊Surface,通(tōng)常稱之為Window Surface。上述View Hierarchy經由繪制流程之後,會生成DisplayList,并在Android渲染線程經由HWUI模塊解析DisplayList生成實際圖形渲染指令交由GPU進行硬件渲染,最終内容均繪制到上述Window Surface,然後與其他Surface一起(比如(rú)狀态欄、SurfaceView等)通(tōng)過系統SurfaceFlinger合成到FrameBuffer并最終顯示到設備上,以上就是Android平台視角下(xià)的渲染流程。

總結與展望

經過上邊多個(gè)視角的分析,相信讀者對渲染流程已有初步的了解,這裡稍稍總結一下(xià),Canvas作為小部件核心能力,通(tōng)過weex内核PlatformView擴展機制支持,這種松耦合、可(kě)插拔的架構模式一方面使得項目可(kě)以敏捷疊代,讓Canvas可(kě)以在新場景快速落地賦能業(yè)務,而另一方面也讓系統更加靈活和(hé)可(kě)擴展。

但與此同時,讀者也可(kě)以看到PlatformView自身其實也存在一些性能缺陷,而性能優化正是我們後續演進的目标之一,下(xià)一步我們會嘗試将Canvas與Weex内核渲染管線深度融合,讓Canvas與Weex内核共享Surface,不再通(tōng)過PlatformView擴展的方式嵌入,此外對于互動(dòng)小部件來說未來我們會提供更精簡的渲染鍊路(lù),敬請期待。

相關(guān)案例查看更多