VirtualView Android實現詳解(一)—— 檔案格式與模板編譯

Longerian發表於2017-12-27

在之前的文章《貓客 Tangram 頁面內元件的動態化方案》裡介紹了 Tangram 頁面的元件動態化方案,但是有很多細節沒有展開講,鑑於內容比較多,打算建一個系列,分多篇文章介紹。本文介紹編譯 XML 模板的過程。

Android

iOS

名詞解釋

Virtualview 方案:簡單來講,就是通過自定義 XML 模板搭建 UI 檢視,並通過自研的渲染引擎渲染介面的一種方案,其中支援定義 Canvas 繪製的控制元件,因此成為 virtualview。 編譯模板:將原始 XML 格式的模板序列化成一種二進位制格式的過程。

為何選用二進位制格式

通過 XML 編寫的業務元件,如果直接載入解析,會有幾個問題:一是原始檔案相對較大,因為 XML 裡會有冗餘資訊,如空格、換行、還有重複出現的字串等,檔案體積比較大;二是解析 XML 會有一定開銷,相對於二進位制資料直接解析,XML 解析會比較重,例如節點遍歷、屬性訪問等都顯得有些臃腫。通過提前將 XML 模板處理成二進位制格式,可以將繁重的解析工作從客戶端執行時中剝離出來,而通過將一些重複的資源做合併處理並建立索引,可以減少冗餘資訊,減少模板檔案大小,通常情況下,處理成二進位制格式的模板比原始模板可減少 50% - 60% 的大小。

二進位制模板的格式

儘管之前的文章已經提過二進位制模板檔案的格式,不過這裡還是要再次提及一下:

VirtualView Android實現詳解(一)—— 檔案格式與模板編譯

  • 開始5個位元組固定為 ALIVV;相當於我們的檔案格式的一個標記。
  • 版本號分三個,分別為主版本號,次版本號和修訂版本號,均為 2 個位元組;在無重大重構更新時,前兩位一般不變,第三位用於元件的業務級別變更升級;
  • 元件區的起始位置和長度,均為 4 個位元組;表示這份檔案裡元件區資料從第幾個位元組開始,它總共有多少個位元組,這樣解析這份資料的時候能直接將檔案指標定位到特定位置來讀取資料。
  • 字串區的起始位置和長度,均為 4 個位元組;表示這份檔案裡字串資料從第幾個位元組開始,它總共有多少個位元組。
  • 表示式區的起始位置和長度,均為 4 個位元組;表示這份檔案裡字串資料從第幾個位元組開始,它總共有多少個位元組。
  • 資料區的起始位置和長度,均為 4 個位元組;表示這份檔案裡附加資料從第幾個位元組開始,它總共有多少個位元組。目前這一區塊是作為一種保留區,實際還未使用到。
  • 當前檔案所屬頁編碼,2 個位元組,唯一標識一個頁(保留使用)
  • 當前檔案依賴頁的個數為 2 個位元組,後面為依賴頁的 Id,依賴頁個數大於 0 表示該頁用到了其他頁的資源或者程式碼,在該頁載入之前需要確保依賴頁必須已經載入;(保留使用)
  • 元件區開始,前 4 個位元組表示檔案裡業務元件個數,目前一個 XML 模板編譯成一個二進位制檔案,故其值固定為 1。每個業務元件前 2 個位元組表示業務元件名稱字串的長度,後面為指定長度的字串位元組資料;緊接著是 2 個位元組的編譯後元件二進位制流長度,後面為二進位制程式碼;二進位制程式碼的內容其實就是按照 XML 裡定義的巢狀結構儲存了一棵 UI 樹,只不過節點開始、節點結束、每個節點tag名、屬性、屬性值等都被對映成一個整型索引;在解析的時候會通過索引值到對應的資源池裡找到具體的資源;
  • 字串區開始,前4個位元組表示字串個數,在我們的框架裡,會內建一些系統級別的字串資源,這些字串不用序列化到二進位制檔案裡,而模板檔案裡出現的非系統字串才會作為資源序列化到二進位制檔案。每個字串資源前 4 個位元組字串索引 Id 即它的 hashCode,後面 2 個自己為字串的長度,再後面為對應的字串;
  • 邏輯表示式程式碼表。前 4 個位元組表示邏輯表示式資源個數,每個表示式資源前 4 個自己表示表示式的索引,它是表示式原始字串的 hashCode,後面 2 個位元組表示表示式的長度,後面為對應的表示式內容;
  • 擴充套件資料段是保留為第三方擴充套件使用;(保留使用)

在一開始的時候,我們將所有模板檔案編譯到一個二進位制檔案裡,類似於 Android 編譯資源時做的處理,這樣能更大程度地節省儲存空間。但是考慮到後續要對模板進行動態下發,我們改成一個 XML 檔案一份二進位制檔案的策略,這樣當有個別模板更新的時候,只需要釋出對應的模板,而不需要整體重新編譯。儘管編譯成一份檔案也可以通過增量編譯等方式來解決個別模板更新的問題,但是從管理、維護、使用等各方面考慮,還是一對一的策略更方便一些。

