Cube 卡片技術(shù)棧詳解 | Cube 技術(shù)解讀
?????♀? 編者按:本文為《Cube 技術(shù)解讀》系列第二篇文章,作者是螞蟻集團(tuán)客戶端工程師京君,從 Cube 卡片生產(chǎn)流程、工作流程、核心系統(tǒng)架構(gòu)、線程模型和數(shù)據(jù)模型等角度出發(fā),對(duì) Cube 卡片技術(shù)棧進(jìn)行詳細(xì)解讀,歡迎查閱~
動(dòng)態(tài)卡片的背景
從 Windows 時(shí)代開(kāi)始,應(yīng)用程序圖標(biāo)就成了用戶(流量)的主入口,并且一直持續(xù)到移動(dòng)端時(shí)代。圖標(biāo)即入口的方式,缺點(diǎn)是不直觀,最少需要一次點(diǎn)擊后才能接觸到想要的信息。在此背景下,iOS 系統(tǒng)和部分Android 系統(tǒng)實(shí)現(xiàn)了把內(nèi)容和服務(wù)前置的卡片,舉個(gè)例子,如下圖1所示,蘋(píng)果左一屏的卡片承載天氣&股市內(nèi)容的展示。此外,鴻蒙系統(tǒng)也提出了類(lèi)似的卡片場(chǎng)景,作為某種流量入口。實(shí)際上,在應(yīng)用內(nèi)部的卡片作為內(nèi)容展示以及服務(wù)入口的場(chǎng)景則更為普遍,圖 2 和圖 3 分別是支付寶首頁(yè)和招行銀行的理財(cái)頁(yè)面,其中每個(gè)小矩形都是一個(gè)卡片。對(duì)于運(yùn)營(yíng)來(lái)說(shuō),卡片樣式和內(nèi)容可以隨時(shí)配置,不用等待應(yīng)用版本升級(jí),是某種剛需。

Cube卡片概要
Cube 卡片是螞蟻金服內(nèi)部自研的一套跨平臺(tái)動(dòng)態(tài)化卡片解決方案,是服務(wù)于應(yīng)用頁(yè)面內(nèi)的區(qū)域動(dòng)態(tài)化技術(shù),面向內(nèi)容運(yùn)營(yíng),幫助產(chǎn)品技術(shù)提高開(kāi)發(fā)效率和運(yùn)營(yíng)效率。每一個(gè) Cube 卡片獨(dú)立嵌在原生頁(yè)面內(nèi)的一個(gè)區(qū)域,區(qū)域內(nèi)容通過(guò)卡片模版進(jìn)行展示。卡片的定位大致如下:
跨平臺(tái)一致性
一套代碼
效果對(duì)齊
動(dòng)態(tài)化
界面結(jié)構(gòu)&樣式動(dòng)態(tài)化
業(yè)務(wù)邏輯動(dòng)態(tài)化
高性能
極致的性能
極致的內(nèi)存
這里展開(kāi)講下高性能。Cube 卡片追求的是接近 native 原生體驗(yàn)。我們定義了兩個(gè)維度:
一個(gè)是極致的性能。在 Cube 小程序能力的基礎(chǔ)上,我們?nèi)サ袅艘恍?fù)雜的 css 能力,例如偽類(lèi)偽元素、inline/block 等,同時(shí)也對(duì) js 的能力做了限制( Cube 卡片使用 quickjs 作為腳本引擎)。此外,我們還對(duì) quickjs 做了一些優(yōu)化,包括不限于離線 atom 編譯優(yōu)化,異步 gc 優(yōu)化等。我們也引入了 wamr 作為 quickjs的“協(xié)處理器”,支持用戶使用javascript和assemblyscript混合開(kāi)發(fā)。這樣用戶可以用 assemblyscript 一些熱點(diǎn)函數(shù)或者模塊。
另一個(gè)維度是極致的內(nèi)存。在信息瀑布場(chǎng)景無(wú)限下拉,Cube 卡片的內(nèi)存增長(zhǎng)接近 Native 卡片。我們對(duì)卡片的能力做了比較精細(xì)分級(jí),通過(guò)在開(kāi)發(fā)時(shí)配置,減小運(yùn)行時(shí)的內(nèi)存消耗。下圖展示了一個(gè)簡(jiǎn)單卡片,如圖所示 Cube 卡片的工程目錄,以及錢(qián)包某個(gè)卡片的真實(shí)代碼和運(yùn)行效果。

Cube 卡片的生產(chǎn)&工作流程

