Tangram 2.0 庫
Android
iOS
背景
技術背景
一直以來,無線應用都在不斷尋求動態化頁面的解決方案,在阿里巴巴集團內,除了風風火火地 Weex 專案外,各個團隊都有大大小小的解決方案。我們貓客一直持續基於 Tangram 方案來解決頁面動態化的問題,然而在面對持續升級的業務需求時,原有的開發模式也慢慢變得無法勝任,本年度以來,我們 Tangram 體系在各個層面都進行了大跨度的技術升級(可參考文章天貓APP改版之首頁架構&開發模式全面升級),本文再詳細介紹一下頁面內元件體系升級方案。
老元件體系的問題
在原有的 Tangram 體系裡,主要解決了頁面內佈局結構的動態化能力,通過 json 資料描述可以組合出常用的頁面結構。然而頁面內具體的坑位樣式,我們稱之為業務元件,是採用常規的 native 程式碼開發的,除非內建了足夠多的邏輯,否則元件的樣式調整或者新元件的開發都要釋出版本,無法滿足業務節奏;當然我們也嘗試過使用 Weex 開發業務元件貼到頁面上,但是在體驗和效能上還是有較大的缺陷。
所以總結起來,就是兩點問題:
- 業務元件無法動態更新;
- 現有的動態元件方案較重,影響效能和體驗;
解決之道
對於上述問題,解決思路其實是比較通用的,要動態更新介面檢視,就需要用介面模板描述檢視,模板與資料分離。將動態下發的模板和資料在端上繫結渲染。要提升效能,也有三大著力點——減少檢視層級與個數,結構儘量扁平化;非同步佈局渲染流程,解放主執行緒計算量;回收與複用元件,減少記憶體開銷。
新的元件體系就是在模板化描述檢視,動態更新檢視,減少檢視層級幾個方面做文章,至於元件的回收複用,則是在頁面級別統一完成;而非同步佈局渲染流程,則是後續的優化方向。
新的元件方案稱之為 VirtualView,簡稱 VV,也稱為2.0元件,它的設計遵循以下幾個思路:
- 以了一種虛擬化開發基礎元件的技術,使用方只要按照指定協議實現一個基礎元件的尺寸計算、繪製邏輯、佈局邏輯,即能實現在宿主容器的 canvas 裡實現直接繪製 UI 內容的,讓最終渲染出來的檢視結構呈現扁平化,提升元件渲染效能。同時為了解決虛擬化 View 帶來的原生 View 的能力損失的問題,它支援載入和渲染原生基礎元件,兩者組合產生合力,既能減少開銷,又能滿足特殊場景下的業務需求。
- 內建實現了一系列基礎元件,可以讓使用方直接上手嘗試;而搭建業務元件的方式採用 XML 模板來編寫,配套 XML 模板更新 sdk,這使得業務元件動態更新成為了可能。XML 模板裡還支援寫資料繫結的表示式,在樣式動態化、資料動態化的場景下能非常方便地實現業務需求。
- XML 模板裡涉及到的基礎節點、屬性、字串資源等都被提前編譯成二進位制資源,客戶端載入通過載入編譯後的模板資料來建立檢視。
設計方案
整體架構
先從整體上預覽一下整個方案的大體結構:
自下往上,自左往右的順序介紹各個模組:
- 基礎模板載入器負責載入編譯後的模板資料,比如從檔案載入、從二進位制陣列載入、從網路載入,將編譯後的二進位制模板資料載入到記憶體裡,通過元件載入器、字串資源載入器、表示式資源載入器等提取出其中的資源。
- 框架還內建了基礎元件,包括原子的基礎元件如文字、圖片、線條,還包括佈局型別的基礎元件,比如線性佈局、幀佈局、網格佈局等;每一種型別的基礎元件提供了原生 Native 版本的實現和虛擬化的實現,使用者也可以自定義自己的基礎元件註冊到框架內部,元件構造器通過載入好的元件資料,來構造出整個業務元件樹,並新增到宿主容器裡,對於虛擬元件,會在渲染階段繪製到宿主容器的 canvas 上,而原生元件會作為子 View 新增到宿主容器裡。
- 框架內部也提供了基礎的表示式能力,主要分兩種,一種是簡單的資料繫結表示式,一種是簡單的邏輯表示式;前者用於在模板裡寫表示式繫結資料到基礎元件的屬性上,而後者提供了一種輕量級的邏輯運算能力,可以訪問基礎元件的屬性並更新,實現一些聯動效果。
- 事件管理,本方案聚焦於介面的動態化建立,但對業務邏輯的處理主要還是靠原生的程式碼實現,因此處理元件的一些常用互動事件,比如元件的點選、長按、觸控、曝光事件等。事件管理模組負責將外部的各個型別的事件處理模組註冊進來,當元件發生特定的事件時,找到對應型別的處理模組來呼叫處理。
- 宿主容器管理負責對虛擬元件的宿主容器進行構建和回收複用的管理。當原有的元件滑出螢幕後,可以回收到統一的池子裡,以便後續複用。
- 元件管理負責對基礎元件進行構建和回收複用管理。當原有的元件滑出螢幕後,除了宿主容器可以回收複用,內部的基礎元件物件也可以回收到統一的池子裡。如果元件的池子是空的,則在需要的時候構造新的元件。
- 擴充套件模組管理則用於註冊外部功能擴充套件模組,當內建的基礎能力無法滿足業務場景的時候,通過擴充套件模組註冊特定的功能模組,然後編寫自定義基礎元件來實現特定功能。
- 模板儲存、模板校驗、模板更新、模板註冊則分別負責模板資料的儲存、安全性校驗、版本校驗、與更新檢查與新模板下載、註冊模板資料到框架,整體協同來完成業務元件的動態更新,它並不與整個渲染元件的核心框架耦合,可以作為獨立模組存在。
- 配套的工具和服務主要包括模板編寫工具、模板編譯工具、模板更新服務.模板編寫工具用於 XML 的模板的編輯,並呼叫編譯模組編譯模板,模板裡涉及到的元件資源、字串資源、表示式資源會分別用對應的模組處理。編譯後端模板資料可以上傳到模板更新服務裡,客戶端呼叫相應的介面檢查是否有更新。
執行流程
有了上述基礎,當我們要開發新的業務元件的時候,除了有新增 Native 邏輯的需求場景(比如新增視訊功能),大部分需求都可以告別原生程式碼的編寫,轉而編寫元件模板。
- 先編寫業務元件的模板。
- 通過工具將模板資料編譯成二進位制資料。
- 客戶端載入二進位制資料可以有兩種路徑,一是直接打包到客戶端裡,寫程式碼載入,另一種是釋出到模板管理後臺,客戶端線上更新到模板資料。
- 不論哪種方式載入二進位制資料,客戶端接下來的工作是解析二進位制資料裡,比如校驗版本號,合法性,讀取頭資訊等等。
- 等要真正建立元件的時候,根據元件名稱找到二進位制資料,從中解析並建立出真正的元件模型資料。
- 從模板裡建立在元件往往不含有業務資料,因為業務資料是動態性的,使用者需要獲取到業務資料繫結到元件上,元件的屬性裡可以寫表示式來指定使用哪一個資料欄位。
值得注意的是,在上述架構及流程裡,描述了一個完整的實踐經驗,但對於本方案來說,核心點在於提供了對元件從編寫到展示流程的實現,其周邊的配套設施,並沒有內建在框架裡,包括客戶端上的模板管理、更新、註冊模組,以及後端的模板釋出服務,因為這些模組往往涉及業務邏輯,且與各個應用的基礎設施相關,內建在框架裡反而限制了使用方的接入。這裡提供一些可供參考的經驗:
- 模板管理後臺要能對模板的進行釋出、更新,並且按照客戶端版本、平臺、元件版本、生效優先順序等幾個維度來管理模板;
- 模板檔案可以存放到 CDN 上供客戶端下載,管理平臺只是對比下發遠資訊;下載檔案要做足夠的校驗;
- 客戶端要內建一份打底的模板資料,這樣不至於因為模板不存在而出現空窗;
- 客戶端可提供一個統一的模板管理模組,面向全應用提供服務,在合適的時候請求管理平臺檢查有沒有更新,比如啟動、使用者重新整理、推送指令的到達,並且負責下載、檔案校驗、通知頁面重新整理等功能;頁面重新整理可以做優先順序區分,比如高優先順序的模板更新主動去重新整理下頁面,而低優先順序的可以等二次進入頁面或者重新整理頁面的時候生效;
幾個核心設計
元件的基礎模型
對於元件,我們做了如下定義,每一個基礎的原子元件或者容器元件都會有以下屬性,自定義的基礎元件應當繼承自基礎定義並做擴充套件。
名稱 | 型別 | 預設值 | 描述 |
---|---|---|---|
id | int | 0 | 元件id |
layoutWidth | int/float/enum(match_parent/wrap_content) | 0 | 元件的佈局寬度,與Android裡的概念類似,寫絕對值的時候表示絕對寬高,match_parent表示儘可能撐滿父容器提供的寬高,wrap_content表示根據自身內容的寬高來佈局 |
layoutHeight | int/float/enum(match_parent/wrap_content) | 0 | 元件的佈局寬度,與Android裡的概念類似,寫絕對值的時候表示絕對寬高,match_parent表示儘可能撐滿父容器提供的寬高,wrap_content表示根據自身內容的寬高來佈局 |
layoutGravity | enum(left/right/top/bottom/v_center/h_center) | left|top | 描述元件在容器中的對齊方式,left:靠左,right:靠右,top:靠上,bottom:靠底,v_center:垂直方向居中,h_center:水平方向居中,可用或 組合描述 |
autoDimX | int/float | 1 | 元件寬高比計算的橫向值 |
autoDimY | int/float | 1 | 元件寬高比計算的豎向值 |
autoDimDirection | enum(X/Y/NONE) | NONE | 元件在佈局中的基準方向,用於計算元件的寬高比,與autoDimX、autoDimY配合使用,設定了這三個屬性時,在計算元件尺寸時具有更高的優先順序。當autoDimDirection=X時,元件的寬度由layoutWidth和父容器決策決定,但高度 = width * (autoDimY / autoDimX),當autoDimDirection=Y時,元件的高度由layoutHeight和父容器決策決定,但寬度 = height * (autoDimX / autoDimY) |
minWidth | int/float | 0 | 最小寬度 |
minHeight | int/float | 0 | 最小高度 |
paddingLeft | int/float | 0 | 左內邊距 |
paddingRight | int/float | 0 | 右內邊距 |
paddingTop | int/float | 0 | 上內邊距 |
paddingBottom | int/float | 0 | 下內邊距 |
layoutMarginLeft | int/float | 0 | 左外邊距 |
layoutMarginRight | int/float | 0 | 右外邊距 |
layoutMarginTop | int/float | 0 | 上外邊距 |
layoutMarginBottom | int/float | 0 | 下外邊距 |
background | int | 0 | 背景色 |
backgroundImage | string | null | 背景圖地址 |
borderWidth | int | 0 | 邊框寬度 |
borderColor | int | 0 | 邊框顏色 |
visibility | enum(visible/invisible/gone) | visible | 可見性,與Android裡的概念類似,visible:可見,invisible:不可見,但佔位,gone:不可見也不佔位 |
gravity | enum(left/right/top/bottom/v_center/h_center) | left|top | 描述內容的對齊,比如文字在文字元件裡的位置、原子元件在容器裡的位置,left:靠左,right:靠右,top:靠上,bottom:靠底,v_center:垂直方向居中,h_center:水平方向居中,可用或 組合描述 |
方案內內建了一系列基礎元件,完整的元件列表如下:
- 虛擬文字元件
- 原生文字元件
- 虛擬圖片元件
- 原生圖片元件
- 虛擬線條元件
- 原生線條元件
- 虛擬進度條元件
- 虛擬圖形元件
- 原生翻頁佈局容器元件
- 原生滾動佈局容器元件
- 虛擬幀佈局容器元件
- 虛擬比例佈局容器元件
- 虛擬網格佈局容器元件
- 原生網格佈局容器元件
- 虛擬線性佈局容器元件
- 原生線性佈局容器元件
虛擬元件
上文提到虛擬化開發的元件的技術,簡稱虛擬元件。很多做效能優化的方案、建議都會提到採用 Canvas 直接繪製的方式來減少 View 的個數,虛擬將這個開發流程做了抽象與規範,可以讓開發人員像定義原生元件一樣定義虛擬元件。
具體來講,基礎元件需要遵循一個介面的規範,這個口定義了渲染過程中需要的三個流程:計算尺寸階段、佈局階段、繪製階段;定義這個三個階段是為了更好的與系統平臺特別是 Android 平臺對接,因為在 Android 原生平臺下也會有這個三個階段,在 iOS 平臺下則也需要按照本方案裡要求的規範去處理。計算尺寸階段定義要觸發一次尺寸計算,需要對其包含的子元件進行計算呼叫;佈局階段定義了要觸發一次佈局,將子元素按照計算好的位置尺寸排布,也要對包含的子元件進行佈局呼叫;繪製階段定義要進行檢視繪製,當然也要對起包含的子元件進行繪製的呼叫;對於虛擬元件,就在這些介面裡實現相關邏輯,而對於原生元件,在這些介面實現裡呼叫原生元件的對應邏輯。
不論是虛擬化元件還是原生元件,都採用上述相同的模型來定義,再加上相同的尺寸計算介面、佈局介面、繪製介面,這樣對於宿主容器來說,包裝在內部的元件就不分虛擬化還是原生,一視同仁,暴露給外面的介面也是一樣的,只要將宿主容器像普通的 View 一樣新增到的檢視介面上,就可以在後續的渲染過程中顯示出來。如果虛擬元件使用的越多,View 的個數就越少,對於系統來說層級越扁平。以下圖示例的元件來說,最終呈現的 View 只有宿主容器和兩個圖片元件,如果將圖片也用虛擬化的方式實現,最終 View 只有一個宿主容器,而介面仍然保持不變。
二進位制檔案的格式
通過 XML 編寫的業務元件,並不直接在客戶端裡執行使用,而是先進行一次二進位制序列化操作,原始的 XML 模板檔案儲存成檔案的時候,就是以純文字的形式存在,會包含很多冗餘資訊,比如空格、換行、還有重複出現的字串等,檔案體積比較大,以xml解析器去解析的時候,也會需要大量字串操作,效率和效能不能達到最優。而將它編譯成二進位制格式,會避免這些問題,比如檔案重複出現的字串只保留一份,通過字串索引去引用它,所有的元件型別也都會被轉換成一個數字索引,在客戶端內通過數字索引反過來找到對應的類例項化。這樣檔案格式會非常緊湊,體積更小。整個設計也借鑑了 Android 系統編譯模板檔案的思路。它的具體格式說明如下:
按照圖中從左往右、從上往下的順序分別說明每個段的作用:
- 開始5個位元組固定為 ALIVV;相當於我們的檔案格式的一個標記。
- 版本號分三個,分別為主版本號,次版本號和修訂版本號,均為 2 個位元組;在無重大重構更新時,前兩位一般不變,第三位用於元件的業務級別變更升級;
- 元件區的起始位置和長度,均為 4 個位元組;表示這份檔案裡元件區資料從第幾個位元組開始,它總共有多少個位元組,這樣解析這份資料的時候能直接將檔案指標定位到特定位置來讀取資料。
- 字串區的起始位置和長度,均為 4 個位元組;表示這份檔案裡字串資料從第幾個位元組開始,它總共有多少個位元組。
- 表示式區的起始位置和長度,均為 4 個位元組;表示這份檔案裡字串資料從第幾個位元組開始,它總共有多少個位元組。
- 資料區的起始位置和長度,均為 4 個位元組;表示這份檔案裡附加資料從第幾個位元組開始,它總共有多少個位元組。目前這一區塊是作為一種保留區,實際還未使用到。
- 當前檔案所屬頁編碼,2 個位元組,唯一標識一個頁(保留使用)
- 當前檔案依賴頁的個數為 2 個位元組,後面為依賴頁的 Id,依賴頁個數大於 0 表示該頁用到了其他頁的資源或者程式碼,在該頁載入之前需要確保依賴頁必須已經載入;(保留使用)
- 元件區開始,前 4 個位元組表示檔案裡業務元件個數,目前一個 XML 模板編譯成一個二進位制檔案,故其值固定為 1。每個業務元件前 2 個位元組表示業務元件名稱字串的長度,後面為指定長度的字串位元組資料;緊接著是 2 個位元組的編譯後元件二進位制流長度,後面為二進位制程式碼;
- 字串區開始,前4個位元組表示字串個數,在我們的框架裡,會內建一些系統級別的字串資源,比如上文5.2開端表格裡提到的那些屬性名,這些字串不用序列化到二進位制檔案裡,而模板檔案裡出現的非系統字串才會作為資源序列化到二進位制檔案。每個字串資源前 4 個位元組字串索引 Id 即它的 hashCode,後面 2 個自己為字串的長度,再後面為對應的字串;
- 邏輯表示式程式碼表。前 4 個位元組表示邏輯表示式資源個數,每個表示式資源前4個自己表示表示式的索引,它是表示式原始字串的hashCode,後面兩個2 個位元組表示表示式的長度,後面為對應的表示式內容,它是表示式按照關鍵字切割後的字串結構;
- 擴充套件資料段是保留為第三方擴充套件使用;
繫結資料的表示式
開發業務元件的時候,基礎屬性或者樣式往往不能在模板裡直接寫死,而是需要從資料裡獲取,所以引入了使用者資料繫結的表示式,語法和實現上目前比較簡單,參考了很多同類的設計,儘可能符合開發人員的直覺。
- 訪問資料屬性的表示式
語法上以 ${ 開頭,以 } 結束。對於Map,通過 . 操作符進行訪問,對於 Array 或者 List 通過 [] 操作符進行訪問。
比如:
${benefitImgUrl}
${data[0].benefitImgUrl}
複製程式碼
- 條件表示式
用來給那些需要根據資料中某個欄位來設定值的屬性,語法上以 @{ 開頭,以 } 結束,中間部分為表示式的具體內容。
條件表示式 ? 結果表示式[1] : 結果表示式[2]
複製程式碼
當條件表示式成立的時候,使用結果表示式[1],否則使用結果表示式[2]。 其中: 條件表示式支援布林型別、字串型別、JSONObject、JSONArray。 以下場景均為 false:
- 布林型別值為 false
- 字串為 null 或者 "" 或者 "null"
- 字串 "false" 或者 "FALSE"
- JSONObject 為空或 JSONObject.NULL
- JSONArray 長度為 0
- 欄位不存在
比如:
@{${logoUrl} ? visible : invisible }
複製程式碼
考慮到篇幅限制,不能將上述架構和流程中的每一細節完全展開,詳情可以參考蘋果核這裡的文件。
效果
與 Tangram 及 TAC 結合
VirtualView 方案是 Tangram 的極大補充,可以解決80%場景下的動態化需求,而 Tangram 依賴的資料則通過 TAC 提供解決,三者結合可以形成一個閉環,讓一個開發從端到端地解決整塊業務的開發。
元件動態下發
以雙十一期間為例,90%的雙十一業務元件都是動態下發的,且隨時可根據業務節奏調整。
展望
儘管在功能流程上已經逐步穩定,能承載起日常及大促的需求變更,我們的方案還是有很多不足之處的,比如我們期望更高的執行效率、更加扁平化的UI結構、更加方便的開發體驗,對此也做了更進一步的規劃建設:
功能 | 計劃 |
---|---|
提供更加完善的文件和教程、Demo,內外版本同步,建立以 github 為中心的迭代開發機制 | 17年12月 |
元件建立、佈局計算、資料繫結機制優化,提升效能 | 18年1月 |
重構模板編譯工具,提升編譯開發體驗 | 18年1月 |
提供預覽服務,提升開發效率 | 18年3月 |
提供配套的後端資料服務與基礎設施,即 TAC 平臺開放 | 18年3月 |
附錄
Tangram 2.0 主要更新說明
- 元件模型的概念升級,從原來的『卡片』+『元件』升級成『佈局』+『元件』,即原來的『卡片』認為是一種具有佈局能力的元件,具備巢狀另一元件的能力;
- 頁面結構優化,頁面下可以直接掛載元件,不需要巢狀一層佈局;
- 元件型別的語義化,從原來的 1、2、3、4...等數字列舉型別定義,升級成字串型別的定義,相容解析原有的數字列舉定義;
- 更好的巢狀佈局實現,流式佈局在模型描述上支援多層次的巢狀,並優化了 Android 端上的實現方式;
- margin 去重的實現,同一層級的容器元件或原子元件直接,支援外邊距 margin 的去重,使得動態資料下控制間距更方便;
- 支援 zIndex,無論是容器元件還是原子元件,支援在其樣式上配置 zIndex,zIndex 值越大,繪製層次越高;
- 升級元件開發方式,引入動態化元件開發技術,提升元件動態性,實現元件樣式的高效渲染與動態更新;