您好,登錄后才能下訂單哦!
背景:由于目前所在公司的iOS項(xiàng)目的依賴管理是比較原始的狀態(tài),但是APP功能又是越來(lái)越復(fù)雜的,這就帶來(lái)的很多問(wèn)題,比如開(kāi)發(fā)時(shí)編譯時(shí)間過(guò)長(zhǎng)、模塊間耦合嚴(yán)重、模塊依賴混亂等。最近又聽(tīng)說(shuō)這個(gè)項(xiàng)目中的部分功能可能需要獨(dú)立出一個(gè)新APP,本著“Don't repeat yourself”的原則,我們?cè)囍殡x出原項(xiàng)目中的各個(gè)模塊,并在新的APP中集成這些模塊。
最近算是初步完成了新APP的模塊化,也算是從中總結(jié)了一些經(jīng)驗(yàn)?zāi)贸鰜?lái)分享一下。同時(shí)也完成了一個(gè)模塊化框架TinyPart歡迎star。
模塊劃分
做模塊化還是要結(jié)合實(shí)際業(yè)務(wù),對(duì)目前APP的功能做一個(gè)模塊劃分,在劃分模塊的時(shí)候還需要關(guān)注模塊之間的層級(jí)。
比如說(shuō),在我們項(xiàng)目中,模塊被分成了3個(gè)層級(jí):基礎(chǔ)層、中間層、業(yè)務(wù)層?;A(chǔ)層模塊比如像網(wǎng)絡(luò)框架、持久化、Log、社交化分享這樣的模塊,這一層的模塊我們可以稱之為組件,具有很強(qiáng)的可重用性。中間層模塊可以有登錄模塊、網(wǎng)絡(luò)層、資源模塊等,這一層模塊有一個(gè)特點(diǎn)是它們依賴著基礎(chǔ)組件但又沒(méi)有很強(qiáng)的業(yè)務(wù)屬性,同時(shí)業(yè)務(wù)層對(duì)這層模塊的依賴是很強(qiáng)的。業(yè)務(wù)層模塊,就是直接和產(chǎn)品需求對(duì)應(yīng)的模塊了,比如類似朋友圈、直播、Feeds流這樣的業(yè)務(wù)功能了。
代碼隔離
模塊化首先要做的是代碼層面上獨(dú)立,任意一個(gè)基礎(chǔ)模塊都是可以獨(dú)立編譯的,底層模塊絕對(duì)不能有對(duì)上層模塊的代碼依賴,還要確保未來(lái)也不會(huì)再出現(xiàn)這樣的代碼。
在這里我們選擇使用CocoaPods來(lái)確保模塊間代碼隔離,基礎(chǔ)和中間層模塊是一定會(huì)做成標(biāo)準(zhǔn)的私有pods組件,加入到私有pods倉(cāng)庫(kù)。業(yè)務(wù)層的模塊,則不一定非要加到私有pods倉(cāng)庫(kù)中,也可以使用submodule + local pods的方案。這樣做有兩個(gè)原因:其一是業(yè)務(wù)模塊的改動(dòng)往往比較頻繁,如果是標(biāo)準(zhǔn)的私有pods組件則需要頻繁的操作pod install或者pod update;其二是如果是local pod會(huì)直接引用對(duì)應(yīng)倉(cāng)庫(kù)的源文件,在主工程對(duì)pods工程下業(yè)務(wù)模塊的改動(dòng)就是直接對(duì)其git倉(cāng)庫(kù)的改動(dòng),沒(méi)有了頻繁的pod repo push和pod install操作。
依賴管理
選擇使用CocoaPods另外一個(gè)重要原因就是,可以通過(guò)它來(lái)管理模塊間的依賴,之前項(xiàng)目各個(gè)功能之所以難以復(fù)用的重要原因之一就是沒(méi)有聲明依賴。這里的依賴不僅僅是A模塊依賴B模塊這樣的事情,還可以是A模塊運(yùn)行需要的所有工程配置,比如A模塊工程需要添加一個(gè)GCC_PREPROCESSOR_DEFINITIONS預(yù)處理宏才能正常編譯。因此,個(gè)人認(rèn)為模塊依賴聲明非常重要,即便沒(méi)有像CocoaPods這樣的管理工具,也應(yīng)該有相關(guān)文檔來(lái)說(shuō)明每個(gè)內(nèi)部模塊或者SDK的依賴。
CocoaPods的方便之處就在于你必須把你模塊的依賴列出來(lái),否則是無(wú)法通過(guò)pod spec lint過(guò)程的,并且所有的依賴項(xiàng)也都是必須是pods倉(cāng)庫(kù)。除此以外,依賴的集成也是自動(dòng)化的,CocoaPods可以自動(dòng)地添加工程配置和依賴組件。
模塊集成
在完成上述兩個(gè)步驟以后,模塊化工程的構(gòu)建工作基本就結(jié)束了,接下來(lái)我們探討一下如何在工程中更好地使用這些模塊。為此我們寫(xiě)了一個(gè)組件化的開(kāi)源方案 TinyPart。
一般來(lái)說(shuō),模塊初始化需要在APP啟動(dòng)或者UI初始化附近的時(shí)機(jī)來(lái)完成,有時(shí)候各個(gè)模塊的啟動(dòng)順序可能也是有講究的,這些初始化邏輯我們往往會(huì)加入到AppDelegate這個(gè)類里。過(guò)一段時(shí)間我們會(huì)發(fā)現(xiàn),AppDelegate這個(gè)類變得臃腫不堪,邏輯復(fù)雜,難以維護(hù)。在TinyPart中,Module的聲明協(xié)議包含了UIApplicationDelegate,這就意味著每一個(gè)模塊都可以實(shí)現(xiàn)有一套自己的UIApplicationDelegate協(xié)議,并且它們之間調(diào)用順序是可以自定義的。
@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
上面的代碼是一個(gè)微信社交分享模塊的初始化內(nèi)容,同時(shí)實(shí)現(xiàn)了微信分享所要求的UIApplicationDelegate中的方法。
消息
在面向?qū)ο笾?,消息是一個(gè)十分重要的概念,它是對(duì)象之前通信的重要方式。但是,在OC中如果想要向一個(gè)對(duì)象發(fā)消息,正常做法就是將改對(duì)象類的頭文件import進(jìn)來(lái),這樣我們就能夠?qū)懗鯷aInstance method]這樣的代碼了。
然而在模塊化中,我們并不希望模塊與模塊之間相互引用各自的類文件,但是又想要實(shí)現(xiàn)通信,那怎么辦呢?通過(guò)協(xié)議來(lái)完成。我們知道OC是一個(gè)動(dòng)態(tài)語(yǔ)言,方法的調(diào)用過(guò)程其實(shí)是動(dòng)態(tài)的,頭文件中消息方法的聲明只是為了通過(guò)編譯前的靜態(tài)檢查。也就是說(shuō),我們只要寫(xiě)一個(gè)協(xié)議來(lái)告訴編譯器有這么一個(gè)方法就可以了,至于實(shí)際上究竟有沒(méi)有這個(gè)方法是在消息發(fā)過(guò)去以后就知道了。既然OC有這個(gè)特性,我們甚至可以直接通過(guò)類名和方法名向一個(gè)對(duì)象發(fā)送消息,這其實(shí)就是網(wǎng)上大部分組件化路由的實(shí)現(xiàn)機(jī)制。
因此在TinyPart中我們既提供了協(xié)議和路由兩種模式來(lái)調(diào)用模塊內(nèi)的服務(wù)。
@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
上面的代碼中,我們定義了一個(gè)服務(wù)的協(xié)議。
#import "TestModuleService1.h" id<TestModuleService1> service1 = [[TPServiceManager sharedInstance] serviceWithName:@"TestModuleService1"]; [service1 function1];
這里我們只需要import上述協(xié)議的頭文件,然后就可以向TestModuleService1發(fā)消息了。
我們看到上述的跨模塊調(diào)用方案中,只需要暴露一個(gè)協(xié)議文件就可以了,下面我們?cè)倏匆幌氯绾斡寐酚傻姆绞絹?lái)做到完全不暴露任何頭文件。
#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的方案,通過(guò)一個(gè)TPROUTER_METHOD_EXPORT宏來(lái)定義一個(gè)可供調(diào)用的路由服務(wù),同時(shí)可以傳一個(gè)params參數(shù)進(jìn)了。然后我們?cè)賮?lái)調(diào)用這個(gè)路由。
[[TPMediator sharedInstance] performAction:@"action1" router:@"Test" params:@{}];
通知
除了上面提到的兩種普通的模塊通信方案,我們發(fā)現(xiàn)在項(xiàng)目中經(jīng)常會(huì)有跨模塊的NSNotification,對(duì)于這樣的觀察者模式使用NSNotification來(lái)實(shí)現(xiàn)是最便捷的方式了。盡管NSNotification可以做到模塊間解耦,但是對(duì)于通知的管理過(guò)于松散會(huì)導(dǎo)致散落在各個(gè)模塊的NSNotification邏輯變得十分復(fù)雜,因此我們?yōu)門(mén)inyPart增加了一種有向通信的方案。
所謂有向通信,則是在NSNotification基礎(chǔ)上對(duì)通知的傳播方向進(jìn)行了限制,底層模塊對(duì)上層模塊的通知稱為廣播Broadcast,上層模塊對(duì)底層模塊或者同層模塊的通知稱為上報(bào)Report。這樣做有兩個(gè)好處:一方面更利于通知的維護(hù),另一方面可以幫助我們劃分模塊層級(jí),如果我們發(fā)現(xiàn)有一個(gè)模塊需要向多個(gè)同級(jí)模塊進(jìn)行Report那么這個(gè)模塊很有可能應(yīng)該被劃分到更底層的模塊。
用法同NSNotification類似,只不過(guò)創(chuàng)建通知的方法是一個(gè)鏈?zhǔn)秸{(diào)用,大概就是這樣:
// 發(fā)送 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];
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。