貓客頁面內組件的動態化方案-Tangram

Tangram 2.0 庫

Android

iOS

背景

技術背景

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

老組件體系的問題

在原有的 Tangram 體系裏,主要解決了頁面內佈局結構的動態化能力,經過 json 數據描述能夠組合出經常使用的頁面結構。然而頁面內具體的坑位樣式,咱們稱之爲業務組件,是採用常規的 native 代碼開發的,除非內置了足夠多的邏輯,不然組件的樣式調整或者新組件的開發都要發佈版本,沒法知足業務節奏;固然咱們也嘗試過使用 Weex 開發業務組件貼到頁面上,可是在體驗和性能上仍是有較大的缺陷。git

因此總結起來,就是兩點問題:github

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

解決之道

對於上述問題,解決思路實際上是比較通用的,要動態更新界面視圖,就須要用界面模板描述視圖,模板與數據分離。將動態下發的模板和數據在端上綁定渲染。要提高性能,也有三大着力點——減小視圖層級與個數,結構儘可能扁平化;異步佈局渲染流程,解放主線程計算量;回收與複用組件,減小內存開銷。json

新的組件體系就是在模板化描述視圖,動態更新視圖,減小視圖層級幾個方面作文章,至於組件的回收複用,則是在頁面級別統一完成;而異步佈局渲染流程,則是後續的優化方向。canvas

新的組件方案稱之爲 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. 從模板裏建立在組件每每不含有業務數據,由於業務數據是動態性的,用戶須要獲取到業務數據綁定到組件上,組件的屬性裏能夠寫表達式來指定使用哪個數據字段。

值得注意的是,在上述架構及流程裏,描述了一個完整的實踐經驗,但對於本方案來講,核心點在於提供了對組件從編寫到展現流程的實現,其周邊的配套設施,並無內置在框架裏,包括客戶端上的模板管理、更新、註冊模塊,以及後端的模板發佈服務,由於這些模塊每每涉及業務邏輯,且與各個應用的基礎設施相關,內置在框架裏反而限制了使用方的接入。這裏提供一些可供參考的經驗:bash

  1. 模板管理後臺要能對模板的進行發佈、更新,而且按照客戶端版本、平臺、組件版本、生效優先級等幾個維度來管理模板;
  2. 模板文件能夠存放到 CDN 上供客戶端下載,管理平臺只是對比下發遠信息;下載文件要作足夠的校驗;
  3. 客戶端要內置一份打底的模板數據,這樣不至於由於模板不存在而出現空窗;
  4. 客戶端可提供一個統一的模板管理模塊,面向全應用提供服務,在合適的時候請求管理平臺檢查有沒有更新,好比啓動、用戶刷新、推送指令的到達,而且負責下載、文件校驗、通知頁面刷新等功能;頁面刷新能夠作優先級區分,好比高優先級的模板更新主動去刷新下頁面,而低優先級的能夠等二次進入頁面或者刷新頁面的時候生效;

幾個核心設計

組件的基礎模型

對於組件,咱們作了以下定義,每個基礎的原子組件或者容器組件都會有如下屬性,自定義的基礎組件應當繼承自基礎定義並作擴展。

名稱 類型 默認值 描述
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...等數字枚舉類型定義,升級成字符串類型的定義,兼容解析原有的數字枚舉定義;
  4. 更好的嵌套佈局實現,流式佈局在模型描述上支持多層次的嵌套,並優化了 Android 端上的實現方式;
  5. margin 去重的實現,同一層級的容器組件或原子組件直接,支持外邊距 margin 的去重,使得動態數據下控制間距更方便;
  6. 支持 zIndex,不管是容器組件仍是原子組件,支持在其樣式上配置 zIndex,zIndex 值越大,繪製層次越高;
  7. 升級組件開發方式,引入動態化組件開發技術,提高組件動態性,實現組件樣式的高效渲染與動態更新;

其餘相關的 Tangram 庫

Android

iOS

工具

相關文章
相關標籤/搜索