Cube 渲染設(shè)計(jì)的前世今生 | Cube 技術(shù)解讀
?????♀? 編者按:本文為《Cube 技術(shù)解讀》系列第四篇文章,作者是螞蟻集團(tuán)客戶(hù)端工程師瀟珺,主要討論 Cube 的渲染設(shè)計(jì),幫助大家了解 Cube 卡片渲染技術(shù)的前世今生,歡迎查閱~
第一篇:支付寶新一代動(dòng)態(tài)化技術(shù)架構(gòu)與選型綜述 | Cube 技術(shù)解讀
第二篇:Cube 卡片技術(shù)棧詳解 | Cube 技術(shù)解讀
第三篇:Cube 小程序技術(shù)詳解 | Cube 技術(shù)解讀
第四篇:本篇
第五篇:文本布局性能提升 60%,Inline Text 技術(shù)原理與實(shí)現(xiàn) | Cube 技術(shù)解讀
第六篇:大屏小程序探索實(shí)踐 | Cube 技術(shù)解讀
2016 - 2017 年,在 Weex 還是 1.0 時(shí)代,React Native 開(kāi)源還沒(méi)多久,F(xiàn)lutter 還沒(méi)誕生的時(shí)候,如何在貼合前端開(kāi)發(fā)環(huán)境的前提下,快速鋪到 android/iOS 雙平臺(tái)是個(gè)大熱點(diǎn),支付寶內(nèi)部孵化一個(gè)動(dòng)態(tài)化跨平臺(tái)方案順勢(shì)而生。
《Cube 技術(shù)解讀》系列前面三篇文章分別介紹了 Cube 當(dāng)前架構(gòu),Cube卡片和 Cube 小程序技術(shù)產(chǎn)品形態(tài)。這篇文章主要討論 Cube 的渲染設(shè)計(jì),幫助大家了解 Cube 卡片渲染技術(shù)的前世今生。
Native 原生渲染的問(wèn)題
我們都知道一個(gè)原生 view 渲染上屏需要幾個(gè)步驟,以 android 舉例:create、measure、layout、draw,這些需要在主線程完成,當(dāng)實(shí)現(xiàn)原生列表時(shí),即使完美復(fù)用item,對(duì)不同數(shù)據(jù)渲染時(shí),也需要 measure、layout、draw 幾步缺一不可,而且隨著 view 嵌套層級(jí)越深,對(duì)主線程資源消耗越大,當(dāng)列表 fly 起來(lái)以后,幀率快速下降,造成頁(yè)面卡頓,基于這個(gè)問(wèn)題,Cube 在調(diào)研期間,如何解決渲染效率是重要的一 part。
通常來(lái)說(shuō)優(yōu)化列表滾動(dòng)幀率,也就是 view 層級(jí)、布局復(fù)雜度、去掉不必要背景色,解決過(guò)度繪制,圖片懶加載、item 復(fù)用等方面下手,但根本還是繞不過(guò) measure、layout、draw。彼時(shí)的 weex 和 RN,也都還是將 html 中的標(biāo)簽映射到平臺(tái)層 view,在某些場(chǎng)景下,開(kāi)發(fā)者又不能像原生開(kāi)發(fā)一樣自行優(yōu)化,在渲染性能上飽受詬病。因此 Cube 調(diào)研期間渲染目標(biāo)是:優(yōu)化渲染效率+跨平臺(tái)。
跨平臺(tái)異步渲染方案
異步渲染
基于上面提到的背景和需求,那么我們就想,能否有一種方式,把關(guān)鍵步驟移除出線程呢,即異步渲染。在列表滾動(dòng)時(shí)基本只有系統(tǒng)手勢(shì)和列表本身滾動(dòng)算法、動(dòng)畫(huà)需要占用主線程,將大大提高幀率。視圖內(nèi)元素繪制的產(chǎn)物是一個(gè)像素緩存(Cube 采用的設(shè)計(jì)是 Bitmap),回到主線程給視圖進(jìn)行刷新顯示。