研發(fā)期
本地開(kāi)發(fā)
Cube 卡片配套獨(dú)立的開(kāi)發(fā)工具,支持卡片的編譯、日志輸出、實(shí)時(shí)預(yù)覽等功能,vue 作為當(dāng)前開(kāi)發(fā)模版的 dsl 語(yǔ)言,支持 js、css 編輯卡片樣式。
卡片管理
卡片本地開(kāi)發(fā)完成后,通過(guò)卡片管理后臺(tái)將卡片編譯產(chǎn)物上傳發(fā)布,可以對(duì)卡片進(jìn)行版本管理,卡片發(fā)布后就可以在客戶端進(jìn)行卡片的動(dòng)態(tài)更新。
運(yùn)行期
為了方便端上業(yè)務(wù)接入 Cube 卡片,我們引入了一個(gè) Cube 卡片容器( CardSDK )的概念。CardSDK 把一些和業(yè)務(wù)層/服務(wù)端聯(lián)系緊密的,且通用能力做了一些封裝。例如我們通過(guò) CubeCardSdk 從服務(wù)端拉去卡片和業(yè)務(wù)數(shù)據(jù)。此外 CardSDK 也負(fù)責(zé)常用的 JSAPI、第三方組件的接入。這樣 Cube 卡片能夠更專(zhuān)注于卡片產(chǎn)品本身。
核心系統(tǒng)架構(gòu)
Cube 卡片的系統(tǒng)架構(gòu)主要包括 JSEngine、CardEngine、RenderEngine 和 Platform 幾部分,絕大部分代碼都是 C++ 實(shí)現(xiàn)。

JSEngine
主要負(fù)責(zé)卡片 js 邏輯執(zhí)行和卡片數(shù)據(jù)變化監(jiān)聽(tīng),從而支持開(kāi)發(fā)者在卡片內(nèi)部寫(xiě)一些業(yè)務(wù)邏輯能力實(shí)現(xiàn)卡片內(nèi)容和樣式的動(dòng)態(tài)變化。
因?yàn)榭ㄆ瑘?chǎng)景對(duì)性能要求較高,綜合包大小和性能等方面考慮,我們選擇了quickjs 作為我們的 js 基礎(chǔ)引擎庫(kù),同時(shí)實(shí)現(xiàn)了一個(gè)非常小的 js 響應(yīng)式框架(JSFM),用來(lái)支持卡片內(nèi)的邏輯代碼能力。
CardEngine
主要負(fù)責(zé)卡片數(shù)據(jù)的解析和綁定、卡片邏輯渲染、構(gòu)建 DOM 指令、JSAPI 管理、JSBinding、Native 事件通信等。
卡片 DOM 樹(shù)的初始化構(gòu)建過(guò)程,我們并沒(méi)有把它放在 js 運(yùn)行時(shí),而是在卡片實(shí)例初始化鏈路中直接通過(guò) C++進(jìn)行指令生成和樹(shù)構(gòu)建,一方面是為了保持js框架更小更快,另一方面 C++的運(yùn)行效率更高。
RenderEngine
后端渲染底座,負(fù)責(zé)卡片布局計(jì)算、樣式解析、Layer計(jì)算、自繪制組件、同層渲染、光柵化上屏等過(guò)程,以及手勢(shì)、動(dòng)效等交互效果。
Platform
平臺(tái)相關(guān)接口,包括原子view封裝、Canvas API、三方組件擴(kuò)展協(xié)議、動(dòng)畫(huà) api 等。
線程模型和數(shù)據(jù)模型
線程模型
Cube 卡片生命周期內(nèi)的主要線程包括業(yè)務(wù)線程和引擎線程,業(yè)務(wù)線程是卡片數(shù)據(jù)的初始化階段由業(yè)務(wù)發(fā)起執(zhí)行,是卡片生命周期的 beforeCreate 階段。引擎線程是所有卡片生命周期運(yùn)行階段的共有線程,主要包括 Bridge 線程、Render 線程、Paint 線程和 UI 主線程。
Bridge 線程
js 運(yùn)行時(shí)線程,也是 Dom 節(jié)點(diǎn)數(shù)據(jù)查詢和處理線程,因?yàn)榛?Cube 卡片小、快的定位,js 邏輯只是卡片一個(gè)輔助能力,不具備過(guò)于復(fù)雜業(yè)務(wù)邏輯能力,所以 Bridge 線程相對(duì)較輕,并設(shè)計(jì)為單線程模式。
Render 線程
渲染相關(guān)數(shù)據(jù)計(jì)算線程,包括渲染樹(shù)構(gòu)建、節(jié)點(diǎn)層級(jí)計(jì)算、Layer 分層繪制計(jì)算、手勢(shì)數(shù)據(jù)計(jì)算以及渲染任務(wù)構(gòu)建,Render 過(guò)程主要涉及樹(shù)的遞歸計(jì)算過(guò)程,相對(duì)渲染過(guò)程耗時(shí)很短, 設(shè)計(jì)為單線程模式。
Paint 線程
繪制線程,執(zhí)行卡片節(jié)點(diǎn)分層繪制及光柵化任務(wù)。Paint 線程并不是一個(gè)固定的線程,根據(jù)當(dāng)前任務(wù)模型,Paint 線程可能是主線程,也可能是一個(gè)線程池里的子線程;在同步渲染模式下,Paint 線程直接是主線程;而在異步渲染模式下,通過(guò)一個(gè)線程池來(lái)實(shí)現(xiàn) Paint 任務(wù)的并發(fā)渲染,提高渲染效率,例如在列表滑動(dòng)場(chǎng)景。
UI 主線程
UI 操作主線程,即為目前的平臺(tái)線程,主要包括手勢(shì)識(shí)別、UI 上屏和三方擴(kuò)展組件的數(shù)據(jù)更新等。
除了以上涉及的主要線程外,還有埋點(diǎn)和監(jiān)控相關(guān)的 playground 后臺(tái)線程,整體優(yōu)先級(jí)比較低。整體的線程模型設(shè)計(jì),最大限度減少 UI 主線程壓力,提高卡片并發(fā)渲染效率。但目前還有一些不足,包括 UI 線程切換頻繁、Bridge 線程越來(lái)越重等,后面會(huì)繼續(xù)優(yōu)化線程模型。