資源的對映處理,有以下邏輯:

  • 顏色:轉換成4位元組整型顏色值,格式 AARRGGBB;
  • 列舉:按照預定義的整數轉換,比如 gravity 的型別,orientation 的型別;
  • 字串:以 hashCode 值作為它的序列化後整數,並在字串資源區建立以 hashCode 為索引的列表,在解析的時候從中獲取原始的字串值;
  • 邏輯表示式:與字串的處理類似;
  • 數字:直接轉換成 4 位元組的整型或者浮點型,並支援帶單位的型別;

其中字串等資源,採用了一個 hashCode 來作為索引值,主要是考慮當模板線上釋出時,字串有變動的情況下,能夠不影響原來的字串資源索引;否則如果按照帶有順序約定的協議來分配資源索引,很容易在模板變更的時候同一索引值在變更前後指向的資源內容是不一樣的,這對穩定性和動態性會產生影響。

另外上面還提到保留使用的一些區段,這是前期設計時考慮加入的,雖然目前沒有在用,可能將來會有使用的地方,比如頁面編碼可以用來歸類别範本的分組,頁面依賴可以指定模板之間資源依賴的關係,可以用來做進一步的資源整合處理。又比如擴充套件資料區,可以用來儲存額外的資料;

編譯的具體流程

VirtualView Android實現詳解(一)—— 檔案格式與模板編譯

  1. 建立一個檔案物件,編譯工具開始編譯模板的時候,先在建立一個輸出檔案的物件,指向特定路徑,後續編譯過程中的資料都寫到這個檔案裡。
  2. 寫入 ALIVV、版本號資料,按照檔案格式,開頭 5 位元組固定未 ALIVV,可先寫入,緊接著 6 個位元組是 3 位版本號,主版本號固定為 1,次版本號固定未 0,修訂版本號每次編譯的時候開發人員通過引數傳入,從 1 開始。
  3. 寫入各區域的佔位空間,根據檔案格式,接下來 32 個位元組分別為元件區、字串區、表示式區、資料區的起始位置值和長度,所以先佔位,初始化為 0。還有當前檔案頁面編碼、以及它的依賴,這也是編譯時使用者傳入,預設頁面編碼為 1,如果沒有依賴的頁面,這一部分不佔空間。
  4. 讀取一個原始模板檔案,一個業務元件對應著一個模板,先讀取一個原始模板資料。
  5. 建立 XML 解析器,因為原始模板是 XML 格式,使用XML解析器來解析其中的內容,XML 解析器會按照 XML 的格式獲取到每個節點以及它的屬性,所以接下來只要遍歷這些節點和屬性來序列化原始資料。
  6. 開始遍歷,先獲取一個節點名,先記錄節點開始標記。
  7. 根據節點名字串,先建立對應的基礎元件編譯器物件,在編譯工具裡,每一個基礎元件都註冊了對應的編譯器型別。使用者開發自定義基礎元件,也要提供自定義編譯器註冊到編譯工具裡。基礎元件和對應的編譯器類通過元件型別關聯起來。
  8. 獲取該基礎元件下所有屬性,開始遍歷屬性並處理。
  9. 每獲取到一個基礎元件屬性,就呼叫編譯器處理屬性,編譯器知道每個屬性應該如何處理,因為這是定義屬性、開發編譯器類的時候確定的,每一種屬性都會被序列化成以下4種型別:int 整型、float 浮點型、string 字串型、表示式型別,前兩者直接作為序列化後的值寫到返回結果裡,後兩者先通過 hashCode 為一個 4 位元組索引作為序列化後的值寫到返回結果裡,真實的內容儲存到臨時列表裡,後面會儲存到單獨的資源區。
  10. 遍歷完當前節點所有屬性。
  11. 按照整型、浮點型、字串、表示式四種類別歸類屬性,按照 4 位元組 key 索引、4 位元組 value 索引存到記憶體裡。
  12. 當前節點處理完畢,寫入一節點結束標記。檢查是否遍歷晚所有節點,如果還有其他節點,回到第 6 步開始處理新的節點,如果沒有,開始下一步準備寫入檔案
  13. 將第 11 步序列化後的元件資料寫入到檔案,將第 9 步裡儲存的字串和表示式資源分別依次寫入到檔案。
  14. 這樣元件區、字串區、表示式區的起始位置都知道了,就可已更新第3步裡預留的空白區域。
  15. 如果有擴充套件資料,可以在表示式區後面寫入擴充套件資料,目前做保留。
  16. 全部寫完之後所有資料輸出到檔案,檔案字尾為 out。

目前的侷限性

在上述編譯過程中,每個基礎元件的編譯都需要對應的編譯模組器來執行二進位制轉換工作,也就是說每個型別的基礎元件都有一個對應的編譯器,這對於擴充套件新的自定義基礎元件帶來了一些不便,因為還要開發對應的編譯器類,目前我們正在將它重構成基於屬性的編譯器模式,並通過配置檔案的方式來解耦對自定義基礎元件節點、自定義屬性編譯處理的邏輯,這樣才能真正釋放它的動態性,有助於提升開發效率與使用便捷度。

相關文章