VirtualView 的重構之路(一)
前言
VirtualView 是 Tangram 2.0 庫中的一個重要組成部分:如果說 Tangram 1.0 解決了 UI 的動態化佈局及回收重用問題,那麼 Tangram 2.0 所包含的 VirtualView 更進一步的解決了動態化下發新元件的問題。
用一張圖來解釋 VirtualView 的主要功能:提供了用 XML 去書寫 UI 元件的方案,然後動態化下發編譯好的二進位制檔案,最後再利用客戶端內建的 SDK 來解析展示這些 UI 元件。
有關 Tangram 2.0 更多的介紹可以參考《貓客頁面內元件的動態化方案-Tangram 2.0》,以下是 Tangram 2.0 的主要開源庫:
Android
iOS
二進位制模板檔案的格式
首先要給大家介紹下我們為什麼要使用二進位制檔案,主要是考慮以下的幾點:
- 效能上的考慮:二進位制體積更小,讀取也更快速
- 安全性的考慮:二進位制比較容易做 hash 校驗,也相對來說不太容易直接篡改
- 方便擴充自定義資料:後續我們會加入一些表示式或者是動畫邏輯等高階功能,使用二進位制進行預編譯可以完成更強大的功能
然後就是介紹下二進位制檔案的格式:
有關檔案格式更詳細的介紹可以參照《VirtualView Android實現詳解(一)》,本文的重點還是介紹重構的思路,以及最新版 VirtualView-iOS 裡模板載入模組的詳細實現。
舊的模板載入功能設計
可以從上文的模板檔案格式裡看到,一個二進位制模板主要包含了以下幾塊內容:
- 檔案頭和模板名等基礎資訊
- 元件樹結構資訊
- 字串資訊
- 表示式資訊(暫未啟用)
- 擴充套件資訊(暫未啟用)
舊的模板載入模組工作模式大致如下圖所示:
可以看到整個模板載入功能分成了兩個模組:模板載入模組和建立元件樹模組。
模板載入模組載入了模板二進位制檔案,但是隻解析了其中的基礎資訊和字串資訊並儲存下來,元件樹結構資訊仍使用二進位制原樣快取下來。
建立元件樹模組在建立新的元件時向模板載入模組拿需要的元件樹結構資訊,進行解析後建立對應的元件樹,並進行屬性的設定,設定字串屬性期間還會向模板載入模組拿需要的字串資訊。
總體來說設計是可以滿足需求的,但是設計上仍然是存在一些缺陷或者不靈活之處:
1. 模組並沒有做到功能單一,相互獨立
首先模板載入模組一個模組負責了兩個功能:解析模板資訊;管理和儲存已載入的模板列表。而且還附帶了字串對映表等一些功能,沒有做到功能單一。這樣會導致模板的解析和管理功能相互耦合,如果不進行剝離以後兩塊程式碼就會耦合越來越嚴重。所以我們需要把模板載入模組分離成模板解析模組、模板管理模組兩塊。
2. 模組間通訊沒有面向介面
建立元件樹模組會向模板載入模組直接拿自己需要的資料資訊,這樣隨著程式碼的堆積,兩個模組會日漸耦合加重,日後任何一方的修改都不可避免的要修改另一方的程式碼。面對這樣的情況,建議抽象出雙方需要通訊的資料的介面,這樣只要雙方都實現了定義好的通訊介面,內部實現怎麼修改都不會影響另一方。
3. 解析模板的工作分散到了兩個模組裡處理
解析工作應該放到一個獨立的模組裡處理。目前的模板是二進位制格式的,但是不排除以後會出現其它格式的模板檔案的可能性。如果新增一種模板檔案格式,就要重新寫兩個配套的模組,這是十分不科學的。
另一方面的原因就是這種分段讀取的模式,導致每次建立新元件的時候都需要重複解析一次元件樹結構資訊的二進位制資料,這也是耗費效能的一個不合理點。
4. 解析模板程式碼分散及耦合的問題導致無法非同步化處理
因為以上第1點和第3點,導致解析模板的程式碼要麼和別的功能耦合,要麼分散到了別處,最終的結果就是沒辦法對解析模板的程式碼進行非同步呼叫。所以為了非同步化載入模板的目標,需要把所有解析模板的程式碼集中到一個模組中,方便進行非同步呼叫。這是一個由目標確定程式碼結構設計的典型例子。
全新的載入模板功能設計
基於上面我們要解決的4個方向,首先我們需要對原來的模組進行拆分和組合:
這樣我們就會得到我們需要完成的三個獨立的模組。
然後為了模組間的通訊,我們需要定義出來一箇中間資料介面:
所以最終總的設計結構大體就是這樣。
1. 模板解析模組
對應 VirtualView-iOS 庫裡的 VVTemplateLoader 類。這裡我把它設計成了一個基類,基類中定義方法進行載入,最終可以吐出模板解析後的中間資料。這樣的好處就是針對不同型別的模板,我們基於這個基類實現不同的解析邏輯,就可以供其它模組無縫切換使用了。目前來說實現了一個二進位制模板的讀取類,那就是 VVTemplateBinaryLoader。
解析基礎資料、字串資料及元件樹資訊的解析程式碼全部被集中到這個模組裡完成,保證相似功能的高度內聚,也使得模組的功能獨立單一。
保證載入解析模板的功能是個純函式式的過程,沒有任何副作用。這要歸功於把模板管理和儲存的功能都移動到了模板管理模組。沒有副作用使得解析邏輯可以被非同步呼叫,有關執行緒的管理就也可以放在管理模組裡進行了。
2. 模板管理模組
載入完的模板都由模板管理模組進行統一儲存管理,這個類就是 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 時,可以放心的把所有的模板全部放到非同步執行緒去載入,而不用擔心後續的呼叫會出問題。
3. 模板中間資料及建立元件樹模組
元件樹的重要資料就兩個,元件樹種每一個節點上元件的 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 及重構的思路有一定幫助。
在接下來,我還會陸續介紹其他模組設計及重構的思路,敬請期待。