跨平臺(tái)架構(gòu)
另一個(gè)目標(biāo)跨平臺(tái),是要做到可以快速擴(kuò)展其他平臺(tái),Cube 將涉及平臺(tái)的部分分離出來(lái),形成 platform 層。
platform
這里提供了各平臺(tái)通用的標(biāo)準(zhǔn) c++ 原子接口,在不同平臺(tái)用平臺(tái)語(yǔ)言實(shí)現(xiàn),初步只實(shí)現(xiàn)了 android、iOS 兩個(gè)平臺(tái),android 通過(guò) jni 調(diào)用 java 方法,iOS 在實(shí)現(xiàn)文件中 c++、OC 混編。如果未來(lái)需要擴(kuò)展其他平臺(tái)例如 macOS,只需實(shí)現(xiàn) platform 層定義的接口即可,可以達(dá)到快速擴(kuò)展其他平臺(tái)的目標(biāo)。
core
library 是基于 platform 原子接口用 c++ 實(shí)現(xiàn)的是基礎(chǔ)庫(kù),例如文件 IO、UI 控件、圖片下載、消息通訊等,供上層引擎使用。library 之上,就是 Cube 渲染的核心實(shí)現(xiàn),渲染部分包括數(shù)據(jù)模型和渲染邏輯,組件庫(kù)指 Cube 內(nèi)部支持的一些系統(tǒng)實(shí)體控件,或者開(kāi)發(fā)者可外接的實(shí)體組件。下圖是第一版 Cube 渲染架構(gòu)圖。

Cube 渲染架構(gòu)圖
異步渲染技術(shù)選型
前面提到了,異步渲染方案里異步繪制的“產(chǎn)物”是一張 bitmap 交給“容器” View,為什么是 bitmap 呢,看起來(lái)對(duì)內(nèi)存很不友好,View 又是個(gè)什么 View,有沒(méi)有特殊性,下面聊聊 Cube 調(diào)研時(shí)期都研究過(guò)哪些方案,最終為什么選型 bitmap。
Android 平臺(tái)技術(shù)選型
android 的選型之路坎坷崎嶇,最先能想到的支持獨(dú)立渲染線程的 textureView、GLSurfaceView 做為容器,但有明顯缺陷,是不能用于常見(jiàn)業(yè)務(wù)的列表場(chǎng)景的,只能應(yīng)用于特定場(chǎng)景。
?1??SurfaceView、GLSurfaceView
SurfaceView 從 android 1.0 開(kāi)始就有,主要特點(diǎn)是它的渲染可以在子線程中實(shí)現(xiàn),因此存在的問(wèn)題是,雖然它繼承View,但是它擁有獨(dú)立的 Surface,不在 View hierachy 中,它的顯示也不受 View 的屬性控制,因此不能像普通view一樣縮放平移,更不能作為 item 放在 listView/RecycleView 中當(dāng)作普通 View 使用,滾動(dòng)起來(lái)會(huì)有不同步的問(wèn)題。
GLSurfaceView 繼承 SurfaceView,它自帶 GLThread,有和 GLSurfaceView 相同的問(wèn)題,總之,這兩個(gè) view 更適合單個(gè)視頻渲染或者像地圖類(lèi)渲染場(chǎng)景。
有人可能要問(wèn),整個(gè)頁(yè)面都用 SurfaceView/GLSurfaceView 不就行了,連列表也在 render 線程實(shí)現(xiàn)?這里兩個(gè)問(wèn)題:
1、如果列表容器也在 render 線程實(shí)現(xiàn),正如現(xiàn)在的 flutter 一樣,那么列表滑動(dòng)手勢(shì)處理需要自己實(shí)現(xiàn),比如 drag,fling,各種列表滾動(dòng)個(gè)動(dòng)畫(huà),以及滾動(dòng)加速度計(jì)算等,成本很高。并且,touch 事件捕獲仍然依賴(lài)平臺(tái)層,而處理事件需要切換到 render 線程,這中間一定有線程切換成本造成的不跟手的體驗(yàn)問(wèn)題?,F(xiàn)在很多基于 flutter 引擎改造的渲染引擎,正面臨著這些問(wèn)題;
2、在當(dāng)時(shí) cube 團(tuán)隊(duì)的主要目標(biāo)是快速驗(yàn)證 ,列表的實(shí)現(xiàn)這種成本過(guò)高,不是主要矛盾所在。

