作者:京君
“ 此前,我們上線了《Cube 技術解讀》系列首篇文章《支付寶新一代動態化技術架構與選型綜述》,本文為 Cube 系列第二篇文章,針對 Cube 卡片技術棧做了深入解讀,歡迎大家關注。”
動態卡片的背景
從Windows時代開始,應用程式圖示就成了使用者(流量)的主入口,並且一直持續到移動端時代。圖示即入口的方式,缺點是不直觀,最少需要一次點選後才能接觸到想要的資訊。在此背景下,iOS系統和部分Android系統實現了把內容和服務前置的卡片,舉個例子,如下圖1所示,蘋果左一屏的卡片承載天氣&股市內容的展示。此外,鴻蒙系統也提出了類似的卡片場景,作為某種流量入口。實際上,在應用內部的卡片作為內容展示以及服務入口的場景則更為普遍,圖2和圖3分別是支付寶首頁和招行銀行的理財頁面,其中每個小矩形都是一個卡片。對於運營來說,卡片樣式和內容可以隨時配置,不用等待應用版本升級,是某種剛需。
Cube卡片概要
Cube卡片是螞蟻金服內部自研的一套跨平臺動態化卡片解決方案,是服務於應用頁面內的區域動態化技術,面向內容運營,幫助產品技術提高開發效率和運營效率。每一個Cube卡片獨立嵌在原生頁面內的一個區域,區域內容通過卡片模版進行展示。卡片的定位大致如下:
跨平臺一致性
- 一套程式碼
- 效果對齊
動態化
- 介面結構&樣式動態化
- 業務邏輯動態化
高效能
- 極致的效能
- 極致的記憶體
這裡展開講下高效能。Cube卡片追求的是接近native原生體驗。我們定義了兩個維度:
一個是極致的效能。在Cube小程式能力的基礎上,我們去掉了一些複雜的css能力,例如偽類偽元素、inline/block等,同時也對js的能力做了限制(Cube卡片使用quickjs作為指令碼引擎)。此外,我們還對quickjs做了一些優化,包括不限於離線atom編譯優化,非同步gc優化等。我們也引入了wamr作為quickjs的“協處理器”,支援使用者使用javascript和assemblyscript混合開發。這樣使用者可以用assemblyscript一些熱點函式或者模組。
另一個維度是極致的記憶體。在資訊瀑布場景無限下拉,Cube卡片的記憶體增長接近Native卡片。我們對卡片的能力做了比較精細分級,通過在開發時配置,減小執行時的記憶體消耗。下圖展示了一個簡單卡片,如圖所示Cube卡片的工程目錄,以及錢包某個卡片的真實程式碼和執行效果。
Cube卡片的生產&工作流程
研發期
- 本地開發
Cube卡片配套獨立的開發工具,支援卡片的編譯、日誌輸出、實時預覽等功能,vue作為當前開發模版的dsl語言,支援js、css編輯卡片樣式。
- 卡片管理
卡片本地開發完成後,通過卡片管理後臺將卡片編譯產物上傳發布,可以對卡片進行版本管理,卡片釋出後就可以在客戶端進行卡片的動態更新。
執行期
為了方便端上業務接入Cube卡片,我們引入了一個Cube卡片容器(CardSDK)的概念。CardSDK把一些和業務層/服務端聯絡緊密的,且通用能力做了一些封裝。例如我們通過CubeCardSdk從服務端拉去卡片和業務資料。此外CardSDK也負責常用的JSAPI、第三方元件的接入。這樣Cube卡片能夠更專注於卡片產品本身。
核心系統架構
Cube卡片的系統架構主要包括JSEngine、CardEngine、RenderEngine和Platform幾部分,絕大部分程式碼都是C++實現。
JSEngine
主要負責卡片js邏輯執行和卡片資料變化監聽,從而支援開發者在卡片內部寫一些業務邏輯能力實現卡片內容和樣式的動態變化。
因為卡片場景對效能要求較高,綜合包大小和效能等方面考慮,我們選擇了quickjs作為我們的js基礎引擎庫,同時實現了一個非常小的js響應式框架(JSFM),用來支援卡片內的邏輯程式碼能力。
CardEngine
主要負責卡片資料的解析和繫結、卡片邏輯渲染、構建DOM指令、JSAPI管理、JSBinding、Native事件通訊等。
卡片DOM樹的初始化構建過程,我們並沒有把它放在js執行時,而是在卡片例項初始化鏈路中直接通過C++進行指令生成和樹構建,一方面是為了保持js框架更小更快,另一方面C++的執行效率更高。
RenderEngine
後端渲染底座,負責卡片佈局計算、樣式解析、Layer計算、自繪製元件、同層渲染、光柵化上屏等過程,以及手勢、動效等互動效果。
Platform
平臺相關介面,包括原子view封裝、Canvas API、三方元件擴充套件協議、動畫api等。
執行緒模型和資料模型
執行緒模型
Cube卡片生命週期內的主要執行緒包括業務執行緒和引擎執行緒,業務執行緒是卡片資料的初始化階段由業務發起執行,是卡片生命週期的beforeCreate階段。引擎執行緒是所有卡片生命週期執行階段的共有執行緒,主要包括Bridge執行緒、Render執行緒、Paint執行緒和UI主執行緒。
Bridge執行緒
js執行時執行緒,也是Dom節點資料查詢和處理執行緒,因為基於Cube卡片小、快的定位,js邏輯只是卡片一個輔助能力,不具備過於複雜業務邏輯能力,所以Bridge執行緒相對較輕,並設計為單執行緒模式。
Render執行緒
渲染相關資料計算執行緒,包括渲染樹構建、節點層級計算、Layer分層繪製計算、手勢資料計算以及渲染任務構建,Render過程主要涉及樹的遞迴計算過程,相對渲染過程耗時很短, 設計為單執行緒模式。
Paint執行緒
繪製執行緒,執行卡片節點分層繪製及光柵化任務。Paint執行緒並不是一個固定的執行緒,根據當前任務模型,Paint執行緒可能是主執行緒,也可能是一個執行緒池裡的子執行緒;在同步渲染模式下,Paint執行緒直接是主執行緒;而在非同步渲染模式下,通過一個執行緒池來實現Paint任務的併發渲染,提高渲染效率,例如在列表滑動場景。
UI主執行緒
UI操作主執行緒,即為目前的平臺執行緒,主要包括手勢識別、UI上屏和三方擴充套件元件的資料更新等。
除了以上涉及的主要執行緒外,還有埋點和監控相關的playground後臺執行緒,整體優先順序比較低。整體的執行緒模型設計,最大限度減少UI主執行緒壓力,提高卡片併發渲染效率。但目前還有一些不足,包括UI執行緒切換頻繁、Bridge執行緒越來越重等,後面會繼續優化執行緒模型。
資料模型
和執行緒模型對應的資料模型主要包括三棵樹:NodeTree、RenderTree、LayerTree,初此之外,還存在一個臨時的PaintTree;
NodeTree
卡片原始節點樹,對應前端的Dom樹,引擎會根據NodeTree做樣式解析和佈局計算;
RenderTree
渲染資料樹,這是一顆變形樹,很多情況下它的樹層級結構和NodeTree是一樣的,其實當初在設計定義引擎資料模型的時候,我們討論過到底要不要這棵樹,有沒有必要存在這樣一顆和NodeTree層級一樣的樹,最終我們還是保留了,原因是這棵樹可以比較靈活的調整樹關係,如果把卡片分為佈局階段和渲染階段,那麼這顆樹就是渲染階段的源樹。
事實證明我們的決定是正確的, 我們後續支援的zindex/static等能力,都是因為這棵樹的存在可以在引擎層很好的去支援, 而不用在平臺層去模擬實現這種層級變更能力從而導致很有限的場景支援,包括以後我們做渲染快照技術也可以從這顆樹去考慮。
LayerTree
LayerTree樹,顧名思義就是一個分層樹,在RenderTree基礎上對節點進行分層,同一層的節點在同一個渲染任務管線內做繪製光柵化,不同層之間相互獨立,可以併發渲染。
PaintTree
PaintTree是一個臨時樹,其實嚴格的說是一個拷貝樹,是通過RenderTree拷貝一個子樹,每次發生渲染時臨時生成,當然也會做些節點優化處理,例如被完全蓋住的節點會被優化調,避免重複渲染。每一個layer上存在一個PaintTree,通過PaintTree進行節點繪製生成光柵化指令或點陣圖。
高效能列表渲染
對於列表內使用卡片的場景,主要考慮的是卡頓影響,尤其是中低端機裝置。Cube卡片支援非同步渲染,所以在列表場景下可以很流暢,同時因為支援多執行緒併發能力,可以多張卡片併發渲染,所以在非同步渲染條件下也不會有明顯的白屏效果。
Native技術優化
我們期望卡片服務於頁面內區域化內容動態展現和簡單業務邏輯,更多的是面向移動端開發者。即使我們使用的卡片DSL語言描述是前端語言,我們也希望能夠對CSS的使用做約束、支援有限的CSS能力,但同時也希望儘可能覆蓋到一些開發者常用的CSS能力。
所以我們針對CSS能力做了一個專項工作,和前端團隊技術同學一起做了Cube卡片CSS能力規範,對CSS能力做了約束限制。即便如此,在Native渲染引擎下,想非常好的去支援這些能力,也是有很多困難,包括zindex的支援、overflow等,因此我們也基於一些依賴的平臺能力也做了優化處理。
Layer容器
我們引入Layer容器概念,前面介紹資料模型時,提到了LayerTree,每一個Layer節點是一個獨立的渲染容器,由平臺View作為Layer容器來渲染其他虛擬節點。如果按照常規的做法是一個View對應一個渲染容器,我們使用兩個View組合為Layer容器(iOS使用CALayer),將內容層和邏輯層分離,這樣做的好處很多,例如layer節點的shadow繪製限制裁剪問題、內容層的畫布切割優化等。
手勢改造
手勢的優化改造主要為了解決平臺系統手勢分發能力的限制,不管是Android平臺還是iOS平臺,系統對手勢的分發處理都有一些限制,例如兄弟節點不能分發事件(iOS)、超過父節點區域無法接收事件(影響overflow能力)等,所以需要對手勢進行改造。
因為卡片渲染支援三方元件擴充套件,為了不影響擴充套件元件的事件響應,我們基於Layer容器接管改造系統手勢行為,內部進行容器節點的手勢分發管理,而對於存在三方元件混合渲染的場景,Layer容器和三方元件之間的手勢分發保持系統行為。
光柵化
Cube卡片渲染過程包括指令渲染和點陣圖渲染兩種渲染模式,這兩種模式會在不同場景條件下切換,用來優化不同場景下效能,例如幀率和記憶體。點陣圖渲染在Android上相對比較複雜。預設是用Bitmap作為離線渲染的快取,缺點是引入一次額外cpu/gpu記憶體拷貝並且無法充分利用GPU資源,優勢是相容性好。我們嘗試過使用textureview作為離線渲染緩衝,發現6.0以下裝置存在嚴重的相容性問題,而且不同裝置之間的穩定性差別巨大。
同時光柵化能力依賴平臺系統的Canvas API,有些高階方法會涉及硬體加速的限制,包括shadow api以及系統對glRender buffer的限制(Android平臺),我們也對大畫布場景做了檢視切割分段渲染來保證渲染效能。我們同事也在著手用Skia Canvas api替代平臺層的Canvas API。
同層渲染
Cube卡片把三方元件當作獨立一層layer單獨進行資料更新,可以非常方便高效的接入擴充套件的三方元件。基於系統的UI能力,使擴充套件元件在卡片內天生統一渲染。同時支援元件在不同卡片上的複用。在實際的業務場景中,同層渲染也帶來了很多額外的問題。諸如地圖/視訊/動畫等元件,一般會伴隨著較大的效能記憶體開銷。這些開銷對卡片的渲染會有負面影響,尤其在列表滾動時。對於地圖/視訊元件,我們配合元件提供方case by case的解決問題,並且試圖在卡片上線時設定卡點。對於動畫元件,Cube持續的在擴充套件屬性動畫/幀動畫能力,並且內建canvas能力。
Cube卡片的業務現狀和未來規劃
目前Cube卡片已經服務錢包的首頁、證券(股票)、卡包、出行等20+的業務場景,日pv超過100億。在未來相當長的一段時間內,我們的主要精力還是會集中在錢包內部的業務場景,把存量的native卡片/h5卡片cube化。服務好錢包內的場景,一方面需要把開發者體驗做好,諸如開發除錯工具鏈條,另一方面要持續的優化基礎效能,諸如追求更小的包體積,更低的記憶體等。
卡片未來規劃一個重點方向是商業化,即把Cube卡片輸出到中小型網際網路公司以及金融企業。這部分的工作已經啟動了一段時間,預計年底前會作為mpaas https://tech.antfin.com/produ... 的一個擴充套件功能釋出。
卡片未來規劃的另一個方向是物聯網裝置(例如RTOS)的應用開發棧。準確說不是Cube卡片,而是Cube卡片和小程式的某種中間形態。物聯網裝置的介面一般比較簡單,近似卡片;但是又需要多個“卡片”之間的路由能力,更接近於應用的形態。這樣一個混合形態既能保留Cube卡片在記憶體/效能/包體積上的優勢,又能滿足物聯網裝置應用開發的訴求。根據我們的調研,大部分RTOS應用開發環境還是停留在傳統的c語言,效能和動態性都不不理想。對於開發者來說,Cube也許是一個選擇。
預告
如你對該系列文章感興趣,感謝大家持續關注本公眾號【阿里巴巴移動技術】,下一篇 Cube 技術解讀文章我們再繼續暢聊。
關注我們,每週 3 篇移動乾貨&實踐給你思考!