數(shù)據(jù)模型
和線程模型對(duì)應(yīng)的數(shù)據(jù)模型主要包括三棵樹(shù):NodeTree、RenderTree、LayerTree,初此之外,還存在一個(gè)臨時(shí)的 PaintTree;
NodeTree
卡片原始節(jié)點(diǎn)樹(shù),對(duì)應(yīng)前端的 Dom 樹(shù),引擎會(huì)根據(jù) NodeTree 做樣式解析和布局計(jì)算;
RenderTree
渲染數(shù)據(jù)樹(shù),這是一顆變形樹(shù),很多情況下它的樹(shù)層級(jí)結(jié)構(gòu)和 NodeTree 是一樣的,其實(shí)當(dāng)初在設(shè)計(jì)定義引擎數(shù)據(jù)模型的時(shí)候,我們討論過(guò)到底要不要這棵樹(shù),有沒(méi)有必要存在這樣一顆和 NodeTree 層級(jí)一樣的樹(shù),最終我們還是保留了,原因是這棵樹(shù)可以比較靈活的調(diào)整樹(shù)關(guān)系,如果把卡片分為布局階段和渲染階段,那么這顆樹(shù)就是渲染階段的源樹(shù)。
事實(shí)證明我們的決定是正確的, 我們后續(xù)支持的 zindex/static 等能力,都是因?yàn)檫@棵樹(shù)的存在可以在引擎層很好的去支持, 而不用在平臺(tái)層去模擬實(shí)現(xiàn)這種層級(jí)變更能力從而導(dǎo)致很有限的場(chǎng)景支持,包括以后我們做渲染快照技術(shù)也可以從這顆樹(shù)去考慮。
LayerTree
LayerTree 樹(shù),顧名思義就是一個(gè)分層樹(shù),在 RenderTree 基礎(chǔ)上對(duì)節(jié)點(diǎn)進(jìn)行分層,同一層的節(jié)點(diǎn)在同一個(gè)渲染任務(wù)管線內(nèi)做繪制光柵化,不同層之間相互獨(dú)立,可以并發(fā)渲染。
PaintTree
PaintTree 是一個(gè)臨時(shí)樹(shù),其實(shí)嚴(yán)格的說(shuō)是一個(gè)拷貝樹(shù),是通過(guò) RenderTree 拷貝一個(gè)子樹(shù),每次發(fā)生渲染時(shí)臨時(shí)生成,當(dāng)然也會(huì)做些節(jié)點(diǎn)優(yōu)化處理,例如被完全蓋住的節(jié)點(diǎn)會(huì)被優(yōu)化調(diào),避免重復(fù)渲染。每一個(gè) layer 上存在一個(gè) PaintTree,通過(guò) PaintTree 進(jìn)行節(jié)點(diǎn)繪制生成光柵化指令或位圖。