?2??TextureView
textureView 是 google 從 android 4.0 開(kāi)始提供的,它的出現(xiàn)很大程度上是為了彌補(bǔ) SurfaceView、GLSurfaceView 與原生 View 融合的不足,基于上面一節(jié)描述的這兩個(gè) View 與原生 View 一起動(dòng)畫(huà)的問(wèn)題,textureView 似乎更適合我們的場(chǎng)景,既能支持獨(dú)立 render 線程,又能保證與原生 View 完美融合。
但是,在實(shí)際的調(diào)研過(guò)程中發(fā)現(xiàn),textureView 的渲染機(jī)制,不適用于長(zhǎng)列表,如果每個(gè)列表的 item 是一個(gè) textureView,那么就涉及到出屏回收,進(jìn)屏創(chuàng)建,否則會(huì)帶來(lái)內(nèi)存問(wèn)題。而回收和創(chuàng)建 SurfaceTexture 是異步過(guò)程,出現(xiàn)了閃黑屏問(wèn)題。除此之外,進(jìn)一步發(fā)現(xiàn) textureView 的數(shù)量和容量(每個(gè) view 的尺寸累計(jì))存在某個(gè)上限,而且不同手機(jī)上限也差異很大。簡(jiǎn)單說(shuō),這是一個(gè)看起來(lái)很美好,但是兼容性坑無(wú)數(shù)的技術(shù)路線。

