VirtualView 的重構之路(一)node
VirtualView 是 Tangram 2.0 庫中的一個重要組成部分:若是說 Tangram 1.0 解決了 UI 的動態化佈局及回收重用問題,那麼 Tangram 2.0 所包含的 VirtualView 更進一步的解決了動態化下發新組件的問題。git
用一張圖來解釋 VirtualView 的主要功能:提供了用 XML 去書寫 UI 組件的方案,而後動態化下發編譯好的二進制文件,最後再利用客戶端內置的 SDK 來解析展現這些 UI 組件。github
有關 Tangram 2.0 更多的介紹能夠參考《貓客頁面內組件的動態化方案-Tangram 2.0》,如下是 Tangram 2.0 的主要開源庫:緩存
首先要給你們介紹下咱們爲何要使用二進制文件,主要是考慮如下的幾點:安全
而後就是介紹下二進制文件的格式:bash
有關文件格式更詳細的介紹能夠參照《VirtualView Android實現詳解(一)》,本文的重點仍是介紹重構的思路,以及最新版 VirtualView-iOS 裏模板加載模塊的詳細實現。數據結構
能夠從上文的模板文件格式裏看到,一個二進制模板主要包含了如下幾塊內容:多線程
舊的模板加載模塊工做模式大體以下圖所示:異步
能夠看到整個模板加載功能分紅了兩個模塊:模板加載模塊和建立組件樹模塊。函數
模板加載模塊加載了模板二進制文件,可是隻解析了其中的基礎信息和字符串信息並存儲下來,組件樹結構信息仍使用二進制原樣緩存下來。
建立組件樹模塊在建立新的組件時向模板加載模塊拿須要的組件樹結構信息,進行解析後建立對應的組件樹,並進行屬性的設置,設置字串屬性期間還會向模板加載模塊拿須要的字符串信息。
整體來講設計是能夠知足需求的,可是設計上仍然是存在一些缺陷或者不靈活之處:
首先模板加載模塊一個模塊負責了兩個功能:解析模板信息;管理和存儲已加載的模板列表。並且還附帶了字串映射表等一些功能,沒有作到功能單一。這樣會致使模板的解析和管理功能相互耦合,若是不進行剝離之後兩塊代碼就會耦合愈來愈嚴重。因此咱們須要把模板加載模塊分離成模板解析模塊、模板管理模塊兩塊。
建立組件樹模塊會向模板加載模塊直接拿本身須要的數據信息,這樣隨着代碼的堆積,兩個模塊會日漸耦合加劇,往後任何一方的修改都不可避免的要修改另外一方的代碼。面對這樣的狀況,建議抽象出雙方須要通訊的數據的接口,這樣只要雙方都實現了定義好的通訊接口,內部實現怎麼修改都不會影響另外一方。
解析工做應該放到一個獨立的模塊裏處理。目前的模板是二進制格式的,可是不排除之後會出現其它格式的模板文件的可能性。若是新增一種模板文件格式,就要從新寫兩個配套的模塊,這是十分不科學的。
另外一方面的緣由就是這種分段讀取的模式,致使每次建立新組件的時候都須要重複解析一次組件樹結構信息的二進制數據,這也是耗費性能的一個不合理點。
由於以上第1點和第3點,致使解析模板的代碼要麼和別的功能耦合,要麼分散到了別處,最終的結果就是沒辦法對解析模板的代碼進行異步調用。因此爲了異步化加載模板的目標,須要把全部解析模板的代碼集中到一個模塊中,方便進行異步調用。這是一個由目標肯定代碼結構設計的典型例子。
基於上面咱們要解決的4個方向,首先咱們須要對原來的模塊進行拆分和組合:
這樣咱們就會獲得咱們須要完成的三個獨立的模塊。
而後爲了模塊間的通信,咱們須要定義出來一箇中間數據接口:
因此最終總的設計結構大致就是這樣。
對應 VirtualView-iOS 庫裏的 VVTemplateLoader 類。這裏我把它設計成了一個基類,基類中定義方法進行加載,最終能夠吐出模板解析後的中間數據。這樣的好處就是針對不一樣類型的模板,咱們基於這個基類實現不一樣的解析邏輯,就能夠供其它模塊無縫切換使用了。目前來講實現了一個二進制模板的讀取類,那就是 VVTemplateBinaryLoader。
解析基礎數據、字串數據及組件樹信息的解析代碼所有被集中到這個模塊裏完成,保證類似功能的高度內聚,也使得模塊的功能獨立單一。
保證加載解析模板的功能是個純函數式的過程,沒有任何反作用。這要歸功於把模板管理和存儲的功能都移動到了模板管理模塊。沒有反作用使得解析邏輯能夠被異步調用,有關線程的管理就也能夠放在管理模塊裏進行了。
加載完的模板都由模板管理模塊進行統一存儲管理,這個類就是 VVTemplateManager。這個類裏還有作的一件主要的事情就是異步加載模板的線程管理工做。你們知道異步和多線程常常遇到的一個問題就是數據同步和操做互斥等問題,問了處理這個問題,VVTemplateManager 採用了最簡單的方案,就是將異步加載完模板獲得的中間數據,所有放在主線程統一加入到緩存字典中。例如存儲數據的這一段代碼:
void (^action)(void) = ^{
[self.versions setObject:version forKey:type];
[self.creaters setObject:creater forKey:type];
};
if ([NSThread isMainThread]) {
action();
} else {
dispatch_sync(dispatch_get_main_queue(), action);
}
複製代碼
這是一段很常見的強制進行主線程調用的代碼。爲何這裏要作一次判斷呢?那是由於在主線程直接用 dispatch_sync 去再次調用主線程,會進入線程死鎖。
另一個重要的邏輯就是將異步隊列中還沒有加載完成的模板在必要時進行提早加載。由於咱們把模板放到異步線程隊列裏去加載,有時候並不能肯定在使用到這個模板的時候它就必定被加載完了。因此代碼裏有這麼一段邏輯:
if ([self.loadedTypes containsObject:type] == NO && _operationQueue) {
// Try to find unloaded template in queue and load it immediately.
BOOL isFirst = YES;
for (NSOperation *operation in _operationQueue.operations.reverseObjectEnumerator) {
if ([operation.name isEqualToString:type]) {
if (isFirst) {
[operation main];
isFirst = NO;
}
[operation cancel];
}
}
}
複製代碼
若是已加載的模板裏沒有包含咱們要使用的 type
,那麼就嘗試從當前的異步讀取隊列裏找一找有沒有對應的 type
,對隊列裏最後一個知足條件的任務進行當即調用,保證對應模板被當即加載,而後把異步隊列裏的對應任務都取消掉。
因此說使用 VirtualView-iOS 時,能夠放心的把全部的模板所有放到異步線程去加載,而不用擔憂後續的調用會出問題。
組件樹的重要數據就兩個,組件樹種每個節點上組件的 class 以及這個組件的屬性列表。組件自己是樹狀結構的,因此中間數據固然也是樹狀結構會最匹配。因此設計出來的最終中間數據結構就是 VVNodeCreater 和 VVPropertySetter:
@interface VVNodeCreater : NSObject
@property (nonatomic, copy, nullable) NSString *nodeClassName;
@property (nonatomic, strong, nonnull) NSMutableArray<VVPropertySetter *> *propertySetters;
@property (nonatomic, strong, nonnull) NSMutableArray<VVNodeCreater *> *subCreaters;
@end
複製代碼
@interface VVPropertySetter : NSObject
@property (nonatomic, assign, readonly) int key;
@property (nonatomic, strong, readonly, nullable) NSString *name;
@end
複製代碼
這裏最先設計的時候打算他們只用來存儲結構和數據,可是後來發現他們本身自己用自遞歸的方式建立組件樹會無比的方便,因此他們同時負責了緩存中間數據及一鍵建立組件樹的功能!
也是由於建立組件樹這個模塊的功能單獨拎出來太過於輕量化了,因此最終的實現中就把它和中間數據的模型直接融合了。融合了以後他們兩個類每一個類的代碼也才五六十行,因此說一開始的設計也的確有點過分設計了。
VVPropertySetter 也採用了設計成基類的方式,這樣不一樣類型的屬性值就能夠經過重載分別實現 VVPropertyIntSetter、VVPropertyFloatSetter 和 VVPropertyStringSetter 來實現。這樣作一方面可使得邏輯能夠不經過一大堆 if...else...
或者 switch...case...
來寫得很難看,使得 VVNodeCreater 在調用時使用統一的入口方便調用,並且另外一方面也是更加方便後續字符串表達式等功能的擴展。關於字符串表達式的實現原理會在後續的文章裏繼續說明。
至此,VirtualView-iOS 模板加載功能的設計及實現細節也介紹的差很少了,但願對你們瞭解 VirtialView 及重構的思路有必定幫助。
在接下來,我還會陸續介紹其餘模塊設計及重構的思路,敬請期待。