背景:因爲目前所在公司的iOS項目的依賴管理是比較原始的狀態,可是APP功能又是愈來愈複雜的,這就帶來的不少問題,好比開發時編譯時間過長、模塊間耦合嚴重、模塊依賴混亂等。最近又據說這個項目中的部分功能可能須要獨立出一個新APP,本着 「Don't repeat yourself」的原則,咱們試着抽離出原項目中的各個模塊,並在新的APP中集成這些模塊。
最近算是初步完成了新APP的模塊化,也算是從中總結了一些經驗拿出來分享一下。同時也完成了一個模塊化框架TinyPart歡迎star。react
作模塊化仍是要結合實際業務,對目前APP的功能作一個模塊劃分,在劃分模塊的時候還須要關注模塊之間的層級。git
好比說,在咱們項目中,模塊被分紅了3個層級:基礎層、中間層、業務層。基礎層模塊好比像網絡框架、持久化、Log、社交化分享這樣的模塊,這一層的模塊咱們能夠稱之爲組件,具備很強的可重用性。中間層模塊能夠有登陸模塊、網絡層、資源模塊等,這一層模塊有一個特色是它們依賴着基礎組件但又沒有很強的業務屬性,同時業務層對這層模塊的依賴是很強的。業務層模塊,就是直接和產品需求對應的模塊了,好比相似朋友圈、直播、Feeds流這樣的業務功能了。github
模塊化首先要作的是代碼層面上獨立,任意一個基礎模塊都是能夠獨立編譯的,底層模塊絕對不能有對上層模塊的代碼依賴,還要確保將來也不會再出現這樣的代碼。react-native
在這裏咱們選擇使用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
能夠自動地添加工程配置和依賴組件。app
在完成上述兩個步驟之後,模塊化工程的構建工做基本就結束了,接下來咱們探討一下如何在工程中更好地使用這些模塊。爲此咱們寫了一個組件化的開源方案 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];