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

阿里巴巴移動技術發表於2022-06-16

作者:何瑾(瀟珺)

本文為《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做為容器,但有明顯缺陷,是不能用於常見業務的列表場景的,只能應用於特定場景。

SurfaceView、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團隊的主要目標是快速驗證 ,列表的實現這種成本過高,不是主要矛盾所在。

TextureVIew

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

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

Bitmap+普通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.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.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.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等多樣化場景,團隊成員將持續在渲染效能、使用者體驗、以及工具鏈等方向持續發力,努力把產品打磨好,把開發者服務好,成長為具有競爭力的跨平臺動態化渲染方案。

關注【阿里巴巴移動技術】,阿里前沿移動乾貨&實踐給你思考!

相關文章