高性能列表渲染
對(duì)于列表內(nèi)使用卡片的場(chǎng)景,主要考慮的是卡頓影響,尤其是中低端機(jī)設(shè)備。Cube 卡片支持異步渲染,所以在列表場(chǎng)景下可以很流暢,同時(shí)因?yàn)橹С侄嗑€程并發(fā)能力,可以多張卡片并發(fā)渲染,所以在異步渲染條件下也不會(huì)有明顯的白屏效果。

Native技術(shù)優(yōu)化
我們期望卡片服務(wù)于頁(yè)面內(nèi)區(qū)域化內(nèi)容動(dòng)態(tài)展現(xiàn)和簡(jiǎn)單業(yè)務(wù)邏輯,更多的是面向移動(dòng)端開(kāi)發(fā)者。即使我們使用的卡片 DSL 語(yǔ)言描述是前端語(yǔ)言,我們也希望能夠?qū)?CSS 的使用做約束、支持有限的 CSS 能力,但同時(shí)也希望盡可能覆蓋到一些開(kāi)發(fā)者常用的 CSS 能力。
所以我們針對(duì) CSS 能力做了一個(gè)專(zhuān)項(xiàng)工作,和前端團(tuán)隊(duì)技術(shù)同學(xué)一起做了 Cube 卡片 CSS 能力規(guī)范,對(duì) CSS 能力做了約束限制。即便如此,在 Native 渲染引擎下,想非常好的去支持這些能力,也是有很多困難,包括 zindex 的支持、overflow 等,因此我們也基于一些依賴(lài)的平臺(tái)能力也做了優(yōu)化處理。
Layer容器
我們引入 Layer 容器概念,前面介紹數(shù)據(jù)模型時(shí),提到了 LayerTree,每一個(gè) Layer 節(jié)點(diǎn)是一個(gè)獨(dú)立的渲染容器,由平臺(tái) View 作為 Layer 容器來(lái)渲染其他虛擬節(jié)點(diǎn)。如果按照常規(guī)的做法是一個(gè) View 對(duì)應(yīng)一個(gè)渲染容器,我們使用兩個(gè) View 組合為 Layer 容器(iOS 使用 CALayer),將內(nèi)容層和邏輯層分離,這樣做的好處很多,例如 layer 節(jié)點(diǎn)的 shadow 繪制限制裁剪問(wèn)題、內(nèi)容層的畫(huà)布切割優(yōu)化等。

手勢(shì)改造
手勢(shì)的優(yōu)化改造主要為了解決平臺(tái)系統(tǒng)手勢(shì)分發(fā)能力的限制,不管是 Android 平臺(tái)還是 iOS 平臺(tái),系統(tǒng)對(duì)手勢(shì)的分發(fā)處理都有一些限制,例如兄弟節(jié)點(diǎn)不能分發(fā)事件(iOS)、超過(guò)父節(jié)點(diǎn)區(qū)域無(wú)法接收事件(影響overflow 能力)等,所以需要對(duì)手勢(shì)進(jìn)行改造。
因?yàn)榭ㄆ秩局С秩浇M件擴(kuò)展,為了不影響擴(kuò)展組件的事件響應(yīng),我們基于 Layer 容器接管改造系統(tǒng)手勢(shì)行為,內(nèi)部進(jìn)行容器節(jié)點(diǎn)的手勢(shì)分發(fā)管理,而對(duì)于存在三方組件混合渲染的場(chǎng)景,Layer 容器和三方組件之間的手勢(shì)分發(fā)保持系統(tǒng)行為。

