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

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

Android

iOS

名詞解釋

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

 

爲何選用二進制格式

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

二進制模板的格式

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

 

  • 開始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 來作爲索引值,主要是考慮當模板在線發佈時,字符串有變動的情況下,能夠不影響原來的字符串資源索引;否則如果按照帶有順序約定的協議來分配資源索引,很容易在模板變更的時候同一索引值在變更前後指向的資源內容是不一樣的,這對穩定性和動態性會產生影響。

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

編譯的具體流程

image

  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

目前的侷限性

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章