背景:由於目前所在公司的iOS專案的依賴管理是比較原始的狀態,但是APP功能又是越來越複雜的,這就帶來的很多問題,比如開發時編譯時間過長、模組間耦合嚴重、模組依賴混亂等。最近又聽說這個專案中的部分功能可能需要獨立出一個新APP,本著Don't repeat yourself的原則,我們試著抽離出原專案中的各個模組,並在新的APP中整合這些模組。
最近算是初步完成了新APP的模組化,也算是從中總結了一些經驗拿出來分享一下。
模組劃分
做模組化還是要結合實際業務,對目前APP的功能做一個模組劃分,在劃分模組的時候還需要關注模組之間的層級。
比如說,在我們專案中,模組被分成了3個層級:基礎層、中間層、業務層。基礎層模組比如像網路框架、持久化、Log、社交化分享這樣的模組,這一層的模組我們可以稱之為元件,具有很強的可重用性。中間層模組可以有登入模組、網路層、資源模組等,這一層模組有一個特點是它們依賴著基礎元件但又沒有很強的業務屬性,同時業務層對這層模組的依賴是很強的。業務層模組,就是直接和產品需求對應的模組了,比如類似朋友圈、直播、Feeds流這樣的業務功能了。
程式碼隔離
模組化首先要做的是程式碼層面上獨立,任意一個基礎模組都是可以獨立編譯的,底層模組絕對不能有對上層模組的程式碼依賴,還要確保未來也不會再出現這樣的程式碼。
在這裡我們選擇使用CocoaPods
來確保模組間程式碼隔離,基礎和中間層模組是一定會做成標準的私有pods元件,加入到私有pods倉庫。業務層的模組,則不一定非要加到私有pods倉庫中,也可以使用submodule + local pods的方案。這樣做有兩個原因:其一是業務模組的改動往往比較頻繁,如果是標準的私有pods元件則需要頻繁的操作pod install
或者pod update
;其二是如果是local pod會直接引用對應倉庫的原始檔,在主工程對pods工程下業務模組的改動就是直接對其git倉庫的改動,沒有了頻繁的pod repo push
和pod install
操作。
依賴管理
選擇使用CocoaPods
另外一個重要原因就是,可以通過它來管理模組間的依賴,之前專案各個功能之所以難以複用的重要原因之一就是沒有宣告依賴。這裡的依賴不僅僅是A模組依賴B模組這樣的事情,還可以是A模組執行需要的所有工程配置,比如A模組工程需要新增一個GCC_PREPROCESSOR_DEFINITIONS
預處理巨集才能正常編譯。因此,個人認為模組依賴宣告非常重要,即便沒有像CocoaPods這樣的管理工具,也應該有相關文件來說明每個內部模組或者SDK的依賴。
CocoaPods
的方便之處就在於你必須把你模組的依賴列出來,否則是無法通過pod spec lint
過程的,並且所有的依賴項也都是必須是pods倉庫
。除此以外,依賴的整合也是自動化的,CocoaPods
可以自動地新增工程配置和依賴元件。
模組整合
在完成上述兩個步驟以後,模組化工程的構建工作基本就結束了,接下來我們探討一下如何在工程中更好地使用這些模組。為此我們寫了一個元件化的開源方案 TinyPart [https://github.com/RyanLeeLY/TinyPart]。
一般來說,模組初始化需要在APP啟動或者UI初始化附近的時機來完成,有時候各個模組的啟動順序可能也是有講究的,這些初始化邏輯我們往往會加入到AppDelegate這個類裡。過一段時間我們會發現,AppDelegate這個類變得臃腫不堪,邏輯複雜,難以維護。在TinyPart中,Module的宣告協議包含了UIApplicationDelegate
,這就意味著每一個模組都可以實現有一套自己的UIApplicationDelegate
協議,並且它們之間呼叫順序是可以自定義的。
@interface TPLShareModule : NSObject <TPModuleProtocol>
@end
@implementation TPLShareModule
TP_MODULE_ASYNC
TP_MODULE_PRIORITY(TPLSHARE_MODULE_PRIORITY)
- (void)moduleDidLoad:(TPContext *)context {
[WXApi registerApp:APPID];
}
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation {
return [WXApi handleOpenURL:url delegate:self];
}
- (BOOL)application:(UIApplication *)app
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
return [WXApi handleOpenURL:url delegate:self];
}
@end
複製程式碼
上面的程式碼是一個微信社交分享模組的初始化內容,同時實現了微信分享所要求的UIApplicationDelegate
中的方法。
通訊
訊息
在物件導向中,訊息是一個十分重要的概念,它是物件之前通訊的重要方式。但是,在OC中如果想要向一個物件發訊息,正常做法就是將改物件類的標頭檔案import進來,這樣我們就能夠寫出[aInstance method]
這樣的程式碼了。
然而在模組化中,我們並不希望模組與模組之間相互引用各自的類檔案,但是又想要實現通訊,那怎麼辦呢?通過協議來完成。我們知道OC是一個動態語言,方法的呼叫過程其實是動態的,標頭檔案中訊息方法的宣告只是為了通過編譯前的靜態檢查。也就是說,我們只要寫一個協議來告訴編譯器有這麼一個方法就可以了,至於實際上究竟有沒有這個方法是在訊息發過去以後就知道了。既然OC有這個特性,我們甚至可以直接通過類名和方法名向一個物件傳送訊息,這其實就是網上大部分元件化路由的實現機制。
因此在TinyPart中我們既提供了協議和路由兩種模式來呼叫模組內的服務。
@protocol TestModuleService1 <TPServiceProtocol>
- (void)function1;
@end
@interface TestModuleService1Imp : NSObject <TestModuleService1>
@end
@implementation TestModuleService1Imp
TPSERVICE_AUTO_REGISTER(TestModuleService1) // Service will be registered in "+load" method
- (void)function1 {
NSLog(@"%@", @"TestModuleService1 function1");
}
@end
複製程式碼
上面的程式碼中,我們定義了一個服務的協議。
#import "TestModuleService1.h"
id<TestModuleService1> service1 = [[TPServiceManager sharedInstance] serviceWithName:@"TestModuleService1"];
[service1 function1];
複製程式碼
這裡我們只需要import上述協議的標頭檔案,然後就可以向TestModuleService1
發訊息了。
我們看到上述的跨模組呼叫方案中,只需要暴露一個協議檔案就可以了,下面我們再看一下如何用路由的方式來做到完全不暴露任何標頭檔案。
#import "TPRouter.h"
@interface TestRouter : TPRouter
@end
@implementation TestRouter
TPROUTER_METHOD_EXPORT(action1, {
NSLog(@"TestRouter action1 params=%@", params);
return nil;
});
TPROUTER_METHOD_EXPORT(action2, {
NSLog(@"TestRouter action2 params=%@", params);
return nil;
});
@end
複製程式碼
在這裡我們參考了ReactNative的方案,通過一個TPROUTER_METHOD_EXPORT
巨集來定義一個可供呼叫的路由服務,同時可以傳一個params
引數進了。然後我們再來呼叫這個路由。
[[TPMediator sharedInstance] performAction:@"action1" router:@"Test" params:@{}];
複製程式碼
通知
除了上面提到的兩種普通的模組通訊方案,我們發現在專案中經常會有跨模組的NSNotification
,對於這樣的觀察者模式使用NSNotification
來實現是最便捷的方式了。儘管NSNotification
可以做到模組間解耦,但是對於通知的管理過於鬆散會導致散落在各個模組的NSNotification
邏輯變得十分複雜,因此我們為TinyPart增加了一種有向通訊的方案。
所謂有向通訊,則是在NSNotification
基礎上對通知的傳播方向進行了限制,底層模組對上層模組的通知稱為廣播Broadcast,上層模組對底層模組或者同層模組的通知稱為上報Report。這樣做有兩個好處:一方面更利於通知的維護,另一方面可以幫助我們劃分模組層級,如果我們發現有一個模組需要向多個同級模組進行Report那麼這個模組很有可能應該被劃分到更底層的模組。
用法同NSNotification
類似,只不過建立通知的方法是一個鏈式呼叫,大概就是這樣:
// 傳送
TPNotificationCenter *center2 = [TestModule2 tp_notificationCenter];
[center2 reportNotification:^(TPNotificationMaker *make) {
make.name(@"report_notification_from_TestModule2");
} targetModule:@"TestModule1"];
[center2 broadcastNotification:^(TPNotificationMaker *make) {
make.name(@"broadcast_notification_from_TestModule2").userInfo(@{@"key":@"value"}).object(self);
}];
// 接收
TPNotificationCenter *center1 = [TestModule1 tp_notificationCenter];
[center1 addObserver:self selector:@selector(testNotification:) name:@"report_notification_from_TestModule2" object:nil];
複製程式碼