?3??Bitmap+普通 View
最終選擇了 bitmap 看起來(lái)并不完美的方案,雖然這被大多數(shù) android 開(kāi)發(fā)認(rèn)為 bitmap 帶來(lái)大量?jī)?nèi)存消耗,視為不可接受,但隨著 cube 的應(yīng)用范圍越來(lái)越廣,這逐漸被證明是在當(dāng)時(shí),最普適的一個(gè)方案。
每一個(gè) layer 對(duì)應(yīng)一個(gè)系統(tǒng) view,每個(gè) view 的繪制內(nèi)容在子線程通過(guò) CanvasAPI 異步繪制在 bitmap 上,當(dāng) view 上屏?xí)r,系統(tǒng) onDraw 繪制這個(gè) bitmap “產(chǎn)物”。
BitmapCache
雖然用了 Bitmap 繪制方案,但必須要考慮內(nèi)存過(guò)載的問(wèn)題,這里我們采用了 BitmapCache,主要針對(duì)列表類(lèi)型場(chǎng)景,依賴(lài)系統(tǒng)的 item 回收回調(diào)通知,將 bitmap 畫(huà)布放入 Cache,item 上屏渲染時(shí),優(yōu)先從 cache 取 bitmap 畫(huà)布使用,優(yōu)先取相同大小的,如果不存在,則取 width、height 大于目標(biāo) width、height,讓 view 只繪制 bitmap 局部,達(dá)到正確渲染的目的
iOS 平臺(tái)技術(shù)選型
iOS 的實(shí)現(xiàn)原理與 android 大致相同,區(qū)別是,iOS 異步線程繪制完成的“產(chǎn)物”,不會(huì)在 UIView 的 drawRect 里利用 CoreGraphics 進(jìn)行渲染,這種方式效率很低,頁(yè)面卡頓明顯,最終采用的是將畫(huà)布賦值給 UIView 的 layer,托管給系統(tǒng)渲染 layer。
渲染技術(shù)的演進(jìn)
上面講了 Cube 異步渲染大體方案和關(guān)鍵技術(shù)選型,事實(shí)上,從 19 年初上線答答星球,到現(xiàn)在,Cube 在支付寶內(nèi)應(yīng)用越來(lái)越廣泛,這中間也伴隨著 Cube 團(tuán)隊(duì)根據(jù)實(shí)際業(yè)務(wù)場(chǎng)景不斷摸索、優(yōu)化的過(guò)程,渲染鏈路經(jīng)歷了兩次重構(gòu)。需要強(qiáng)調(diào)的,這個(gè)演進(jìn)過(guò)程是在嚴(yán)格的內(nèi)存/性能下完成的,而且要對(duì) Android 兼容性做出妥協(xié)。一些看起來(lái)不那么優(yōu)雅或者先進(jìn)的設(shè)計(jì),事實(shí)上是不得不這么做,比如選擇 Bitmap 作為像素緩沖,比如接入三方組件的設(shè)計(jì)等。從某種意義上,拋開(kāi)約束談?wù)摷夹g(shù)優(yōu)劣也意義不大。我們?cè)?jīng)借鑒 flutter 的部分,但 Cube 最終還是沿著適合自身場(chǎng)景的技術(shù)路線往前走。
常見(jiàn)術(shù)語(yǔ)
LayoutTree:DomApi 通過(guò) add、update、remove 構(gòu)建的經(jīng)過(guò) yoga 布局的,用來(lái)描述節(jié)點(diǎn)父子關(guān)系,包含布局信息的原始樹(shù)型結(jié)構(gòu);
RenderTree:用來(lái)描述繪制節(jié)點(diǎn)父子關(guān)系,包含繪制信息的樹(shù)型結(jié)構(gòu),與 layoutTree 的區(qū)別舉例:一個(gè) layoutNode visible 為 gone,則該節(jié)點(diǎn)不會(huì)在 RenderTree 中出現(xiàn);
Layer:一般情況下,根節(jié)點(diǎn)及其子節(jié)點(diǎn)繪制在同一個(gè)畫(huà)布上,定義為一個(gè) layer,對(duì)應(yīng)平臺(tái)層一個(gè) view,當(dāng)子節(jié)點(diǎn)有動(dòng)畫(huà)屬性,或者超出父節(jié)點(diǎn)范圍,則需要獨(dú)立出一個(gè) layer;
LayerTree:上面提到的 layer 節(jié)點(diǎn),構(gòu)建的樹(shù)型結(jié)構(gòu),一個(gè) layer 對(duì)應(yīng)平臺(tái)層一個(gè) view,我們叫 ContainerView;
實(shí)體節(jié)點(diǎn):需要獨(dú)立 layer 的節(jié)點(diǎn)為實(shí)體節(jié)點(diǎn);
虛擬節(jié)點(diǎn):除了實(shí)體節(jié)點(diǎn)以外,其他節(jié)點(diǎn)均會(huì)被繪制在父容器的畫(huà)布上,這些是虛擬節(jié)點(diǎn)。
演進(jìn)過(guò)程
?1??調(diào)研初期——1.0驗(yàn)證方案的可行性
調(diào)研時(shí)期驗(yàn)證方案可行性,場(chǎng)景比較簡(jiǎn)單,以支付寶內(nèi)朋友動(dòng)態(tài)頁(yè)面為驗(yàn)證場(chǎng)景,每條狀態(tài)(一個(gè)item/cell)作為一個(gè)渲染單元,這里只考慮了 layerTree 只有一個(gè)layer的情況,頭像、昵稱(chēng)、時(shí)間、配圖、“贊”、“賞”,“評(píng)”等元素均繪制在 root 節(jié)點(diǎn)對(duì)應(yīng)的 layer 上,“贊”、“賞”,“評(píng)”文本旁邊的小圖標(biāo)則作為外接實(shí)體組件,通過(guò) addSubView 添加在 rootLayer 的 View 上。

數(shù)據(jù)模型
如下圖所示,根據(jù) layoutTree 構(gòu)建 RenderTree,但非渲染節(jié)點(diǎn)不在 renderTree 上,layerTree 只有一個(gè)自繪制 layer(rootLayer),和其他自定義組件 X,最終除自定義組件外,其他所有節(jié)點(diǎn)都繪制在 rootLayer 上。

渲染流程
bridge 線程通過(guò) DomApi 構(gòu)建 layoutTree,當(dāng)主線程觸發(fā)渲染時(shí),主線程根據(jù) layoutTree 構(gòu)建 RenderTree,構(gòu)建過(guò)程中遇到外接實(shí)體組件,創(chuàng)建實(shí)例并 addSubView,之后切換子線程繪制 RenderTree,即 rootLayer 上的所有虛擬節(jié)點(diǎn),繪制完成后切換主線程貼圖(bitmap“產(chǎn)物”)。

