Cube 技術解讀 | Cube 渲染設計的前世今生

本文為《Cube 技術解讀》系列第四篇文章,往期文章《Cube 小程序技術詳解》、《支付寶新一代動態化技術架構與選型綜述》、《Cube卡片技術棧解讀》歡迎大家回顧。」

阿里是個重運營的公司,前端開發者居多,2016-2017年,在Weex還是1.0時代,React Native開源還沒多久,Flutter還沒誕生的時候,如何在貼合前端開發環境的前提下,快速鋪到android/iOS雙平台是個大熱點,支付寶內部孵化一個動態化跨平台方案順勢而生。

前面三篇文章分別介紹了Cube當前架構,Cube卡片和Cube小程序技術產品形態。這篇文章主要討論Cube的渲染設計,幫助大家了解Cube卡片渲染技術的前世今生。

Native原生渲染的問題

我們都知道一個原生view渲染上屏需要幾個步驟,以android舉例:create、measure、layout、draw,這些需要在主線程完成,當實現原生列表時,即使完美復用item,對不同數據渲染時,也需要measure、layout、draw幾步缺一不可,而且隨着view嵌套層級越深,對主線程資源消耗越大,當列表fly起來以後,幀率快速下降,造成頁面卡頓,基於這個問題,cube在調研期間,如何解決渲染效率是重要的一part。

通常來說優化列表滾動幀率,也就是view層級、布局複雜度、去掉不必要背景色,解決過度繪製,圖片懶加載、item復用等方面下手,但根本還是繞不過measure、layout、draw。彼時的weex和RN,也都還是將html中的標籤映射到平台層view,在某些場景下,開發者又不能像原生開發一樣自行優化,在渲染性能上飽受詬病。因此cube調研期間渲染目標是:優化渲染效率+跨平台。

跨平台異步渲染方案

異步渲染

基於上面提到的背景和需求,那麼我們就想,能否有一種方式,把關鍵步驟移除出線程呢,即異步渲染。在列表滾動時基本只有系統手勢和列表本身滾動算法、動畫需要佔用主線程,將大大提高幀率。視圖內元素繪製的產物是一個像素緩存(Cube採用的設計是Bitmap),回到主線程給視圖進行刷新顯示。

跨平台架構

另一個目標跨平台,是要做到可以快速擴展其他平台,cube將涉及平台的部分分離出來,形成platform 層。

platform

這裡提供了各平台通用的標準c++原子接口,在不同平台用平台語言實現,初步只實現了android、iOS兩個平台,android通過jni調用java方法,iOS在實現文件中c++、OC混編。如果未來需要擴展其他平台例如macOS,只需實現platform層定義的接口即可,可以達到快速擴展其他平台的目標。

core

library是基於platform原子接口用c++實現的是基礎庫,例如文件IO、UI控件、圖片下載、消息通訊等,供上層引擎使用。library之上,就是cube渲染的核心實現,渲染部分包括數據模型和渲染邏輯,組件庫指cube內部支持的一些系統實體控件,或者開發者可外接的實體組件。

下圖是第一版cube渲染架構圖。

cube渲染架構圖

異步渲染技術選型

前面提到了,異步渲染方案里異步繪製的「產物」是一張bitmap交給「容器」View,為什麼是bitmap呢,看起來對內存很不友好,View又是個什麼View,有沒有特殊性,下面聊聊cube調研時期都研究過哪些方案,最終為什麼選型bitmap。

Android平台技術選型

android的選型之路坎坷崎嶇,最先能想到的支持獨立渲染線程的textureView、GLSurfaceView作為容器,但有明顯缺陷,是不能用於常見業務的列表場景的,只能應用於特定場景。

1SurfaceView、GLSurfaceView

SurfaceView從android1.0開始就有,主要特點是它的渲染可以在子線程中實現,因此存在的問題是,雖然它繼承View,但是它擁有獨立的Surface,不在View hierachy中,它的顯示也不受View的屬性控制,因此不能像普通view一樣縮放平移,更不能作為item放在listView/RecycleView中當作普通view使用,滾動起來會有不同步的問題。

GLSurfaceView繼承SurfaceView,它自帶GLThread,有和GLSurfaceView相同的問題,總之,這兩個view更適合單個視頻渲染或者像地圖類渲染場景。

有人可能要問,整個頁面都用SurfaceView/GLSurfaceView不就行了,連列表也在render線程實現?這裡兩個問題:

1、如果列表容器也在render線程實現,正如現在的flutter一樣,那麼列表滑動手勢處理需要自己實現,比如drag,fling,各種列表滾動動畫,以及滾動加速度計算等,成本很高。並且,touch事件捕獲仍然依賴平台層,而處理事件需要切換到render線程,這中間一定有線程切換成本造成的不跟手的體驗問題。現在很多基於flutter引擎改造的渲染引擎,正面臨著這些問題;

