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

原文鏈接:http://pingguohe.net/2017/12/27/deep-into-virtualview-android-1.html

在之前的文章《貓客 Tangram 頁面內(nèi)組件的動態(tài)化方案》里介紹了 Tangram 頁面的組件動態(tài)化方案,但是有很多細節(jié)沒有展開講,鑒于內(nèi)容比較多,打算建一個系列,分多篇文章介紹。本文介紹編譯 XML 模板的過程。

Android

iOS

名詞解釋

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

為何選用二進制格式

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

二進制模板的格式

盡管之前的文章已經(jīng)提過二進制模板文件的格式,不過這里還是要再次提及一下:

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

在一開始的時候,我們將所有模板文件編譯到一個二進制文件里,類似于 Android 編譯資源時做的處理,這樣能更大程度地節(jié)省存儲空間。但是考慮到后續(xù)要對模板進行動態(tài)下發(fā),我們改成一個 XML 文件一份二進制文件的策略,這樣當有個別模板更新的時候,只需要發(fā)布對應(yīng)的模板,而不需要整體重新編譯。盡管編譯成一份文件也可以通過增量編譯等方式來解決個別模板更新的問題,但是從管理、維護、使用等各方面考慮,還是一對一的策略更方便一些。

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

  • 顏色:轉(zhuǎn)換成4字節(jié)整型顏色值,格式 AARRGGBB;
  • 枚舉:按照預(yù)定義的整數(shù)轉(zhuǎn)換,比如 gravity 的類型,orientation 的類型;
  • 字符串:以 hashCode 值作為它的序列化后整數(shù),并在字符串資源區(qū)建立以 hashCode 為索引的列表,在解析的時候從中獲取原始的字符串值;
  • 邏輯表達式:與字符串的處理類似;
  • 數(shù)字:直接轉(zhuǎn)換成 4 字節(jié)的整型或者浮點型,并支持帶單位的類型;

其中字符串等資源,采用了一個 hashCode 來作為索引值,主要是考慮當模板在線發(fā)布時,字符串有變動的情況下,能夠不影響原來的字符串資源索引;否則如果按照帶有順序約定的協(xié)議來分配資源索引,很容易在模板變更的時候同一索引值在變更前后指向的資源內(nèi)容是不一樣的,這對穩(wěn)定性和動態(tài)性會產(chǎn)生影響。

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

編譯的具體流程

