IOS-组件化架构漫谈
2019獨角獸企業(yè)重金招聘Python工程師標(biāo)準(zhǔn)>>>
組件化架構(gòu)的由來
隨著移動互聯(lián)網(wǎng)的不斷發(fā)展,很多程序代碼量和業(yè)務(wù)越來越多,現(xiàn)有架構(gòu)已經(jīng)不適合公司業(yè)務(wù)的發(fā)展速度了,很多都面臨著重構(gòu)的問題。
在公司項目開發(fā)中,如果項目比較小,普通的單工程+MVC架構(gòu)就可以滿足大多數(shù)需求了。但是像淘寶、蘑菇街、微信這樣的大型項目,原有的單工程架構(gòu)就不足以滿足架構(gòu)需求了。
就拿淘寶來說,淘寶在13年開啟的“All in 無線”戰(zhàn)略中,就將阿里系大多數(shù)業(yè)務(wù)都加入到手機淘寶中,使客戶端出現(xiàn)了業(yè)務(wù)的爆發(fā)。在這種情況下,單工程架構(gòu)則已經(jīng)遠遠不能滿足現(xiàn)有業(yè)務(wù)需求了。所以在這種情況下,淘寶在13年開啟了插件化架構(gòu)的重構(gòu),后來在14年迎來了手機淘寶有史以來最大規(guī)模的重構(gòu),將其徹底重構(gòu)為組件化架構(gòu)。
蘑菇街的組件化架構(gòu)
原因
在一個項目越來越大,開發(fā)人員越來越多的情況下,項目會遇到很多問題。
- 業(yè)務(wù)模塊間劃分不清晰,模塊之間耦合度很大,非常難維護。
- 所有模塊代碼都編寫在一個項目中,測試某個模塊或功能,需要編譯運行整個項目。
?
耦合嚴(yán)重的工程
為了解決上面的問題,可以考慮加一個中間層來協(xié)調(diào)模塊間的調(diào)用,所有的模塊間的調(diào)用都會經(jīng)過中間層中轉(zhuǎn)。(注意看兩張圖的箭頭方向)
?
添加中間層
但是發(fā)現(xiàn)增加這個中間層后,耦合還是存在的。中間層對被調(diào)用模塊存在耦合,其他模塊也需要耦合中間層才能發(fā)起調(diào)用。這樣還是存在之前的相互耦合的問題,而且本質(zhì)上比之前更麻煩了。
大體結(jié)構(gòu)
所以應(yīng)該做的是,只讓其他模塊對中間層產(chǎn)生耦合關(guān)系,中間層不對其他模塊發(fā)生耦合。
對于這個問題,可以采用組件化的架構(gòu),將每個模塊作為一個組件。并且建立一個主項目,這個主項目負(fù)責(zé)集成所有組件。這樣帶來的好處是很多的:
- 業(yè)務(wù)劃分更佳清晰,新人接手更佳容易,可以按組件分配開發(fā)任務(wù)。
- 項目可維護性更強,提高開發(fā)效率。
- 更好排查問題,某個組件出現(xiàn)問題,直接對組件進行處理。
- 開發(fā)測試過程中,可以只編譯自己那部分代碼,不需要編譯整個項目代碼。
?
組件化結(jié)構(gòu)
進行組件化開發(fā)后,可以把每個組件當(dāng)做一個獨立的app,每個組件甚至可以采取不同的架構(gòu),例如分別使用MVVM、MVC、MVCS等架構(gòu)。
MGJRouter方案
蘑菇街通過MGJRouter實現(xiàn)中間層,通過MGJRouter進行組件間的消息轉(zhuǎn)發(fā),從名字上來說更像是路由器。實現(xiàn)方式大致是,在提供服務(wù)的組件中提前注冊block,然后在調(diào)用方組件中通過URL調(diào)用block,下面是調(diào)用方式。
架構(gòu)設(shè)計
?
MGJRouter組件化架構(gòu)
MGJRouter是一個單例對象,在其內(nèi)部維護著一個“URL -> block”格式的注冊表,通過這個注冊表來保存服務(wù)方注冊的block,以及使調(diào)用方可以通過URL映射出block,并通過MGJRouter對服務(wù)方發(fā)起調(diào)用。
在服務(wù)方組件中都對外提供一個接口類,在接口類內(nèi)部實現(xiàn)block的注冊工作,以及block對外提供服務(wù)的代碼實現(xiàn)。每一個block都對應(yīng)著一個URL,調(diào)用方可以通過URL對block發(fā)起調(diào)用。
在程序開始運行時,需要將所有服務(wù)方的接口類實例化,以完成這個注冊工作,使MGJRouter中所有服務(wù)方的block可以正常提供服務(wù)。在這個服務(wù)注冊完成后,就可以被調(diào)用方調(diào)起并提供服務(wù)。
蘑菇街項目使用git作為版本控制工具,將每個組件都當(dāng)做一個獨立工程,并建立主項目來集成所有組件。集成方式是在主項目中通過CocoaPods來集成,將所有組件當(dāng)做二方庫集成到項目中。詳細(xì)的集成技術(shù)點在下面“標(biāo)準(zhǔn)組件化架構(gòu)設(shè)計”章節(jié)中會講到。
MGJRouter調(diào)用
代碼模擬對詳情頁的注冊、調(diào)用,在調(diào)用過程中傳遞id參數(shù)。下面是注冊的示例代碼:
?
?
| 1 2 3 4 | [MGJRouter?registerURLPattern:@"mgj://detail?id=id"?toHandler:^(NSDictionary *routerParameters)?{ ?????// 下面可以在拿到參數(shù)后,為其他組件提供對應(yīng)的服務(wù) ?????NSString?uid?=?routerParameters[@"id"]; }]; |
通過openURL:方法傳入的URL參數(shù),對詳情頁已經(jīng)注冊的block方法發(fā)起調(diào)用。調(diào)用方式類似于GET請求,URL地址后面拼接參數(shù)。
?
?
| 1 | [MGJRouter?openURL:@"mgj://detail?id=404"]; |
也可以通過字典方式傳參,MGJRouter提供了帶有字典參數(shù)的方法,這樣就可以傳遞非字符串之外的其他類型參數(shù)。
?
?
| 1 | [MGJRouter?openURL:@"mgj://detail?"?withParam:@{@"id"?:?@"404"}]; |
?
組件間傳值
有的時候組件間調(diào)用過程中,需要服務(wù)方在完成調(diào)用后返回相應(yīng)的參數(shù)。蘑菇街提供了另外的方法,專門來完成這個操作。
?
?
| 1 2 3 | [MGJRouter?registerURLPattern:@"mgj://cart/ordercount"?toObjectHandler:^id(NSDictionary *routerParamters){ ?????return?@42; }]; |
通過下面的方式發(fā)起調(diào)用,并獲取服務(wù)方返回的返回值,要做的就是傳遞正確的URL和參數(shù)即可。
?
?
| 1 | NSNumber *orderCount?=?[MGJRouter?objectForURL:@"mgj://cart/ordercount"]; |
?
短鏈管理
這時候會發(fā)現(xiàn)一個問題,在蘑菇街組件化架構(gòu)中,存在了很多硬編碼的URL和參數(shù)。在代碼實現(xiàn)過程中URL編寫出錯會導(dǎo)致調(diào)用失敗,而且參數(shù)是一個字典類型,調(diào)用方不知道服務(wù)方需要哪些參數(shù),這些都是個問題。
對于這些數(shù)據(jù)的管理,蘑菇街開發(fā)了一個web頁面,這個web頁面統(tǒng)一來管理所有的URL和參數(shù),Android和iOS都使用這一套URL,可以保持統(tǒng)一性。
基礎(chǔ)組件
在項目中存在很多公共部分的東西,例如封裝的網(wǎng)絡(luò)請求、緩存、數(shù)據(jù)處理等功能,以及項目中所用到的資源文件。
蘑菇街將這些部分也當(dāng)做組件,劃分為基礎(chǔ)組件,位于業(yè)務(wù)組件下層。所有業(yè)務(wù)組件都使用同一個基礎(chǔ)組件,也可以保證公共部分的統(tǒng)一性。
Protocol方案
整體架構(gòu)
?
Protocol方案的中間件
為了解決MGJRouter方案中URL硬編碼,以及字典參數(shù)類型不明確等問題,蘑菇街在原有組件化方案的基礎(chǔ)上推出了Protocol方案。Protocol方案由兩部分組成,進行組件間通信的ModuleManager類以及MGJComponentProtocol協(xié)議類。
通過中間件ModuleManager進行消息的調(diào)用轉(zhuǎn)發(fā),在ModuleManager內(nèi)部維護一張映射表,映射表由之前的"URL -> block"變成"Protocol -> Class"。
在中間件中創(chuàng)建MGJComponentProtocol文件,服務(wù)方組件將可以用來調(diào)用的方法都定義在Protocol中,將所有服務(wù)方的Protocol都分別定義到MGJComponentProtocol文件中,如果協(xié)議比較多也可以分開幾個文件定義。這樣所有調(diào)用方依然是只依賴中間件,不需要依賴除中間件之外的其他組件。
Protocol方案中每個組件也需要一個“接口類”,此類負(fù)責(zé)實現(xiàn)當(dāng)前組件對應(yīng)的協(xié)議方法,也就是對外提供服務(wù)的實現(xiàn)。在程序開始運行時將自身的Class注冊到ModuleManager中,并將Protocol反射出字符串當(dāng)做key。這個注冊過程和MGJRouter是類似的,都需要提前注冊服務(wù)。
示例代碼
創(chuàng)建MGJUserImpl類當(dāng)做User模塊的服務(wù)類,并在MGJComponentProtocol.h中定義MGJUserProtocol協(xié)議,由MGJUserImpl類實現(xiàn)協(xié)議中定義的方法,完成對外提供服務(wù)的過程。下面是協(xié)議定義:
?
?
| 1 2 3 | @protocol?MGJUserProtocol -?(NSString *)getUserName; @end |
Class遵守協(xié)議并實現(xiàn)定義的方法,外界通過Protocol獲取的Class實例化為對象,調(diào)用服務(wù)方實現(xiàn)的協(xié)議方法。
ModuleManager的協(xié)議注冊方法,注冊時將Protocol反射為字符串當(dāng)做存儲的key,將實現(xiàn)協(xié)議的Class當(dāng)做值存儲。通過Protocol取Class的時候,就是通過Protocol從ModuleManager中將Class映射出來。
?
?
| 1 | [ModuleManager?registerClass:MGJUserImpl?forProtocol:@protocol(MGJUserProtocol)]; |
調(diào)用時通過Protocol從ModuleManager中映射出注冊的Class,將獲取到的Class實例化,并調(diào)用Class實現(xiàn)的協(xié)議方法完成服務(wù)調(diào)用。
?
?
| 1 2 3 | Class?cls?=?[[ModuleManager?sharedInstance]?classForProtocol:@protocol(MGJUserProtocol)]; id?userComponent?=?[[cls?alloc]?init]; NSString *userName?=?[userComponent?getUserName]; |
?
整體調(diào)用流程
蘑菇街是OpenURL和Protocol混用的方式,兩種實現(xiàn)的調(diào)用方式不同,但大體調(diào)用邏輯和實現(xiàn)思路類似,所以下面的調(diào)用流程二者差不多。在OpenURL不能滿足需求或調(diào)用不方便時,就可以通過Protocol的方式調(diào)用。
內(nèi)存管理
蘑菇街組件化方案有兩種,Protocol和MGJRouter的方式,但都需要進行register操作。Protocol注冊的是Class,MGJRouter注冊的是Block,注冊表是一個NSMutableDictionary類型的字典,而字典的擁有者又是一個單例對象,這樣會造成內(nèi)存的常駐。
下面是對兩種實現(xiàn)方式內(nèi)存消耗的分析:
- 首先說一下block實現(xiàn)方式可能導(dǎo)致的內(nèi)存問題,block如果使用不當(dāng),很容易造成循環(huán)引用的問題。
經(jīng)過暴力測試,證明并不會導(dǎo)致內(nèi)存問題。被保存在字典中是一個block對象,而block對象本身并不會占用多少內(nèi)存。在調(diào)用block后會對block體中的方法進行執(zhí)行,執(zhí)行完成后block體中的對象釋放。
而block自身的實現(xiàn)只是一個結(jié)構(gòu)體,也就相當(dāng)于字典中存放的是很多結(jié)構(gòu)體,所以內(nèi)存的占用并不是很大。 - 對于協(xié)議這種實現(xiàn)方式,和block內(nèi)存常駐方式差不多。只是將存儲的block對象換成Class對象,如果不是已經(jīng)實例化的對象,內(nèi)存占用還是比較小的。
casatwy組件化方案
整體架構(gòu)
casatwy組件化方案分為兩種調(diào)用方式,遠程調(diào)用和本地調(diào)用,對于兩個不同的調(diào)用方式分別對應(yīng)兩個接口。
- 遠程調(diào)用通過AppDelegate代理方法傳遞到當(dāng)前應(yīng)用后,調(diào)用遠程接口并在內(nèi)部做一些處理,處理完成后會在遠程接口內(nèi)部調(diào)用本地接口,以實現(xiàn)本地調(diào)用為遠程調(diào)用服務(wù)。
- 本地調(diào)用由performTarget:action:params:方法負(fù)責(zé),但調(diào)用方一般不直接調(diào)用performTarget:方法。CTMediator會對外提供明確參數(shù)和方法名的方法,在方法內(nèi)部調(diào)用performTarget:方法和參數(shù)的轉(zhuǎn)換。
?
casatwy提出的組件化架構(gòu)
架構(gòu)設(shè)計思路
casatwy是通過CTMediator類實現(xiàn)組件化的,在此類中對外提供明確參數(shù)類型的接口,接口內(nèi)部通過performTarget方法調(diào)用服務(wù)方組件的Target、Action。由于CTMediator類的調(diào)用是通過runtime主動發(fā)現(xiàn)服務(wù)的,所以服務(wù)方對此類是完全解耦的。
但如果CTMediator類對外提供的方法都放在此類中,將會對CTMediator造成極大的負(fù)擔(dān)和代碼量。解決方法就是對每個服務(wù)方組件創(chuàng)建一個CTMediator的Category,并將對服務(wù)方的performTarget調(diào)用放在對應(yīng)的Category中,這些Category都屬于CTMediator中間件,從而實現(xiàn)了感官上的接口分離。
?
casatwy組件化實現(xiàn)細(xì)節(jié)
對于服務(wù)方的組件來說,每個組件都提供一個或多個Target類,在Target類中聲明Action方法。Target類是當(dāng)前組件對外提供的一個“服務(wù)類”,Target將當(dāng)前組件中所有的服務(wù)都定義在里面,CTMediator通過runtime主動發(fā)現(xiàn)服務(wù)。
在Target中的所有Action方法,都只有一個字典參數(shù),所以可以傳遞的參數(shù)很靈活,這也是casatwy提出的去Model化的概念。在Action的方法實現(xiàn)中,對傳進來的字典參數(shù)進行解析,再調(diào)用組件內(nèi)部的類和方法。
架構(gòu)分析
casatwy為我們提供了一個Demo,通過這個Demo可以很好的理解casatwy的設(shè)計思路,下面按照我的理解講解一下這個Demo。
?
文件目錄
打開Demo后可以看到文件目錄非常清楚,在上圖中用藍框框出來的就是中間件部分,紅框框出來的就是業(yè)務(wù)組件部分。我對每個文件夾做了一個簡單的注釋,包含了其在架構(gòu)中的職責(zé)。
在CTMediator中定義遠程調(diào)用和本地調(diào)用的兩個方法,其他業(yè)務(wù)相關(guān)的調(diào)用由Category完成。
?
?
| 1 2 3 4 | // 遠程App調(diào)用入口 -?(id)performActionWithUrl:(NSURL *)url?completion:(void(^)(NSDictionary *info))completion; // 本地組件調(diào)用入口 -?(id)performTarget:(NSString *)targetName?action:(NSString *)actionName?params:(NSDictionary *)params; |
在CTMediator中定義的ModuleA的Category,對外提供了一個獲取控制器并跳轉(zhuǎn)的功能,下面是代碼實現(xiàn)。由于casatwy的方案中使用performTarget的方式進行調(diào)用,所以涉及到很多硬編碼字符串的問題,casatwy采取定義常量字符串來解決這個問題,這樣管理也更方便。
?
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #import "CTMediator+CTMediatorModuleAActions.h" ? NSString *?const?kCTMediatorTargetA?=?@"A"; NSString *?const?kCTMediatorActionNativFetchDetailViewController?=?@"nativeFetchDetailViewController"; ? @implementation?CTMediator?(CTMediatorModuleAActions) ? -?(UIViewController *)CTMediator_viewControllerForDetail?{ ????UIViewController *viewController?=?[self?performTarget:kCTMediatorTargetA ????????????????????????????????????????????????????action:kCTMediatorActionNativFetchDetailViewController ????????????????????????????????????????????????????params:@{@"key":@"value"}]; ????if?([viewController?isKindOfClass:[UIViewController?class]])?{ ????????// view controller 交付出去之后,可以由外界選擇是push還是present ????????return?viewController; ????}?else?{ ????????// 這里處理異常場景,具體如何處理取決于產(chǎn)品 ????????return?[[UIViewController?alloc]?init]; ????} } |
下面是ModuleA組件中提供的服務(wù),被定義在Target_A類中,這些服務(wù)可以被CTMediator通過runtime的方式調(diào)用,這個過程就叫做發(fā)現(xiàn)服務(wù)。
我們發(fā)現(xiàn),在這個方法中其實做了參數(shù)處理和內(nèi)部調(diào)用的功能,這樣就可以保證組件內(nèi)部的業(yè)務(wù)不受外部影響,對內(nèi)部業(yè)務(wù)沒有侵入性。
?
?
| 1 2 3 4 5 6 | -?(UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params?{ ????// 對傳過來的字典參數(shù)進行解析,并調(diào)用ModuleA內(nèi)部的代碼 ????DemoModuleADetailViewController *viewController?=?[[DemoModuleADetailViewController?alloc]?init]; ????viewController.valueLabel.text?=?params[@"key"]; ????return?viewController; } |
?
命名規(guī)范
在大型項目中代碼量比較大,需要避免命名沖突的問題。對于這個問題casatwy采取的是加前綴的方式,從casatwy的Demo中也可以看出,其組件ModuleA的Target命名為Target_A,被調(diào)用的Action命名為Action_nativeFetchDetailViewController:。
casatwy將類和方法的命名,都統(tǒng)一按照其功能做區(qū)分當(dāng)做前綴,這樣很好的將組件相關(guān)和組件內(nèi)部代碼進行了劃分。
標(biāo)準(zhǔn)組件化架構(gòu)設(shè)計
這個章節(jié)叫做“標(biāo)準(zhǔn)組件化架構(gòu)設(shè)計”,對于項目架構(gòu)來說并沒有絕對意義的標(biāo)準(zhǔn)之說。這里說到的“標(biāo)準(zhǔn)組件化架構(gòu)設(shè)計”只是因為采取這樣的方式的人比較多,且這種方式相比而言較合理。
在上面文章中提到了casatwy方案的CTMediator,蘑菇街方案的MGJRouter和ModuleManager,下面統(tǒng)稱為中間件。
整體架構(gòu)
組件化架構(gòu)中,首先有一個主工程,主工程負(fù)責(zé)集成所有組件。每個組件都是一個單獨的工程,創(chuàng)建不同的git私有倉庫來管理,每個組件都有對應(yīng)的開發(fā)人員負(fù)責(zé)開發(fā)。開發(fā)人員只需要關(guān)注與其相關(guān)組件的代碼,其他業(yè)務(wù)代碼和其無關(guān),來新人也好上手。
組件的劃分需要注意組件粒度,粒度根據(jù)業(yè)務(wù)可大可小。組件劃分后屬于業(yè)務(wù)組件,對于一些多個組件共同的東西,例如網(wǎng)絡(luò)、數(shù)據(jù)庫之類的,應(yīng)該劃分到單獨的組件或基礎(chǔ)組件中。對于圖片或配置表這樣的資源文件,應(yīng)該再單獨劃分一個資源組件,這樣避免資源的重復(fù)性。
服務(wù)方組件對外提供服務(wù),由中間件調(diào)用或發(fā)現(xiàn)服務(wù),服務(wù)對當(dāng)前組件無侵入性,只負(fù)責(zé)對傳遞過來的數(shù)據(jù)進行解析和組件內(nèi)調(diào)用的功能。需要被其他組件調(diào)用的組件都是服務(wù)方,服務(wù)方也可以調(diào)用其他組件的服務(wù)。
通過這樣的組件劃分,組件的開發(fā)進度不會受其他業(yè)務(wù)的影響,可以多個組件單獨的并行開發(fā)。組件間的通信都交給中間件來進行,需要通信的類只需要接觸中間件,而中間件不需要耦合其他組件,這就實現(xiàn)了組件間的解耦。中間件負(fù)責(zé)處理所有組件之間的調(diào)度,在所有組件之間起到控制核心的作用。
這套框架清晰的劃分了不同組件,從整體架構(gòu)上來約束開發(fā)人員進行組件化開發(fā),避免某個開發(fā)人員偷懶直接引用頭文件,產(chǎn)生組件間的耦合,破壞整體架構(gòu)。假設(shè)以后某個業(yè)務(wù)發(fā)生大的改變,需要對相關(guān)代碼進行重構(gòu),可以在單個組件進行重構(gòu)。組件化架構(gòu)降低了重構(gòu)的風(fēng)險,保證了代碼的健壯性。
組件集成
?
組件化架構(gòu)圖
每個組件都是一個單獨的工程,在組件開發(fā)完成后上傳到git倉庫。主工程通過Cocoapods集成各個組件,集成和更新組件時只需要pod update即可。這樣就是把每個組件當(dāng)做第三方來管理,管理起來非常方便。
Cocoapods可以控制每個組件的版本,例如在主項目中回滾某個組件到特定版本,就可以通過修改podfile文件實現(xiàn)。選擇Cocoapods主要因為其本身功能很強大,可以很方便的集成整個項目,也有利于代碼的復(fù)用。通過這種集成方式,可以很好的避免在傳統(tǒng)項目中代碼沖突的問題。
集成方式
對于組件化架構(gòu)的集成方式,我在看完bang的博客后專門請教了一下bang。根據(jù)在微博上和bang的聊天以及其他博客中的學(xué)習(xí),在主項目中集成組件主要分為兩種方式——源碼和framework,但都是通過CocoaPods來集成。
無論是用CocoaPods管理源碼,還是直接管理framework,效果都是一樣的,都是可以直接進行pod update之類的操作的。
這兩種組件集成方案,實踐中也是各有利弊。直接在主工程中集成代碼文件,可以在主工程中進行調(diào)試。集成framework的方式,可以加快編譯速度,而且對每個組件的代碼有很好的保密性。如果公司對代碼安全比較看重,可以考慮framework的形式,但framework不利于主工程中的調(diào)試。
例如手機QQ或者支付寶這樣的大型程序,一般都會采取framework的形式。而且一般這樣的大公司,都會有自己的組件庫,這個組件庫往往可以代表一個大的功能或業(yè)務(wù)組件,直接添加項目中就可以使用。關(guān)于組件化庫在后面講淘寶組件化架構(gòu)的時候會提到。
不推薦的集成方式
之前有些項目是直接用workspace的方式集成的,或者直接在原有項目中建立子項目,直接做文件引用。但這兩點都是不建議做的,因為沒有真正意義上實現(xiàn)業(yè)務(wù)組件的剝離,只是像之前的項目一樣從文件目錄結(jié)構(gòu)上進行了劃分。
組件化開發(fā)總結(jié)
對于項目架構(gòu)來說,一定要建立于業(yè)務(wù)之上來設(shè)計架構(gòu)。不同的項目業(yè)務(wù)不同,組件化方案的設(shè)計也會不同,應(yīng)該設(shè)計最適合公司業(yè)務(wù)的架構(gòu)。
架構(gòu)對比
在除蘑菇街Protocol方案外,其他兩種方案都或多或少的存在硬編碼問題,硬編碼如果量比較大的話挺麻煩的。
在casatwy的CTMediator方案中需要硬編碼Target、Action字符串,只不過這個缺陷被封閉在中間件里面了,將這些字符串都統(tǒng)一定義為常量,外界使用不需要接觸到硬編碼。蘑菇街的MGJRouter的方案也是一樣的,也有硬編碼URL的問題,蘑菇街可能也做了類似的處理。
casatwy和蘑菇街提出的兩套組件化方案,大體結(jié)構(gòu)是類似的,三套方案都分為調(diào)用方、中間件、服務(wù)方,只是在具體實現(xiàn)過程中有些不同。例如Protocol方案在中間件中加入了Protocol文件,casatwy的方案在中間件中加入了Category。
三種方案內(nèi)部都有容錯處理,所以三種方案的穩(wěn)定性都是比較好的,而且都可以拿出來單獨運行,在服務(wù)方不存在的情況下也不會有問題。
在三套方案中,服務(wù)方都對外提供一個供外界調(diào)用的接口類,這個類中實現(xiàn)組件對外提供的服務(wù),中間件通過接口類來實現(xiàn)組件間的通信。在此類中統(tǒng)一定義對外提供的服務(wù),外界調(diào)用時就知道服務(wù)方可以做什么。
調(diào)用流程也不大一樣,蘑菇街的兩套方案都需要注冊操作,無論是Block還是Protocol都需要注冊后才可以提供服務(wù)。而casatwy的方案則不需要,直接通過runtime調(diào)用。casatwy的方案實現(xiàn)了真正的對服務(wù)方解耦,而蘑菇街的兩套方案則沒有,對服務(wù)方和調(diào)用方都造成了耦合。
我認(rèn)為三套方案中,Protocol方案是調(diào)用和維護最麻煩的一套方案。維護時需要同時維護Protocol、接口類兩部分。而且調(diào)用時需要將服務(wù)方的接口類返回給調(diào)用方,并由調(diào)用方執(zhí)行一系列調(diào)用邏輯,調(diào)用一個服務(wù)的邏輯非常復(fù)雜,這在開發(fā)中是非常影響開發(fā)效率的。
總結(jié)
下面是組件化開發(fā)中的一個小總結(jié),也是開發(fā)過程中的一些注意點。
- 在MGJRouter方案中,是通過調(diào)用OpenURL:方法并傳入URL來發(fā)起調(diào)用。鑒于URL協(xié)議名等固定格式,可以通過判斷協(xié)議名的方式,使用配置表控制H5和native的切換,配置表可以從后臺更新,只需要將協(xié)議名更改一下即可。
mgj://detail?id=123456
http://www.mogujie.com/detail?id=123456
假設(shè)現(xiàn)在線上的native組件出現(xiàn)嚴(yán)重bug,在后臺將配置文件中原有的本地URL換成H5的URL,并更新客戶端配置文件。在調(diào)用MGJRouter時傳入這個H5的URL即可完成切換,MGJRouter判斷如果傳進來的是一個H5的URL就直接跳轉(zhuǎn)webView。而且URL可以傳遞參數(shù)給MGJRouter,只需要MGJRouter內(nèi)部做參數(shù)截取即可。
- casatwy方案和蘑菇街Protocol方案,都提供了傳遞明確類型參數(shù)的方法。在MGJRouter方案中,傳遞參數(shù)主要是通過類似GET請求一樣在URL后面拼接參數(shù),和在字典中傳遞參數(shù)兩種方式組成。這兩種方式會造成傳遞參數(shù)類型不明確,傳遞參數(shù)類型受限(GET請求不能傳遞對象)等問題,后來使用Protocol方案彌補這個問題。
- 組件化開發(fā)可以很好的提升代碼復(fù)用性,組件可以直接拿到其他項目中使用,這個優(yōu)點在下面淘寶架構(gòu)中會著重講一下。
- 對于調(diào)試工作,應(yīng)該放在每個組件中完成。單獨的業(yè)務(wù)組件可以直接提交給測試提測,這樣測試起來也比較方便。最后組件開發(fā)完成并測試通過后,再將所有組件更新到主項目,提交給測試進行集成測試即可。
- 使用組件化架構(gòu)開發(fā),組件間的通信都是有成本的。所以盡量將業(yè)務(wù)封裝在組件內(nèi)部,對外只提供簡單的接口。即“高內(nèi)聚、低耦合”原則。
- 把握好劃分粒度的細(xì)化程度,太細(xì)則項目過于分散,太大則項目組件臃腫。但是項目都是從小到大的一個發(fā)展過程,所以不斷進行重構(gòu)是掌握這個組件的細(xì)化程度最好的方式。
我公司架構(gòu)
下面就簡單說說我公司項目架構(gòu),公司項目是一個地圖導(dǎo)航應(yīng)用,業(yè)務(wù)層之下的基礎(chǔ)組件占比較大。且基礎(chǔ)組件相對比較獨立,對外提供了很多調(diào)用接口。剛開始想的是采用MGJRouter的方案,但如果這些調(diào)用都通過Router進行,開發(fā)起來比較復(fù)雜,反而會適得其反。最主要我們項目也并不是非常大,沒必要都用Router轉(zhuǎn)發(fā)。
對于這個問題,公司項目的架構(gòu)設(shè)計是:層級架構(gòu)+組件化架構(gòu),組件化架構(gòu)處于層級架構(gòu)的最上層,也就是業(yè)務(wù)層。采取這種結(jié)構(gòu)混合的方式進行整體架構(gòu),這個對于公共組件的管理和層級劃分比較有利,符合公司業(yè)務(wù)需求。
?
公司組件化架構(gòu)
對于業(yè)務(wù)層級依然采用組件化架構(gòu)的設(shè)計,這樣可以充分利用組件化架構(gòu)的優(yōu)勢,對項目組件間進行解耦。在上層和下層的調(diào)用中,下層的功能組件應(yīng)該對外開放一個接口類,在接口類中聲明所有的服務(wù),實現(xiàn)上層調(diào)用當(dāng)前組件的一個中轉(zhuǎn),上層直接調(diào)用接口類。這樣做的好處在于,如果下層發(fā)生改變不會對上層造成影響,而且也省去了部分Router轉(zhuǎn)發(fā)的工作。
在設(shè)計層級架構(gòu)時,需要注意只能上層對下層依賴,下層對上層不能有依賴,下層中不要包含上層業(yè)務(wù)邏輯。對于項目中存在的公共資源和代碼,應(yīng)該將其下沉到下層中。
為什么這么做?
首先就像我剛才說的,我公司項目并不是很大,根本沒必要拆分的那么徹底。
因為組件化開發(fā)有一個很重要的原因就是解耦合,如果我做到了底層不對上層依賴,這樣就已經(jīng)解除了上下層的相互耦合。而且上層對下層進行調(diào)用的時候,也不是直接調(diào)用下層,通過一個接口類進行中轉(zhuǎn),實現(xiàn)了下層的改變對上層無影響,這也是上層對下層解耦的表現(xiàn)。
所以對于第三方就不用說了,上層直接調(diào)用下層的第三方也是沒問題的,這都是解耦的。
模型類怎么辦,放在哪合適?
casatwy對模型類的觀點是去Model化,簡單來說就是用字典代替Model存儲數(shù)據(jù)。這對于組件化架構(gòu)來說,是解決組件之間數(shù)據(jù)傳遞的一個很好的方法。
因為模型類是關(guān)乎業(yè)務(wù)的,理論上必須放在業(yè)務(wù)層也就是業(yè)務(wù)組件這一層。但是要把模型對象從一個組件中當(dāng)做參數(shù)傳遞到另一個組件中,模型類放在調(diào)用方和服務(wù)方的哪個組件都不太合適,而且有可能不只兩個組件使用到這個模型對象。這樣的話在其他組件使用模型對象,必然會造成引用和耦合。
那么如果把模型類放在Router中,這樣會造成Router耦合了業(yè)務(wù),造成業(yè)務(wù)的侵入性。如果在用到這個模型對象的所有組件中,都分別維護一份相同的模型類,這樣之后業(yè)務(wù)發(fā)生改變模型類就會很麻煩。
那應(yīng)該怎么辦呢?
如果將模型類單獨拉出來,定義一個模型組件呢?這個看起來比較可行,將這個定義模型的組件下沉到下層,模型組件不包含業(yè)務(wù),只聲明模型對象的類。但是一般組件的模型對象都是當(dāng)前組件內(nèi)使用的,將模型對象傳遞給其他組件的需求非常少,那所有的模型類都定義到模型組件嗎?
對于這個問題,我建議在項目開發(fā)中將模型類還定義在當(dāng)前業(yè)務(wù)組件中,在組件間傳遞模型對象時進行去Model化,傳遞字典類型的參數(shù)。
上面只是思考,恰巧我公司持久化方案用的是CoreData,所有模型的定義都在CoreData組件中,這樣就避免了業(yè)務(wù)層組件之間因為模型類的耦合。
滴滴組件化架構(gòu)
之前看過滴滴iOS負(fù)責(zé)人李賢輝的技術(shù)分享,分享的是滴滴iOS客戶端的架構(gòu)發(fā)展歷程,下面簡單總結(jié)一下。
發(fā)展歷程
滴滴在最開始的時候架構(gòu)較混亂。然后在2.0時期重構(gòu)為MVC架構(gòu),使項目劃分更加清晰。在3.0時期上線了新的業(yè)務(wù)線,這時采用的游戲開發(fā)中的狀態(tài)機機制,暫時可以滿足現(xiàn)有業(yè)務(wù)。
然而在后期不斷上線順風(fēng)車、代駕、巴士等多條業(yè)務(wù)線的情況下,現(xiàn)有架構(gòu)變得非常臃腫,代碼耦合嚴(yán)重。從而在2015年開始了代號為“The One”的方案,這套方案就是滴滴的組件化方案。
架構(gòu)設(shè)計
滴滴的組件化方案,和蘑菇街方案類似,也是通過私有CocoaPods來管理各個組件。將整個項目拆分為業(yè)務(wù)部分和技術(shù)部分,業(yè)務(wù)部分包括專車、拼車、巴士等業(yè)務(wù)模塊,每個業(yè)務(wù)模塊就是一個單獨的組件,使用一個pods管理。技術(shù)部分則分為登錄分享、網(wǎng)絡(luò)、緩存這樣的一些基礎(chǔ)組件,分別使用不同的pods管理。
組件間通信通過ONERouter中間件進行通信,ONERouter類似于MGJRouter,擔(dān)負(fù)起協(xié)調(diào)和調(diào)用各個組件的作用。組件間通信通過OpenURL方法,來進行對應(yīng)的調(diào)用。ONERouter內(nèi)部保存一份Class-URL的映射表,通過URL找到Class并發(fā)起調(diào)用,Class的注冊放在+load方法中進行。
滴滴在組件內(nèi)部的業(yè)務(wù)模塊中,模塊內(nèi)部使用MVVM+MVCS混合架構(gòu),兩種架構(gòu)都是MVC的衍生版本。其中MVCS中的Store負(fù)責(zé)數(shù)據(jù)相關(guān)邏輯,例如訂單狀態(tài)、地址管理等數(shù)據(jù)處理。通過MVVM中的VM給控制器瘦身,最后Controller的代碼量就很少了。
滴滴首頁分析
滴滴文章中說道首頁只能有一個地圖實例,這在很多地圖導(dǎo)航相關(guān)應(yīng)用中都是這樣做的。滴滴首頁主控制器持有導(dǎo)航欄和地圖,每個業(yè)務(wù)線首頁控制器都添加在主控制器上,并且業(yè)務(wù)線控制器背景都設(shè)置為透明,將透明部分響應(yīng)事件傳遞到下面的地圖中,只響應(yīng)屬于自己的響應(yīng)事件。
由主控制器來切換各個業(yè)務(wù)線首頁,切換頁面后根據(jù)不同的業(yè)務(wù)線來更新地圖數(shù)據(jù)。
淘寶組件化架構(gòu)
本章節(jié)源自于宗心在阿里技術(shù)沙龍上的一次分享
架構(gòu)發(fā)展
淘寶iOS客戶端初期是單工程的普通項目,但隨著業(yè)務(wù)的飛速發(fā)展,現(xiàn)有架構(gòu)并不能承載越來越多的業(yè)務(wù)需求,導(dǎo)致代碼間耦合很嚴(yán)重。后期開發(fā)團隊對其不斷進行重構(gòu),淘寶iOS和Android兩個平臺,除了某個平臺特有的一些特性或某些方案不便實施之外,大體架構(gòu)都是差不多的。
發(fā)展歷程:
淘寶開始實行插件化架構(gòu),將每個業(yè)務(wù)模塊劃分為一個組件,將組件以framework二方庫的形式集成到主工程。但這種方式并沒有做到真正的拆分,還是在一個工程中使用git進行merge,這樣還會造成合并沖突、不好回退等問題。
架構(gòu)優(yōu)勢
淘寶是使用git來做源碼管理的,在插件化架構(gòu)時需要盡可能避免merge操作,否則在大團隊中協(xié)作成本是很大的。而使用CocoaPods進行組件化開發(fā),則避免了這個問題。
在CocoaPods中可以通過podfile很好的配置各個組件,包括組件的增加和刪除,以及控制某個組件的版本。使用CocoaPods的原因,很大程度是為了解決大型項目中,代碼管理工具merge代碼導(dǎo)致的沖突。并且可以通過配置podfile文件,輕松配置項目。
每個組件工程有兩個target,一個負(fù)責(zé)編譯當(dāng)前組件和運行調(diào)試,另一個負(fù)責(zé)打包framework。先在組件工程做測試,測試完成后再集成到主工程中集成測試。
每個組件都是一個獨立app,可以獨立開發(fā)、測試,使得業(yè)務(wù)組件更加獨立,所有組件可以并行開發(fā)。下層為上層提供能滿足需求的底層庫,保證上層業(yè)務(wù)層可以正常開發(fā),并將底層庫封裝成framework集成到項目中。
使用CocoaPods進行組件集成的好處在于,在集成測試自己組件時,可以直接將本地主工程podfile文件中的當(dāng)前組件指向本地,就可以直接進行集成測試,不需要提交到服務(wù)器倉庫。
淘寶四層架構(gòu)
?
淘寶四層架構(gòu)(圖片來自淘寶技術(shù)分享)
淘寶架構(gòu)的核心思想是一切皆組件,將工程中所有代碼都抽象為組件。
淘寶架構(gòu)主要分為四層,最上層是組件Bundle(業(yè)務(wù)組件),依次往下是容器(核心層),中間件Bundle(功能封裝),基礎(chǔ)庫Bundle(底層庫)。容器層為整個架構(gòu)的核心,負(fù)責(zé)組件間的調(diào)度和消息派發(fā)。
總線設(shè)計
總線設(shè)計:URL路由+服務(wù)+消息。統(tǒng)一所有組件的通信標(biāo)準(zhǔn),各個業(yè)務(wù)間通過總線進行通信。
?
總線設(shè)計(圖片來自淘寶技術(shù)分享)
URL可以請求也可以接受返回值,和MGJRouter差不多。URL路由請求可以被解析就直接拿來使用,如果不能被解析就跳轉(zhuǎn)H5頁面。這樣就完成了一個對不存在組件調(diào)用的兼容,使用戶手中比較老的版本依然可以顯示新的組件。
服務(wù)提供一些公共服務(wù),由服務(wù)方組件負(fù)責(zé)實現(xiàn),通過Protocol實現(xiàn)。消息負(fù)責(zé)統(tǒng)一發(fā)送消息,類似于通知也需要注冊。
Bundle App
?
Bundle App(圖片來自淘寶技術(shù)分享)
淘寶提出Bundle App的概念,可以通過已有組件,進行簡單配置后就可以組成一個新的app出來。解決了多個應(yīng)用業(yè)務(wù)復(fù)用的問題,防止重復(fù)開發(fā)同一業(yè)務(wù)或功能。
Bundle即App,容器即OS,所有Bundle App被集成到OS上,使每個組件的開發(fā)就像app開發(fā)一樣簡單。這樣就做到了從巨型app回歸普通app的輕盈,使大型項目的開發(fā)問題徹底得到了解決。
總結(jié)
留個小思考
到目前為止組件化架構(gòu)文章就寫完了,文章確實挺長的,看到這里真是辛苦你了?。下面留個小思考,把下面字符串復(fù)制到微信輸入框隨便發(fā)給一個好友,然后點擊下面鏈接大概也能猜到微信的組件化方案。
?
?
| 1 | weixin://dl/profile |
?
總結(jié)
各位可以來我博客評論區(qū)討論,可以討論文中提到的技術(shù)細(xì)節(jié),也可以討論自己公司架構(gòu)所遇到的問題,或自己獨到的見解等等。無論是不是架構(gòu)師或新入行的iOS開發(fā),歡迎各位以一個討論技術(shù)的心態(tài)來討論。在評論區(qū)你的問題可以被其他人看到,這樣可能會給其他人帶來一些啟發(fā)。
本人博客地址
現(xiàn)在H5技術(shù)比較火,好多應(yīng)用都用H5來完成一些頁面的開發(fā),H5的跨平臺和實時更新等是非常大的優(yōu)點,但其性能和交互也是缺點。如果以后客戶端能夠發(fā)展到可以動態(tài)部署線上代碼,不用打包上線應(yīng)用市場,直接就可以做到原生應(yīng)用更新,這樣就可以解決原生應(yīng)用最大的痛點。這段時間公司項目比較忙,有時間我打算研究一下這個技術(shù)點?。
Demo地址:蘑菇街和casatwy組件化方案,其Github上都給出了Demo,這里就貼出其Github地址了。
蘑菇街-MGJRouter
casatwy-CTMediator
轉(zhuǎn)載于:https://my.oschina.net/HeroOneHY/blog/1335202
與50位技術(shù)專家面對面20年技術(shù)見證,附贈技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的IOS-组件化架构漫谈的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 负载均衡之总结篇
- 下一篇: 如何打造一个小而精的电商网站架构?