缺點(diǎn)
不能支持多 layer 結(jié)構(gòu)
實(shí)體 view 沒(méi)有復(fù)用,也就是朋友動(dòng)態(tài)列表中有多少 item/cell,就會(huì)有多少“贊”、“賞”,“評(píng)”實(shí)體組件
但這個(gè)調(diào)研驗(yàn)證了異步渲染的可行性,在列表滾動(dòng)時(shí)幀率大幅提升。
?2??產(chǎn)品化時(shí)期——2.0 支持多 layer
前面驗(yàn)證了可行性,在進(jìn)行產(chǎn)品化設(shè)計(jì)時(shí),就必須要滿足多 layer 結(jié)構(gòu)了,即實(shí)際的一張卡片中,會(huì)有一個(gè)或幾個(gè)不同的節(jié)點(diǎn)被設(shè)置為 layer,這些節(jié)點(diǎn)及其子節(jié)點(diǎn),分別繪制在不同畫(huà)布上,供不同的 layer 渲染。
數(shù)據(jù)模型
改進(jìn)之處時(shí) layerTree 里有個(gè)多 layer 節(jié)點(diǎn),layer 節(jié)點(diǎn)下面的子虛擬節(jié)點(diǎn),將繪制在該 layer 的 bitmap “產(chǎn)物”上。

渲染流程
brige 線程構(gòu)建 layoutTree 的過(guò)程中,每個(gè)指令(addNode、removeNode……)都會(huì)相應(yīng)分發(fā)到 render 模塊的主線程,render 根據(jù)指令構(gòu)建 RenderTree,并用指令信息生成 task 入隊(duì),當(dāng) VSync 信號(hào)來(lái)時(shí),觸發(fā)任務(wù)出隊(duì)并去重,構(gòu)建 layerTree,不同 layer 分發(fā)到不同 draw 線程繪制,繪制完成后切主線程貼圖(bitmap “產(chǎn)物”)。

缺點(diǎn)
主線程計(jì)算量大,可能造成卡頓
render 節(jié)點(diǎn)既包含繪制信息,是繪制對(duì)象,還包含邏輯,例如 display:"none" 節(jié)點(diǎn)忽略不顯示,職責(zé)不清晰。
?3??優(yōu)化時(shí)期—— 3.0 取長(zhǎng)補(bǔ)短
上面可以看到 renderTree 的構(gòu)建以及 layerTree 的構(gòu)建,都是在 UI 線程,在節(jié)點(diǎn)數(shù)比較多活復(fù)雜的情況下會(huì)造成 UI 的卡頓,為了追求極致滾動(dòng)幀率,盡可能減少主線程計(jì)算內(nèi)容,優(yōu)化 3.0 版本將 renderObject 構(gòu)建 layer、以及計(jì)算節(jié)點(diǎn)變更導(dǎo)致的繪制影響范圍,的部分改在子線程完成,形成了現(xiàn)在線上運(yùn)行的版本。
數(shù)據(jù)模型
新增了 PaintTree 這個(gè)結(jié)構(gòu),它掛載在 Layer 節(jié)點(diǎn)上,樣式和屬性值從 RenderTree 拷貝而來(lái),但不涉及任何邏輯處理,單純的是一個(gè)繪制對(duì)象,每個(gè)繪制任務(wù)只繪制 paintTree 上的 paint 節(jié)點(diǎn),與 layerTree 和 renderTree 沒(méi)有并發(fā)問(wèn)題。

渲染流程
layout 線程構(gòu)建 layoutTree,切換到 render 線程構(gòu)建 renderTree,當(dāng)平臺(tái)層觸發(fā)渲染,切換到 renderTree 構(gòu)建 layerTree,并計(jì)算影響范圍等,切換到主線程將 layer 對(duì)應(yīng)的實(shí)體化 View 添加在容器 View 上,生成繪制任務(wù)在 paint 線程執(zhí)行,繪制結(jié)束后切換主線程貼圖(bitmap 產(chǎn)物)。