image
  1. 創(chuàng)建一個文件對象,編譯工具開始編譯模板的時候,先在創(chuàng)建一個輸出文件的對象,指向特定路徑,后續(xù)編譯過程中的數(shù)據(jù)都寫到這個文件里。
  2. 寫入 ALIVV、版本號數(shù)據(jù),按照文件格式,開頭 5 字節(jié)固定未 ALIVV,可先寫入,緊接著 6 個字節(jié)是 3 位版本號,主版本號固定為 1,次版本號固定未 0,修訂版本號每次編譯的時候開發(fā)人員通過參數(shù)傳入,從 1 開始。
  3. 寫入各區(qū)域的占位空間,根據(jù)文件格式,接下來 32 個字節(jié)分別為組件區(qū)、字符串區(qū)、表達式區(qū)、數(shù)據(jù)區(qū)的起始位置值和長度,所以先占位,初始化為 0。還有當前文件頁面編碼、以及它的依賴,這也是編譯時用戶傳入,默認頁面編碼為 1,如果沒有依賴的頁面,這一部分不占空間。
  4. 讀取一個原始模板文件,一個業(yè)務(wù)組件對應(yīng)著一個模板,先讀取一個原始模板數(shù)據(jù)。
  5. 創(chuàng)建 XML 解析器,因為原始模板是 XML 格式,使用XML解析器來解析其中的內(nèi)容,XML 解析器會按照 XML 的格式獲取到每個節(jié)點以及它的屬性,所以接下來只要遍歷這些節(jié)點和屬性來序列化原始數(shù)據(jù)。
  6. 開始遍歷,先獲取一個節(jié)點名,先記錄節(jié)點開始標記。
  7. 根據(jù)節(jié)點名字符串,先創(chuàng)建對應(yīng)的基礎(chǔ)組件編譯器對象,在編譯工具里,每一個基礎(chǔ)組件都注冊了對應(yīng)的編譯器類型。用戶開發(fā)自定義基礎(chǔ)組件,也要提供自定義編譯器注冊到編譯工具里。基礎(chǔ)組件和對應(yīng)的編譯器類通過組件類型關(guān)聯(lián)起來。
  8. 獲取該基礎(chǔ)組件下所有屬性,開始遍歷屬性并處理。
  9. 每獲取到一個基礎(chǔ)組件屬性,就調(diào)用編譯器處理屬性,編譯器知道每個屬性應(yīng)該如何處理,因為這是定義屬性、開發(fā)編譯器類的時候確定的,每一種屬性都會被序列化成以下4種類型:int 整型、float 浮點型、string 字符串型、表達式類型,前兩者直接作為序列化后的值寫到返回結(jié)果里,后兩者先通過 hashCode 為一個 4 字節(jié)索引作為序列化后的值寫到返回結(jié)果里,真實的內(nèi)容存儲到臨時列表里,后面會存儲到單獨的資源區(qū)。
  10. 遍歷完當前節(jié)點所有屬性。
  11. 按照整型、浮點型、字符串、表達式四種類別歸類屬性,按照 4 字節(jié) key 索引、4 字節(jié) value 索引存到內(nèi)存里。
  12. 當前節(jié)點處理完畢,寫入一節(jié)點結(jié)束標記。檢查是否遍歷晚所有節(jié)點,如果還有其他節(jié)點,回到第 6 步開始處理新的節(jié)點,如果沒有,開始下一步準備寫入文件
  13. 將第 11 步序列化后的組件數(shù)據(jù)寫入到文件,將第 9 步里存儲的字符串和表達式資源分別依次寫入到文件。
  14. 這樣組件區(qū)、字符串區(qū)、表達式區(qū)的起始位置都知道了,就可已更新第3步里預(yù)留的空白區(qū)域。
  15. 如果有擴展數(shù)據(jù),可以在表達式區(qū)后面寫入擴展數(shù)據(jù),目前做保留。
  16. 全部寫完之后所有數(shù)據(jù)輸出到文件,文件后綴為 .out

目前的局限性

在上述編譯過程中,每個基礎(chǔ)組件的編譯都需要對應(yīng)的編譯模塊器來執(zhí)行二進制轉(zhuǎn)換工作,也就是說每個類型的基礎(chǔ)組件都有一個對應(yīng)的編譯器,這對于擴展新的自定義基礎(chǔ)組件帶來了一些不便,因為還要開發(fā)對應(yīng)的編譯器類,目前我們正在將它重構(gòu)成基于屬性的編譯器模式,并通過配置文件的方式來解耦對自定義基礎(chǔ)組件節(jié)點、自定義屬性編譯處理的邏輯,這樣才能真正釋放它的動態(tài)性,有助于提升開發(fā)效率與使用便捷度。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,837評論 18 139
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,740評論 18 399
  • 早上起來梳頭的時候又發(fā)現(xiàn)幾根白發(fā),幾欲拔掉,最終忍下。 從當初看見白發(fā)就驚呼心痛到后來黯然神傷再到如今坦然面對,我...
    楊榆閱讀 393評論 0 1
  • 對象=引用類型的值=引用類型的實例 ECMAscript原生引用類型:Object,Array Object創(chuàng)建實...
    余生筑閱讀 187評論 0 0
  • 哎呀~~ 此時此刻~我想吟詩一首 啊!~~ 古巷古韻古河陽, 一年一戶一花燈。 燈火笙歌春如海, 三里龍街不夜村。
    小城人閱讀 143評論 2 1