天天看點

貓客頁面内元件的動态化方案-Tangram

Tangram 2.0 庫

Android

  • Tangram-Android
  • Virtualview-Android

iOS

  • Tangram-iOS
  • Virtualview-iOS

背景

技術背景

一直以來,無線應用都在不斷尋求動态化頁面的解決方案,在阿裡巴巴集團内,除了風風火火地 Weex 項目外,各個團隊都有大大小小的解決方案。我們貓客一直持續基于 Tangram 方案來解決頁面動态化的問題,然而在面對持續更新的業務需求時,原有的開發模式也慢慢變得無法勝任,本年度以來,我們 Tangram 體系在各個層面都進行了大跨度的技術更新(可參考文章天貓APP改版之首頁架構&開發模式全面更新),本文再詳細介紹一下頁面内元件體系更新方案。

老元件體系的問題

在原有的 Tangram 體系裡,主要解決了頁面内布局結構的動态化能力,通過 json 資料描述可以組合出常用的頁面結構。然而頁面内具體的坑位樣式,我們稱之為業務元件,是采用正常的 native 代碼開發的,除非内置了足夠多的邏輯,否則元件的樣式調整或者新元件的開發都要釋出版本,無法滿足業務節奏;當然我們也嘗試過使用 Weex 開發業務元件貼到頁面上,但是在體驗和性能上還是有較大的缺陷。

是以總結起來,就是兩點問題:

  1. 業務元件無法動态更新;
  2. 現有的動态元件方案較重,影響性能和體驗;

解決之道

對于上述問題,解決思路其實是比較通用的,要動态更新界面視圖,就需要用界面模闆描述視圖,模闆與資料分離。将動态下發的模闆和資料在端上綁定渲染。要提升性能,也有三大着力點——減少視圖層級與個數,結構盡量扁平化;異步布局渲染流程,解放主線程計算量;回收與複用元件,減少記憶體開銷。

新的元件體系就是在模闆化描述視圖,動态更新視圖,減少視圖層級幾個方面做文章,至于元件的回收複用,則是在頁面級别統一完成;而異步布局渲染流程,則是後續的優化方向。

新的元件方案稱之為 VirtualView,簡稱 VV,也稱為2.0元件,它的設計遵循以下幾個思路:

  1. 以了一種虛拟化開發基礎元件的技術,使用方隻要按照指定協定實作一個基礎元件的尺寸計算、繪制邏輯、布局邏輯,即能實作在宿主容器的 canvas 裡實作直接繪制 UI 内容的,讓最終渲染出來的視圖結構呈現扁平化,提升元件渲染性能。同時為了解決虛拟化 View 帶來的原生 View 的能力損失的問題,它支援加載和渲染原生基礎元件,兩者組合産生合力,既能減少開銷,又能滿足特殊場景下的業務需求。
  2. 内置實作了一系列基礎元件,可以讓使用方直接上手嘗試;而搭建業務元件的方式采用 XML 模闆來編寫,配套 XML 模闆更新 sdk,這使得業務元件動态更新成為了可能。XML 模闆裡還支援寫資料綁定的表達式,在樣式動态化、資料動态化的場景下能非常友善地實作業務需求。
  3. XML 模闆裡涉及到的基礎節點、屬性、字元串資源等都被提前編譯成二進制資源,用戶端加載通過加載編譯後的模闆資料來建立視圖。

設計方案

整體架構

先從整體上預覽一下整個方案的大體結構:

自下往上,自左往右的順序介紹各個子產品:

  • 基礎模闆加載器負責加載編譯後的模闆資料,比如從檔案加載、從二進制數組加載、從網絡加載,将編譯後的二進制模闆資料加載到記憶體裡,通過元件加載器、字元串資源加載器、表達式資源加載器等提取出其中的資源。
  • 架構還内置了基礎元件,包括原子的基礎元件如文本、圖檔、線條,還包括布局類型的基礎元件,比如線性布局、幀布局、網格布局等;每一種類型的基礎元件提供了原生 Native 版本的實作和虛拟化的實作,使用者也可以自定義自己的基礎元件注冊到架構内部,元件構造器通過加載好的元件資料,來構造出整個業務元件樹,并添加到宿主容器裡,對于虛拟元件,會在渲染階段繪制到宿主容器的 canvas 上,而原生元件會作為子 View 添加到宿主容器裡。
  • 架構内部也提供了基礎的表達式能力,主要分兩種,一種是簡單的資料綁定表達式,一種是簡單的邏輯表達式;前者用于在模闆裡寫表達式綁定資料到基礎元件的屬性上,而後者提供了一種輕量級的邏輯運算能力,可以通路基礎元件的屬性并更新,實作一些關聯效果。
  • 事件管理,本方案聚焦于界面的動态化建立,但對業務邏輯的處理主要還是靠原生的代碼實作,是以處理元件的一些常用互動事件,比如元件的點選、長按、觸摸、曝光事件等。事件管理子產品負責将外部的各個類型的事件處理子產品注冊進來,當元件發生特定的事件時,找到對應類型的處理子產品來調用處理。
  • 宿主容器管理負責對虛拟元件的宿主容器進行建構和回收複用的管理。當原有的元件滑出螢幕後,可以回收到統一的池子裡,以便後續複用。
  • 元件管理負責對基礎元件進行建構和回收複用管理。當原有的元件滑出螢幕後,除了宿主容器可以回收複用,内部的基礎元件對象也可以回收到統一的池子裡。如果元件的池子是空的,則在需要的時候構造新的元件。
  • 擴充子產品管理則用于注冊外部功能擴充子產品,當内置的基礎能力無法滿足業務場景的時候,通過擴充子產品注冊特定的功能子產品,然後編寫自定義基礎元件來實作特定功能。
  • 模闆存儲、模闆校驗、模闆更新、模闆注冊則分别負責模闆資料的存儲、安全性校驗、版本校驗、與更新檢查與新模闆下載下傳、注冊模闆資料到架構,整體協同來完成業務元件的動态更新,它并不與整個渲染元件的核心架構耦合,可以作為獨立子產品存在。
  • 配套的工具和服務主要包括模闆編寫工具、模闆編譯工具、模闆更新服務.模闆編寫工具用于 XML 的模闆的編輯,并調用編譯子產品編譯模闆,模闆裡涉及到的元件資源、字元串資源、表達式資源會分别用對應的子產品處理。編譯後端模闆資料可以上傳到模闆更新服務裡,用戶端調用相應的接口檢查是否有更新。