光柵化
Cube 卡片渲染過(guò)程包括指令渲染和位圖渲染兩種渲染模式,這兩種模式會(huì)在不同場(chǎng)景條件下切換,用來(lái)優(yōu)化不同場(chǎng)景下性能,例如幀率和內(nèi)存。位圖渲染在 Android 上相對(duì)比較復(fù)雜。默認(rèn)是用 Bitmap 作為離線渲染的緩存,缺點(diǎn)是引入一次額外 cpu/gpu 內(nèi)存拷貝并且無(wú)法充分利用 GPU 資源,優(yōu)勢(shì)是兼容性好。我們嘗試過(guò)使用 textureview 作為離線渲染緩沖,發(fā)現(xiàn) 6.0 以下設(shè)備存在嚴(yán)重的兼容性問(wèn)題,而且不同設(shè)備之間的穩(wěn)定性差別巨大。
同時(shí)光柵化能力依賴(lài)平臺(tái)系統(tǒng)的 Canvas API,有些高階方法會(huì)涉及硬件加速的限制,包括 shadow api 以及系統(tǒng)對(duì) glRender buffer 的限制(Android 平臺(tái)),我們也對(duì)大畫(huà)布場(chǎng)景做了視圖切割分段渲染來(lái)保證渲染性能。我們同事也在著手用 Skia Canvas api 替代平臺(tái)層的 Canvas API。
同層渲染
Cube 卡片把三方組件當(dāng)作獨(dú)立一層 layer 單獨(dú)進(jìn)行數(shù)據(jù)更新,可以非常方便高效的接入擴(kuò)展的三方組件?;谙到y(tǒng)的 UI 能力,使擴(kuò)展組件在卡片內(nèi)天生統(tǒng)一渲染。同時(shí)支持組件在不同卡片上的復(fù)用。在實(shí)際的業(yè)務(wù)場(chǎng)景中,同層渲染也帶來(lái)了很多額外的問(wèn)題。諸如地圖/視頻/動(dòng)畫(huà)等組件,一般會(huì)伴隨著較大的性能內(nèi)存開(kāi)銷(xiāo)。這些開(kāi)銷(xiāo)對(duì)卡片的渲染會(huì)有負(fù)面影響,尤其在列表滾動(dòng)時(shí)。對(duì)于地圖/視頻組件,我們配合組件提供方 case by case 的解決問(wèn)題,并且試圖在卡片上線時(shí)設(shè)置卡點(diǎn)。對(duì)于動(dòng)畫(huà)組件,Cube 持續(xù)的在擴(kuò)展屬性動(dòng)畫(huà)/幀動(dòng)畫(huà)能力,并且內(nèi)置 canvas 能力。
Cube 卡片的業(yè)務(wù)現(xiàn)狀和未來(lái)規(guī)劃
目前 Cube 卡片已經(jīng)服務(wù)錢(qián)包的首頁(yè)、證券(股票)、卡包、出行等 20+ 的業(yè)務(wù)場(chǎng)景,日 pv 超過(guò) 100 億。在未來(lái)相當(dāng)長(zhǎng)的一段時(shí)間內(nèi),我們的主要精力還是會(huì)集中在錢(qián)包內(nèi)部的業(yè)務(wù)場(chǎng)景,把存量的 native 卡片/h5 卡片 cube 化。服務(wù)好錢(qián)包內(nèi)的場(chǎng)景,一方面需要把開(kāi)發(fā)者體驗(yàn)做好,諸如開(kāi)發(fā)調(diào)試工具鏈條,另一方面要持續(xù)的優(yōu)化基礎(chǔ)性能,諸如追求更小的包體積,更低的內(nèi)存等。


卡片未來(lái)規(guī)劃一個(gè)重點(diǎn)方向是商業(yè)化,即把 Cube 卡片輸出到中小型互聯(lián)網(wǎng)公司以及金融企業(yè)。這部分的工作已經(jīng)啟動(dòng)了一段時(shí)間,預(yù)計(jì)年底前會(huì)作為 mpaas (https://tech.antfin.com/products/MPAAS)的一個(gè)擴(kuò)展功能發(fā)布。
卡片未來(lái)規(guī)劃的另一個(gè)方向是物聯(lián)網(wǎng)設(shè)備(例如 RTOS)的應(yīng)用開(kāi)發(fā)棧。準(zhǔn)確說(shuō)不是 Cube 卡片,而是 Cube 卡片和小程序的某種中間形態(tài)。物聯(lián)網(wǎng)設(shè)備的界面一般比較簡(jiǎn)單,近似卡片;但是又需要多個(gè)“卡片”之間的路由能力,更接近于應(yīng)用的形態(tài)。這樣一個(gè)混合形態(tài)既能保留 Cube 卡片在內(nèi)存/性能/包體積上的優(yōu)勢(shì),又能滿足物聯(lián)網(wǎng)設(shè)備應(yīng)用開(kāi)發(fā)的訴求。根據(jù)我們的調(diào)研,大部分 RTOS 應(yīng)用開(kāi)發(fā)環(huán)境還是停留在傳統(tǒng)的 C 語(yǔ)言,效能和動(dòng)態(tài)性都不不理想。對(duì)于開(kāi)發(fā)者來(lái)說(shuō),Cube 也許是一個(gè)選擇。