2、在當時cube團隊的主要目標是快速驗證 ,列表的實現這種成本過高,不是主要矛盾所在。

2TextureVIew

textureView是googleandroid4.0開始提供的,它的出現很大程度上是為了彌補SurfaceView、GLSurfaceView與原生View融合的不足,基於上面一節描述的這兩個view與原生view一起動畫的問題,textureView似乎更適合我們的場景,既能支持獨立render線程,又能保證與原生view完美融合。

但是,在實際的調研過程中發現,textureView的渲染機制,不適用於長列表,如果每個列表的item是一個textureView,那麼就涉及到出屏回收,進屏創建,否則會帶來內存問題。而回收和創建SurfaceTexture是異步過程,出現了閃黑屏問題。除此之外,進一步發現textureView的數量和容量(每個view的尺寸累計)存在某個上限,而且不同手機上限也差異很大。簡單說,這是一個看起來很美好,但是兼容性坑無數的技術路線。

3Bitmap+普通View

最終選擇了bitmap看起來並不完美的方案,雖然這被大多數android開發認為bitmap帶來大量內存消耗,視為不可接受,但隨着cube的應用範圍越來越廣,這逐漸被證明是在當時,最普適的一個方案。

每一個layer對應一個系統view,每個view的繪製內容在子線程通過CanvasAPI異步繪製在bitmap上,當view上屏時,系統onDraw繪製這個bitmap「產物」。

BitmapCache

雖然用了Bitmap繪製方案,但必須要考慮內存過載的問題,這裡我們採用了BitmapCache,主要針對列表類型場景,依賴系統的item回收回調通知,將bitmap畫布放入Cache,item上屏渲染時,優先從cache取bitmap畫布使用,優先取相同大小的,如果不存在,則取width、height大於目標width、height,讓view只繪製bitmap局部,達到正確渲染的目的

iOS平台技術選型

iOS的實現原理與android大致相同,區別是,iOS異步線程繪製完成的「產物」,不會在UIView的drawRect里利用CoreGraphics進行渲染,這種方式效率很低,頁面卡頓明顯,最終採用的是將畫布賦值給UIView的layer,託管給系統渲染layer。

渲染技術的演進

上面講了cube異步渲染大體方案和關鍵技術選型,事實上,從19年初上線答答星球,到現在,cube在支付寶內應用越來越廣泛,這中間也伴隨着cube團隊根據實際業務場景不斷摸索、優化的過程,渲染鏈路經歷了兩次重構。需要強調的,這個演進過程是在嚴格的內存/性能下完成的,而且要對Android兼容性做出妥協。一些看起來不那麼優雅或者先進的設計,事實上是不得不這麼做,比如選擇Bitmap作為像素緩衝,比如接入三方組件的設計等。從某種意義上,拋開約束談論技術優劣也意義不大。我們曾經借鑒flutter的部分,但Cube最終還是沿着適合自身場景的技術路線往前走。

常見術語

LayoutTree:DomApi通過add、update、remove構建的經過yoga布局的,用來描述節點父子關係,包含布局信息的原始樹型結構;

  • RenderTree:用來描述繪製節點父子關係,包含繪製信息的樹型結構,與layoutTree的區別舉例:一個layoutNode visible為gone,則該節點不會在RenderTree中出現;
  • Layer:一般情況下,根節點及其子節點繪製在同一個畫布上,定義為一個layer,對應平台層一個view,當子節點有動畫屬性,或者超出父節點範圍,則需要獨立出一個layer;
  • LayerTree:上面提到的layer節點,構建的樹型結構,一個layer對應平台層一個view,我們叫ContainerView;
  • 實體節點:需要獨立layer的節點為實體節點;
  • 虛擬節點:除了實體節點以外,其他節點均會被繪製在父容器的畫布上,這些是虛擬節點。

演進過程

1調研初期——1.0驗證方案的可行性

調研時期驗證方案可行性,場景比較簡單,以支付寶內朋友動態頁面為驗證場景,每條狀態(一個item/cell)作為一個渲染單元,這裡只考慮了layerTree只有一個layer的情況,頭像、昵稱、時間、配圖、「贊」、「賞」,「評」等元素均繪製在root節點對應的layer上,「贊」、「賞」,「評」文本旁邊的小圖標則作為外接實體組件,通過addSubView添加在rootLayer的View上。

數據模型

如下圖所示,根據layoutTree構建RenderTree,但非渲染節點不在renderTree上,layerTree只有一個自繪製layer(rootLayer),和其他自定義組件X,最終除自定義組件外,其他所有節點都繪製在rootLayer上。

渲染流程

bridge線程通過DomApi構建layoutTree,當主線程觸發渲染時,主線程根據layoutTree構建RenderTree,構建過程中遇到外接實體組件,創建實例並addSubView,之後切換子線程繪製RenderTree,即rootLayer上的所有虛擬節點,繪製完成後切換主線程貼圖(bitmap「產物」)。

