作者:史健平(楚奕)
上篇回顧:《 淘寶小部件:全新的開放卡片技術!》、《淘寶小部件在 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 篇移動技術實踐&乾貨給你思考!