缺點(diǎn)
render 線程繁忙時(shí)造成的閃白率升高
以上就是 Cube 渲染從誕生到現(xiàn)在線上方案的演進(jìn),目前在支付寶端內(nèi)卡片形態(tài)接入業(yè)務(wù)超過(guò) 20+,線上運(yùn)行的卡片模版?zhèn)€數(shù)達(dá)到 500 多個(gè),顯示 PV 過(guò)百億,經(jīng)受住了各業(yè)務(wù)方的考驗(yàn)。
但在技術(shù)支持中也發(fā)現(xiàn)了一些問(wèn)題,例如渲染任務(wù)過(guò)多時(shí),render 線程阻塞排隊(duì),不能及時(shí)消費(fèi)導(dǎo)致白屏概率變大,最近 Cube 也在繼續(xù)研究?jī)?yōu)化方案。
存在的問(wèn)題
兩端一致性問(wèn)題
Cube 目前的繪制 api,采用的系統(tǒng)平臺(tái)層提供的 CanvasApi(iOS 是 CoreGraphics),這就導(dǎo)致了兩個(gè)平臺(tái)在繪制點(diǎn)線面的細(xì)節(jié)上必須兩端人工代碼對(duì)齊,否則就會(huì)產(chǎn)生效果差異,當(dāng)新增一些 feature,例如支持點(diǎn)劃線,需要兩個(gè)平臺(tái)各自實(shí)現(xiàn) DrawDottedLine 接口,但這個(gè)問(wèn)題,Cube 團(tuán)隊(duì)正調(diào)研自繪制,即使用 skia api 將繪制接口下沉到 c++,實(shí)現(xiàn)跨平臺(tái)自繪制;
文本也是容易產(chǎn)生差異的一個(gè)點(diǎn),利用平臺(tái)層 api 對(duì)文本進(jìn)行布局,在繪制時(shí)調(diào)用布局的 api 進(jìn)行繪制,因此可能會(huì)產(chǎn)品平臺(tái)差異,但 Cube 團(tuán)隊(duì)目前已經(jīng)在 Cube 小程序上把文本布局,布局算法下沉在 c++ 層,不依賴(lài)平臺(tái) api,實(shí)現(xiàn)雙平臺(tái)一致;限于內(nèi)存/性能的約束尚未在 Cube 卡片上應(yīng)用。
閃白問(wèn)題
因?yàn)闈L動(dòng)采用的異步渲染,所以必然會(huì)產(chǎn)生主線程卡片已經(jīng)上屏,異步繪制還未完成造成的閃白問(wèn)題,線程切換有成本,這個(gè)閃白理論上一定存在,只是時(shí)間長(zhǎng)短問(wèn)題,Cube 團(tuán)隊(duì)致力于提高渲染效率,將線程切換帶來(lái)的損耗降到最低,使用戶(hù)在列表滾動(dòng)中體驗(yàn)提升。
未來(lái)規(guī)劃
針對(duì)目前已知的問(wèn)題,Cube 團(tuán)隊(duì)致力于持續(xù)優(yōu)化,主要優(yōu)化點(diǎn)包括但不限于以下:
渲染快照,提高冷啟的渲染效率,減少閃白時(shí)間;
渲染策略,例如預(yù)渲染、同異步繪制自適應(yīng)、線程模型優(yōu)化、組件緩存和預(yù)加載等,減少閃白率,提升渲染效率;
用于 Cube 卡片的 yoga 布局引擎優(yōu)化,提升 layout 布局效率;
skia 自繪制實(shí)現(xiàn),實(shí)現(xiàn)雙端一致性;
Cube 的渲染技術(shù)的應(yīng)用包含卡片和小程序兩種技術(shù)形態(tài),場(chǎng)景包括支付寶端內(nèi)、端外、IOT 等多樣化場(chǎng)景,團(tuán)隊(duì)成員將持續(xù)在渲染性能、用戶(hù)體驗(yàn)、以及工具鏈等方向持續(xù)發(fā)力,努力把產(chǎn)品打磨好,把開(kāi)發(fā)者服務(wù)好,成長(zhǎng)為具有競(jìng)爭(zhēng)力的跨平臺(tái)動(dòng)態(tài)化渲染方案。