缺點

  • 不能支持多layer結構
  • 實體view沒有復用,也就是朋友動態列表中有多少item/cell,就會有多少「贊」、「賞」,「評」實體組件

但這個調研驗證了異步渲染的可行性,在列表滾動時幀率大幅提升。

2產品化時期——2.0支持多layer

前面驗證了可行性,在進行產品化設計時,就必須要滿足多layer結構了,即實際的一張卡片中,會有一個或幾個不同的節點被設置為layer,這些節點及其子節點,分別繪製在不同畫布上,供不同的layer渲染。

數據模型

改進之處時layerTree里有個多layer節點,layer節點下面的子虛擬節點,將繪製在該layer的bitmap「產物」上。

渲染流程

brige線程構建layoutTree的過程中,每個指令(addNode、removeNode……)都會相應分發到render模塊的主線程,render根據指令構建RenderTree,並用指令信息生成task入隊,當VSync信號來時,觸發任務出隊並去重,構建layerTree,不同layer分發到不同draw線程繪製,繪製完成後切主線程貼圖(bitmap「產物」)。

缺點

主線程計算量大,可能造成卡頓

  • render節點既包含繪製信息,是繪製對象,還包含邏輯,例如display:"none"節點忽略不顯示,職責不清晰。

3優化時期——3.0取長補短

上面可以看到renderTree的構建以及layerTree的構建,都是在UI線程,在節點數比較多或複雜的情況下會造成UI的卡頓,為了追求極致滾動幀率,儘可能減少主線程計算內容,優化3.0版本將renderObject構建layer、以及計算節點變更導致的繪製影響範圍,的部分改在子線程完成,形成了現在線上運行的版本。

數據模型

新增了PaintTree這個結構,它掛載在Layer節點上,樣式和屬性值從RenderTree拷貝而來,但不涉及任何邏輯處理,單純的是一個繪製對象,每個繪製任務只繪製paintTree上的paint節點,與layerTree和renderTree沒有並發問題。

渲染流程

layout線程構建layoutTree,切換到render線程構建renderTree,當平台層觸發渲染,切換到renderTree構建layerTree,並計算影響範圍等,切換到主線程將layer對應的實體化View添加在容器View上,生成繪製任務在paint線程執行,繪製結束後切換主線程貼圖(bitmap產物)。

缺點

  • render線程繁忙時造成的閃白率升高

以上就是cube渲染從誕生到現在線上方案的演進,目前在支付寶端內卡片形態接入業務超過20+,線上運行的卡片模版個數達到500多個,顯示PV過百億,經受住了各業務方的考驗。

但在技術支持中也發現了一些問題,例如渲染任務過多時,render線程阻塞排隊,不能及時消費導致白屏概率變大,最近cube也在繼續研究優化方案。

存在的問題

兩端一致性問題

  • cube目前的繪製api,採用的系統平台層提供的CanvasApi(iOS是CoreGraphics),這就導致了兩個平台在繪製點線面的細節上必須兩端人工代碼對齊,否則就會產生效果差異,當新增一些feature,例如支持點劃線,需要兩個平台各自實現DrawDottedLine接口,但這個問題,cube團隊正調研自繪製,即使用skia api將繪製接口下沉到c++,實現跨平台自繪製;
  • 文本也是容易產生差異的一個點,利用平台層api對文本進行布局,在繪製時調用布局的api進行繪製,因此可能會產品平台差異,但cube團隊目前已經在Cube小程序上把文本布局,布局算法下沉在c++層,不依賴平台api,實現雙平台一致;限於內存/性能的約束尚未在Cube卡片上應用。

閃白問題

因為滾動採用的異步渲染,所以必然會產生主線程卡片已經上屏,異步繪製還未完成造成的閃白問題,線程切換有成本,這個閃白理論上一定存在,只是時間長短問題,cube團隊致力於提高渲染效率,將線程切換帶來的損耗降到最低,使用戶在列表滾動中體驗提升。

未來規劃

針對目前已知的問題,cube團隊致力於持續優化,主要優化點包括但不限於以下:

  • 渲染快照,提高冷啟的渲染效率,減少閃白時間;
  • 渲染策略,例如預渲染、同異步繪製自適應、線程模型優化、組件緩存和預加載等,減少閃白率,提升渲染效率;
  • 用於Cube卡片的yoga布局引擎優化,提升layout布局效率;
  • skia自繪製實現,實現雙端一致性;

cube的渲染技術的應用包含卡片和小程序兩種技術形態,場景包括支付寶端內、端外、IOT等多樣化場景,團隊成員將持續在渲染性能、用戶體驗、以及工具鏈等方向持續發力,努力把產品打磨好,把開發者服務好,成長為具有競爭力的跨平台動態化渲染方案。