運作流程

有了上述基礎,當我們要開發新的業務元件的時候,除了有新增 Native 邏輯的需求場景(比如新增視訊功能),大部分需求都可以告别原生代碼的編寫,轉而編寫元件模闆。

  1. 先編寫業務元件的模闆。
  2. 通過工具将模闆資料編譯成二進制資料。
  3. 用戶端加載二進制資料可以有兩種路徑,一是直接打包到用戶端裡,寫代碼加載,另一種是釋出到模闆管理背景,用戶端線上更新到模闆資料。
  4. 不論哪種方式加載二進制資料,用戶端接下來的工作是解析二進制資料裡,比如校驗版本号,合法性,讀取頭資訊等等。
  5. 等要真正建立元件的時候,根據元件名稱找到二進制資料,從中解析并建立出真正的元件模型資料。
  6. 從模闆裡建立在元件往往不含有業務資料,因為業務資料是動态性的,使用者需要擷取到業務資料綁定到元件上,元件的屬性裡可以寫表達式來指定使用哪一個資料字段。

值得注意的是,在上述架構及流程裡,描述了一個完整的實踐經驗,但對于本方案來說,核心點在于提供了對元件從編寫到展示流程的實作,其周邊的配套設施,并沒有内置在架構裡,包括用戶端上的模闆管理、更新、注冊子產品,以及後端的模闆釋出服務,因為這些子產品往往涉及業務邏輯,且與各個應用的基礎設施相關,内置在架構裡反而限制了使用方的接入。這裡提供一些可供參考的經驗:

  1. 模闆管理背景要能對模闆的進行釋出、更新,并且按照用戶端版本、平台、元件版本、生效優先級等幾個次元來管理模闆;
  2. 模闆檔案可以存放到 CDN 上供用戶端下載下傳,管理平台隻是對比下發遠資訊;下載下傳檔案要做足夠的校驗;
  3. 用戶端要内置一份打底的模闆資料,這樣不至于因為模闆不存在而出現空窗;
  4. 用戶端可提供一個統一的模闆管理子產品,面向全應用提供服務,在合适的時候請求管理平台檢查有沒有更新,比如啟動、使用者重新整理、推送指令的到達,并且負責下載下傳、檔案校驗、通知頁面重新整理等功能;頁面重新整理可以做優先級區分,比如高優先級的模闆更新主動去重新整理下頁面,而低優先級的可以等二次進入頁面或者重新整理頁面的時候生效;

幾個核心設計

元件的基礎模型

對于元件,我們做了如下定義,每一個基礎的原子元件或者容器元件都會有以下屬性,自定義的基礎元件應當繼承自基礎定義并做擴充。

名稱 類型 預設值 描述
id int 元件id
layoutWidth int/float/enum(match_parent/wrap_content) 元件的布局寬度,與Android裡的概念類似,寫絕對值的時候表示絕對寬高,match_parent表示盡可能撐滿父容器提供的寬高,wrap_content表示根據自身内容的寬高來布局
layoutHeight int/float/enum(match_parent/wrap_content) 元件的布局寬度,與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 最小寬度
minHeight int/float 最小高度
paddingLeft int/float 左内邊距
paddingRight int/float 右内邊距
paddingTop int/float 上内邊距
paddingBottom int/float 下内邊距
layoutMarginLeft int/float 左外邊距
layoutMarginRight int/float 右外邊距
layoutMarginTop int/float 上外邊距
layoutMarginBottom int/float 下外邊距
background int 背景色
backgroundImage string null 背景圖位址
borderWidth int 邊框寬度
borderColor int 邊框顔色
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. 元件類型的語義化,從原來的 1、2、3、4...等數字枚舉類型定義,更新成字元串類型的定義,相容解析原有的數字枚舉定義;
  4. 更好的嵌套布局實作,流式布局在模型描述上支援多層次的嵌套,并優化了 Android 端上的實作方式;
  5. margin 去重的實作,同一層級的容器元件或原子元件直接,支援外邊距 margin 的去重,使得動态資料下控制間距更友善;
  6. 支援 zIndex,無論是容器元件還是原子元件,支援在其樣式上配置 zIndex,zIndex 值越大,繪制層次越高;
  7. 更新元件開發方式,引入動态化元件開發技術,提升元件動态性,實作元件樣式的高效渲染與動态更新;

其他相關的 Tangram 庫

Android

  • vlayout
  • UltraViewPager

iOS

  • LazyScrollView

工具

  • virtualview_tools

繼續閱讀