在以前的文章《貓客 Tangram 頁面內組件的動態化方案》 裏介紹了 Tangram 頁面的組件動態化方案,可是有不少細節沒有展開講,鑑於內容比較多,打算建一個系列,分多篇文章介紹。本文介紹編譯 XML 模板的過程。html
Android
iOS
名詞解釋
Virtualview 方案:簡單來說,就是經過自定義 XML 模板搭建 UI 視圖,並經過自研的渲染引擎渲染界面的一種方案,其中支持定義 Canvas 繪製的控件,所以成爲 virtualview。 編譯模板:將原始 XML 格式的模板序列化成一種二進制格式的過程。git
爲什麼選用二進制格式
經過 XML 編寫的業務組件,若是直接加載解析,會有幾個問題:一是原始文件相對較大,由於 XML 裏會有冗餘信息,如空格、換行、還有重複出現的字符串等,文件體積比較大;二是解析 XML 會有必定開銷,相對於二進制數據直接解析,XML 解析會比較重,例如節點遍歷、屬性訪問等都顯得有些臃腫。經過提早將 XML 模板處理成二進制格式,能夠將繁重的解析工做從客戶端運行時中剝離出來,而經過將一些重複的資源作合併處理並創建索引,能夠減小冗餘信息,減小模板文件大小,一般狀況下,處理成二進制格式的模板比原始模板可減小 50% - 60% 的大小。github
二進制模板的格式
儘管以前的文章已經提過二進制模板文件的格式,不過這裏仍是要再次說起一下:框架
開始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 來做爲索引值,主要是考慮當模板在線發佈時,字符串有變更的狀況下,可以不影響原來的字符串資源索引;不然若是按照帶有順序約定的協議來分配資源索引,很容易在模板變動的時候同一索引值在變動先後指向的資源內容是不同的,這對穩定性和動態性會產生影響。.net
另外上面還提到保留使用的一些區段,這是前期設計時考慮加入的,雖然目前沒有在用,可能未來會有使用的地方,好比頁面編碼能夠用來歸類模板的分組,頁面依賴能夠指定模板之間資源依賴的關係,能夠用來作進一步的資源整合處理。又好比擴展數據區,能夠用來存儲額外的數據;設計
編譯的具體流程
建立一個文件對象,編譯工具開始編譯模板的時候,先在建立一個輸出文件的對象,指向特定路徑,後續編譯過程當中的數據都寫到這個文件裏。
寫入 ALIVV、版本號數據,按照文件格式,開頭 5 字節固定未 ALIVV,可先寫入,緊接着 6 個字節是 3 位版本號,主版本號固定爲 1,次版本號固定未 0,修訂版本號每次編譯的時候開發人員經過參數傳入,從 1 開始。
寫入各區域的佔位空間,根據文件格式,接下來 32 個字節分別爲組件區、字符串區、表達式區、數據區的起始位置值和長度,因此先佔位,初始化爲 0。還有當前文件頁面編碼、以及它的依賴,這也是編譯時用戶傳入,默認頁面編碼爲 1,若是沒有依賴的頁面,這一部分不佔空間。
讀取一個原始模板文件,一個業務組件對應着一個模板,先讀取一個原始模板數據。
建立 XML 解析器,由於原始模板是 XML 格式,使用XML解析器來解析其中的內容,XML 解析器會按照 XML 的格式獲取到每一個節點以及它的屬性,因此接下來只要遍歷這些節點和屬性來序列化原始數據。
開始遍歷,先獲取一個節點名,先記錄節點開始標記。
根據節點名字符串,先建立對應的基礎組件編譯器對象,在編譯工具裏,每個基礎組件都註冊了對應的編譯器類型。用戶開發自定義基礎組件,也要提供自定義編譯器註冊到編譯工具裏。基礎組件和對應的編譯器類經過組件類型關聯起來。
獲取該基礎組件下全部屬性,開始遍歷屬性並處理。
每獲取到一個基礎組件屬性,就調用編譯器處理屬性,編譯器知道每一個屬性應該如何處理,由於這是定義屬性、開發編譯器類的時候肯定的,每一種屬性都會被序列化成如下4種類型:int 整型、float 浮點型、string 字符串型、表達式類型,前二者直接做爲序列化後的值寫到返回結果裏,後二者先經過 hashCode 爲一個 4 字節索引做爲序列化後的值寫到返回結果裏,真實的內容存儲到臨時列表裏,後面會存儲到單獨的資源區。
遍歷完當前節點全部屬性。
按照整型、浮點型、字符串、表達式四種類別歸類屬性,按照 4 字節 key 索引、4 字節 value 索引存到內存裏。
當前節點處理完畢,寫入一節點結束標記。檢查是否遍歷晚全部節點,若是還有其餘節點,回到第 6 步開始處理新的節點,若是沒有,開始下一步準備寫入文件
將第 11 步序列化後的組件數據寫入到文件,將第 9 步裏存儲的字符串和表達式資源分別依次寫入到文件。
這樣組件區、字符串區、表達式區的起始位置都知道了,就可已更新第3步裏預留的空白區域。
若是有擴展數據,能夠在表達式區後面寫入擴展數據,目前作保留。
所有寫完以後全部數據輸出到文件,文件後綴爲 out。
目前的侷限性
在上述編譯過程當中,每一個基礎組件的編譯都須要對應的編譯模塊器來執行二進制轉換工做,也就是說每一個類型的基礎組件都有一個對應的編譯器,這對於擴展新的自定義基礎組件帶來了一些不便,由於還要開發對應的編譯器類,目前咱們正在將它重構成基於屬性的編譯器模式,並經過配置文件的方式來解耦對自定義基礎組件節點、自定義屬性編譯處理的邏輯,這樣才能真正釋放它的動態性,有助於提高開發效率與使用便捷度。指針