天天看點

IOS-元件化架構漫談

本文作者: 伯樂線上 - 劉小壯 。未經作者許可,禁止轉載!

歡迎加入伯樂線上 專欄作者。

前段時間公司項目打算重構,準确來說應該是按之前的産品邏輯重寫一個項目?。在重構項目之前涉及到架構選型的問題,我群組裡小夥伴一起研究了一下元件化架構,打算将項目重構為元件化架構。當然不是直接拿來照搬,還是要根據公司具體的業務需求設計架構。

在學習元件化架構的過程中,從很多高品質的部落格中學到不少東西,例如蘑菇街李忠、casatwy、bang的部落格。在學習過程中也遇到一些問題,在微網誌和QQ上和一些做

iOS

的朋友進行了交流,非常感謝這些朋友的幫助。

本篇文章主要針對于之前蘑菇街提出的元件化方案,以及casatwy提出的元件化方案進行分析,後面還會簡單提到滴滴、淘寶、微信的元件化架構,最後會簡單說一下我公司設計的元件化架構。

元件化架構的由來

随着移動網際網路的不斷發展,很多程式代碼量和業務越來越多,現有架構已經不适合公司業務的發展速度了,很多都面臨着重構的問題。

在公司項目開發中,如果項目比較小,普通的

單工程+MVC架構

就可以滿足大多數需求了。但是像淘寶、蘑菇街、微信這樣的大型項目,原有的單工程架構就不足以滿足架構需求了。

就拿淘寶來說,淘寶在13年開啟的

“All in 無線”

戰略中,就将阿裡系大多數業務都加入到手機淘寶中,使用戶端出現了業務的爆發。在這種情況下,單工程架構則已經遠遠不能滿足現有業務需求了。是以在這種情況下,淘寶在13年開啟了插件化架構的重構,後來在14年迎來了手機淘寶有史以來最大規模的重構,将其徹底重構為元件化架構。

蘑菇街的元件化架構

原因

在一個項目越來越大,開發人員越來越多的情況下,項目會遇到很多問題。

  • 業務子產品間劃分不清晰,子產品之間耦合度很大,非常難維護。
  • 所有子產品代碼都編寫在一個項目中,測試某個子產品或功能,需要編譯運作整個項目。
IOS-元件化架構漫談

耦合嚴重的工程

為了解決上面的問題,可以考慮加一個中間層來協調子產品間的調用,所有的子產品間的調用都會經過中間層中轉。(注意看兩張圖的箭頭方向)

IOS-元件化架構漫談

添加中間層

但是發現增加這個中間層後,耦合還是存在的。中間層對被調用子產品存在耦合,其他子產品也需要耦合中間層才能發起調用。這樣還是存在之前的互相耦合的問題,而且本質上比之前更麻煩了。

大體結構

是以應該做的是,隻讓其他子產品對中間層産生耦合關系,中間層不對其他子產品發生耦合。

對于這個問題,可以采用元件化的架構,将每個子產品作為一個元件。并且建立一個主項目,這個主項目負責內建所有元件。這樣帶來的好處是很多的:

  • 業務劃分更佳清晰,新人接手更佳容易,可以按元件配置設定開發任務。
  • 項目可維護性更強,提高開發效率。
  • 更好排查問題,某個元件出現問題,直接對元件進行處理。
  • 開發測試過程中,可以隻編譯自己那部分代碼,不需要編譯整個項目代碼。
IOS-元件化架構漫談

元件化結構

進行元件化開發後,可以把每個元件當做一個獨立的app,每個元件甚至可以采取不同的架構,例如分别使用

MVVM

MVC

MVCS

等架構。

MGJRouter方案

蘑菇街通過

MGJRouter

實作中間層,通過

MGJRouter

進行元件間的消息轉發,從名字上來說更像是路由器。實作方式大緻是,在提供服務的元件中提前注冊

block

,然後在調用方元件中通過

URL

調用

block

,下面是調用方式。

架構設計

IOS-元件化架構漫談

MGJRouter元件化架構

MGJRouter

是一個單例對象,在其内部維護着一個

“URL -> block”

格式的系統資料庫,通過這個系統資料庫來儲存服務方注冊的

block

,以及使調用方可以通過

URL

映射出

block

,并通過

MGJRouter

對服務方發起調用。

在服務方元件中都對外提供一個接口類,在接口類内部實作

block

的注冊工作,以及

block

對外提供服務的代碼實作。每一個

block

都對應着一個

URL

,調用方可以通過

URL

block

發起調用。

在程式開始運作時,需要将所有服務方的接口類執行個體化,以完成這個注冊工作,使

MGJRouter

中所有服務方的

block

可以正常提供服務。在這個服務注冊完成後,就可以被調用方調起并提供服務。

蘑菇街項目使用

git

作為版本控制工具,将每個元件都當做一個獨立工程,并建立主項目來內建所有元件。內建方式是在主項目中通過

CocoaPods

來內建,将所有元件當做二方庫內建到項目中。詳細的內建技術點在下面

“标準元件化架構設計”

章節中會講到。

MGJRouter調用

代碼模拟對詳情頁的注冊、調用,在調用過程中傳遞

id

參數。下面是注冊的示例代碼:

1 2 3 4 [MGJRouter registerURLPattern:@"mgj://detail?id=id" toHandler:^(NSDictionary *routerParameters) {      // 下面可以在拿到參數後,為其他元件提供對應的服務      NSString uid = routerParameters[@"id"]; }];

通過

openURL:

方法傳入的

URL

參數,對詳情頁已經注冊的

block

方法發起調用。調用方式類似于

GET

請求,

URL

位址後面拼接參數。

1 [MGJRouter openURL:@"mgj://detail?id=404"];

也可以通過字典方式傳參,

MGJRouter

提供了帶有字典參數的方法,這樣就可以傳遞非字元串之外的其他類型參數。

1 [MGJRouter openURL:@"mgj://detail?" withParam:@{@"id" : @"404"}];

元件間傳值

有的時候元件間調用過程中,需要服務方在完成調用後傳回相應的參數。蘑菇街提供了另外的方法,專門來完成這個操作。

1 2 3 [MGJRouter registerURLPattern:@"mgj://cart/ordercount" toObjectHandler:^id(NSDictionary *routerParamters){      return @42; }];

通過下面的方式發起調用,并擷取服務方傳回的傳回值,要做的就是傳遞正确的

URL

和參數即可。

1 NSNumber *orderCount = [MGJRouter objectForURL:@"mgj://cart/ordercount"];

短鍊管理

這時候會發現一個問題,在蘑菇街元件化架構中,存在了很多寫死的URL和參數。在代碼實作過程中

URL

編寫出錯會導緻調用失敗,而且參數是一個字典類型,調用方不知道服務方需要哪些參數,這些都是個問題。

對于這些資料的管理,蘑菇街開發了一個

web

頁面,這個

web

頁面統一來管理所有的

URL

和參數,

Android

iOS

都使用這一套

URL

,可以保持統一性。

基礎元件

在項目中存在很多公共部分的東西,例如封裝的網絡請求、緩存、資料處理等功能,以及項目中所用到的資源檔案。

蘑菇街将這些部分也當做元件,劃分為基礎元件,位于業務元件下層。所有業務元件都使用同一個基礎元件,也可以保證公共部分的統一性。

Protocol方案

整體架構

IOS-元件化架構漫談

Protocol方案的中間件

為了解決

MGJRouter

方案中

URL

寫死,以及字典參數類型不明确等問題,蘑菇街在原有元件化方案的基礎上推出了

Protocol

方案。

Protocol

方案由兩部分組成,進行元件間通信的

ModuleManager

類以及

MGJComponentProtocol

協定類。

通過中間件

ModuleManager

進行消息的調用轉發,在

ModuleManager

内部維護一張映射表,映射表由之前的

"URL -> block"

變成

"Protocol -> Class"

在中間件中建立

MGJComponentProtocol

檔案,服務方元件将可以用來調用的方法都定義在

Protocol

中,将所有服務方的

Protocol

都分别定義到

MGJComponentProtocol

檔案中,如果協定比較多也可以分開幾個檔案定義。這樣所有調用方依然是隻依賴中間件,不需要依賴除中間件之外的其他元件。

Protocol

方案中每個元件也需要一個“接口類”,此類負責實作目前元件對應的協定方法,也就是對外提供服務的實作。在程式開始運作時将自身的

Class

注冊到

ModuleManager

中,并将

Protocol

反射出字元串當做

key

。這個注冊過程和

MGJRouter

是類似的,都需要提前注冊服務。

示例代碼

建立

MGJUserImpl

類當做

User

子產品的服務類,并在

MGJComponentProtocol.h

中定義

MGJUserProtocol

協定,由

MGJUserImpl

類實作協定中定義的方法,完成對外提供服務的過程。下面是協定定義:

1 2 3 @protocol MGJUserProtocol - (NSString *)getUserName; @end

Class

遵守協定并實作定義的方法,外界通過

Protocol

擷取的

Class

執行個體化為對象,調用服務方實作的協定方法。

ModuleManager

的協定注冊方法,注冊時将

Protocol

反射為字元串當做存儲的

key

,将實作協定的

Class

當做值存儲。通過

Protocol

Class

的時候,就是通過

Protocol

ModuleManager

中将

Class

映射出來。

1 [ModuleManager registerClass:MGJUserImpl forProtocol:@protocol(MGJUserProtocol)];

調用時通過

Protocol

ModuleManager

中映射出注冊的

Class

,将擷取到的

Class

執行個體化,并調用

Class

實作的協定方法完成服務調用。

1 2 3 Class cls = [[ModuleManager sharedInstance] classForProtocol:@protocol(MGJUserProtocol)]; id userComponent = [[cls alloc] init]; NSString *userName = [userComponent getUserName];

整體調用流程

蘑菇街是

OpenURL

Protocol

混用的方式,兩種實作的調用方式不同,但大體調用邏輯和實作思路類似,是以下面的調用流程二者差不多。在

OpenURL

不能滿足需求或調用不友善時,就可以通過

Protocol

的方式調用。

  1. 在進入程式後,先使用

    MGJRouter

    對服務方元件進行注冊。每個

    URL

    對應一個

    block

    的實作,

    block

    中的代碼就是服務方對外提供的服務,調用方可以通過

    URL

    調用這個服務。
  2. 調用方通過

    MGJRouter

    調用

    openURL:

    方法,并将被調用代碼對應的

    URL

    傳入,

    MGJRouter

    會根據

    URL

    查找對應的

    block

    實作,進而調用服務方元件的代碼進行通信。
  3. 調用和注冊

    block

    時,

    block

    有一個字典用來傳遞參數。這樣的優勢就是參數類型和數量理論上是不受限制的,但是需要很多寫死的

    key

    名在項目中。

記憶體管理

蘑菇街元件化方案有兩種,

Protocol

MGJRouter

的方式,但都需要進行

register

操作。

Protocol

注冊的是

Class

MGJRouter

注冊的是

Block

,系統資料庫是一個

NSMutableDictionary

類型的字典,而字典的擁有者又是一個單例對象,這樣會造成記憶體的常駐。

下面是對兩種實作方式記憶體消耗的分析:

  • 首先說一下

    block

    實作方式可能導緻的記憶體問題,

    block

    如果使用不當,很容易造成循環引用的問題。

    經過暴力測試,證明并不會導緻記憶體問題。被儲存在字典中是一個

    block

    對象,而

    block

    對象本身并不會占用多少記憶體。在調用

    block

    後會對

    block

    體中的方法進行執行,執行完成後

    block

    體中的對象釋放。

    block

    自身的實作隻是一個結構體,也就相當于字典中存放的是很多結構體,是以記憶體的占用并不是很大。
  • 對于協定這種實作方式,和

    block

    記憶體常駐方式差不多。隻是将存儲的

    block

    對象換成

    Class

    對象,如果不是已經執行個體化的對象,記憶體占用還是比較小的。

casatwy元件化方案

整體架構

casatwy元件化方案分為兩種調用方式,遠端調用和本地調用,對于兩個不同的調用方式分别對應兩個接口。

  • 遠端調用通過

    AppDelegate

    代理方法傳遞到目前應用後,調用遠端接口并在内部做一些處理,處理完成後會在遠端接口内部調用本地接口,以實作本地調用為遠端調用服務。
  • 本地調用由

    performTarget:action:params:

    方法負責,但調用方一般不直接調用

    performTarget:

    方法。

    CTMediator

    會對外提供明确參數和方法名的方法,在方法内部調用

    performTarget:

    方法和參數的轉換。
IOS-元件化架構漫談

casatwy提出的元件化架構

架構設計思路

casatwy是通過

CTMediator

類實作元件化的,在此類中對外提供明确參數類型的接口,接口内部通過

performTarget

方法調用服務方元件的

Target

Action

。由于

CTMediator

類的調用是通過

runtime

主動發現服務的,是以服務方對此類是完全解耦的。

但如果

CTMediator

類對外提供的方法都放在此類中,将會對

CTMediator

造成極大的負擔和代碼量。解決方法就是對每個服務方元件建立一個

CTMediator

Category

,并将對服務方的

performTarget

調用放在對應的

Category

中,這些

Category

都屬于

CTMediator

中間件,進而實作了感官上的接口分離。

IOS-元件化架構漫談

casatwy元件化實作細節

對于服務方的元件來說,每個元件都提供一個或多個

Target

類,在

Target

類中聲明

Action

方法。

Target

類是目前元件對外提供的一個“服務類”,

Target

将目前元件中所有的服務都定義在裡面,

CTMediator

通過

runtime

主動發現服務。

Target

中的所有

Action

方法,都隻有一個字典參數,是以可以傳遞的參數很靈活,這也是casatwy提出的去

Model

化的概念。在

Action

的方法實作中,對傳進來的字典參數進行解析,再調用元件内部的類和方法。

架構分析

casatwy為我們提供了一個Demo,通過這個

Demo

可以很好的了解casatwy的設計思路,下面按照我的了解講解一下這個

Demo

IOS-元件化架構漫談

檔案目錄

打開

Demo

後可以看到檔案目錄非常清楚,在上圖中用藍框框出來的就是中間件部分,紅框框出來的就是業務元件部分。我對每個檔案夾做了一個簡單的注釋,包含了其在架構中的職責。

CTMediator

中定義遠端調用和本地調用的兩個方法,其他業務相關的調用由

Category

完成。

1 2 3 4 // 遠端App調用入口 - (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion; // 本地元件調用入口 - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params;

CTMediator

中定義的

ModuleA

Category

,對外提供了一個擷取控制器并跳轉的功能,下面是代碼實作。由于casatwy的方案中使用

performTarget

的方式進行調用,是以涉及到很多寫死字元串的問題,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 {         // 這裡處理異常場景,具體如何處理取決于産品         return [[UIViewController alloc] init];     } }

下面是

ModuleA

元件中提供的服務,被定義在

Target_A

類中,這些服務可以被

CTMediator

通過

runtime

的方式調用,這個過程就叫做發現服務。

我們發現,在這個方法中其實做了參數處理和内部調用的功能,這樣就可以保證元件内部的業務不受外部影響,對内部業務沒有侵入性。

1 2 3 4 5 6 - (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params {     // 對傳過來的字典參數進行解析,并調用ModuleA内部的代碼     DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];     viewController.valueLabel.text = params[@"key"];     return viewController; }

命名規範

在大型項目中代碼量比較大,需要避免命名沖突的問題。對于這個問題casatwy采取的是加字首的方式,從casatwy的

Demo

中也可以看出,其元件

ModuleA

Target

命名為

Target_A

,被調用的

Action

命名為

Action_nativeFetchDetailViewController:

casatwy将類和方法的命名,都統一按照其功能做區分當做字首,這樣很好的将元件相關群組件内部代碼進行了劃分。

标準元件化架構設計

這個章節叫做“标準元件化架構設計”,對于項目架構來說并沒有絕對意義的标準之說。這裡說到的“标準元件化架構設計”隻是因為采取這樣的方式的人比較多,且這種方式相比而言較合理。

在上面文章中提到了casatwy方案的

CTMediator

,蘑菇街方案的

MGJRouter

ModuleManager

,下面統稱為中間件。

整體架構

元件化架構中,首先有一個主工程,主工程負責內建所有元件。每個元件都是一個單獨的工程,建立不同的

git

私有倉庫來管理,每個元件都有對應的開發人員負責開發。開發人員隻需要關注與其相關元件的代碼,其他業務代碼和其無關,來新人也好上手。

元件的劃分需要注意元件粒度,粒度根據業務可大可小。元件劃分後屬于業務元件,對于一些多個元件共同的東西,例如網絡、資料庫之類的,應該劃分到單獨的元件或基礎元件中。對于圖檔或配置表這樣的資源檔案,應該再單獨劃分一個資源元件,這樣避免資源的重複性。

服務方元件對外提供服務,由中間件調用或發現服務,服務對目前元件無侵入性,隻負責對傳遞過來的資料進行解析群組件内調用的功能。需要被其他元件調用的元件都是服務方,服務方也可以調用其他元件的服務。

通過這樣的元件劃分,元件的開發進度不會受其他業務的影響,可以多個元件單獨的并行開發。元件間的通信都交給中間件來進行,需要通信的類隻需要接觸中間件,而中間件不需要耦合其他元件,這就實作了元件間的解耦。中間件負責處理所有元件之間的排程,在所有元件之間起到控制核心的作用。

這套架構清晰的劃分了不同元件,從整體架構上來限制開發人員進行元件化開發,避免某個開發人員偷懶直接引用頭檔案,産生元件間的耦合,破壞整體架構。假設以後某個業務發生大的改變,需要對相關代碼進行重構,可以在單個元件進行重構。元件化架構降低了重構的風險,保證了代碼的健壯性。

元件內建

IOS-元件化架構漫談

元件化架構圖

每個元件都是一個單獨的工程,在元件開發完成後上傳到

git

倉庫。主工程通過

Cocoapods

內建各個元件,內建和更新元件時隻需要

pod update

即可。這樣就是把每個元件當做第三方來管理,管理起來非常友善。

Cocoapods

可以控制每個元件的版本,例如在主項目中復原某個元件到特定版本,就可以通過修改

podfile

檔案實作。選擇

Cocoapods

主要因為其本身功能很強大,可以很友善的內建整個項目,也有利于代碼的複用。通過這種內建方式,可以很好的避免在傳統項目中代碼沖突的問題。

內建方式

對于元件化架構的內建方式,我在看完bang的部落格後專門請教了一下bang。根據在微網誌上和bang的聊天以及其他部落格中的學習,在主項目中內建元件主要分為兩種方式——源碼和

framework

,但都是通過

CocoaPods

來內建。

無論是用

CocoaPods

管理源碼,還是直接管理

framework

,效果都是一樣的,都是可以直接進行

pod update

之類的操作的。

這兩種元件內建方案,實踐中也是各有利弊。直接在主工程中內建代碼檔案,可以在主工程中進行調試。內建

framework

的方式,可以加快編譯速度,而且對每個元件的代碼有很好的保密性。如果公司對代碼安全比較看重,可以考慮

framework

的形式,但

framework

不利于主工程中的調試。

例如手機QQ或者支付寶這樣的大型程式,一般都會采取

framework

的形式。而且一般這樣的大公司,都會有自己的元件庫,這個元件庫往往可以代表一個大的功能或業務元件,直接添加項目中就可以使用。關于元件化庫在後面講淘寶元件化架構的時候會提到。

不推薦的內建方式

之前有些項目是直接用

workspace

的方式內建的,或者直接在原有項目中建立子項目,直接做檔案引用。但這兩點都是不建議做的,因為沒有真正意義上實作業務元件的剝離,隻是像之前的項目一樣從檔案目錄結構上進行了劃分。

元件化開發總結

對于項目架構來說,一定要建立于業務之上來設計架構。不同的項目業務不同,元件化方案的設計也會不同,應該設計最适合公司業務的架構。

架構對比

在除蘑菇街

Protocol

方案外,其他兩種方案都或多或少的存在寫死問題,寫死如果量比較大的話挺麻煩的。

在casatwy的

CTMediator

方案中需要寫死

Target

Action

字元串,隻不過這個缺陷被封閉在中間件裡面了,将這些字元串都統一定義為常量,外界使用不需要接觸到寫死。蘑菇街的

MGJRouter

的方案也是一樣的,也有寫死

URL

的問題,蘑菇街可能也做了類似的處理。

casatwy和蘑菇街提出的兩套元件化方案,大體結構是類似的,三套方案都分為調用方、中間件、服務方,隻是在具體實作過程中有些不同。例如

Protocol

方案在中間件中加入了

Protocol

檔案,casatwy的方案在中間件中加入了

Category

三種方案内部都有容錯處理,是以三種方案的穩定性都是比較好的,而且都可以拿出來單獨運作,在服務方不存在的情況下也不會有問題。

在三套方案中,服務方都對外提供一個供外界調用的接口類,這個類中實作元件對外提供的服務,中間件通過接口類來實作元件間的通信。在此類中統一定義對外提供的服務,外界調用時就知道服務方可以做什麼。

調用流程也不大一樣,蘑菇街的兩套方案都需要注冊操作,無論是

Block

還是

Protocol

都需要注冊後才可以提供服務。而casatwy的方案則不需要,直接通過

runtime

調用。casatwy的方案實作了真正的對服務方解耦,而蘑菇街的兩套方案則沒有,對服務方和調用方都造成了耦合。

我認為三套方案中,

Protocol

方案是調用和維護最麻煩的一套方案。維護時需要同時維護

Protocol

、接口類兩部分。而且調用時需要将服務方的接口類傳回給調用方,并由調用方執行一系列調用邏輯,調用一個服務的邏輯非常複雜,這在開發中是非常影響開發效率的。

總結

下面是元件化開發中的一個小總結,也是開發過程中的一些注意點。

  • MGJRouter

    方案中,是通過調用

    OpenURL:

    方法并傳入

    URL

    來發起調用。鑒于

    URL

    協定名等固定格式,可以通過判斷協定名的方式,使用配置表控制

    H5

    native

    的切換,配置表可以從背景更新,隻需要将協定名更改一下即可。

mgj://detail?id=123456

http://www.mogujie.com/detail?id=123456

假設現線上上的

native

元件出現嚴重

bug

,在背景将配置檔案中原有的本地

URL

換成

H5

URL

,并更新用戶端配置檔案。在調用

MGJRouter

時傳入這個

H5

URL

即可完成切換,

MGJRouter

判斷如果傳進來的是一個

H5

URL

就直接跳轉

webView

。而且

URL

可以傳遞參數給

MGJRouter

,隻需要

MGJRouter

内部做參數截取即可。

  • casatwy方案和蘑菇街

    Protocol

    方案,都提供了傳遞明确類型參數的方法。在

    MGJRouter

    方案中,傳遞參數主要是通過類似

    GET

    請求一樣在

    URL

    後面拼接參數,和在字典中傳遞參數兩種方式組成。這兩種方式會造成傳遞參數類型不明确,傳遞參數類型受限(

    GET

    請求不能傳遞對象)等問題,後來使用

    Protocol

    方案彌補這個問題。
  • 元件化開發可以很好的提升代碼複用性,元件可以直接拿到其他項目中使用,這個優點在下面淘寶架構中會着重講一下。
  • 對于調試工作,應該放在每個元件中完成。單獨的業務元件可以直接送出給測試提測,這樣測試起來也比較友善。最後元件開發完成并測試通過後,再将所有元件更新到主項目,送出給測試進行內建測試即可。
  • 使用元件化架構開發,元件間的通信都是有成本的。是以盡量将業務封裝在元件内部,對外隻提供簡單的接口。即“高内聚、低耦合”原則。
  • 把握好劃分粒度的細化程度,太細則項目過于分散,太大則項目元件臃腫。但是項目都是從小到大的一個發展過程,是以不斷進行重構是掌握這個元件的細化程度最好的方式。

我公司架構

下面就簡單說說我公司項目架構,公司項目是一個地圖導航應用,業務層之下的基礎元件占比較大。且基礎元件相對比較獨立,對外提供了很多調用接口。剛開始想的是采用

MGJRouter

的方案,但如果這些調用都通過

Router

進行,開發起來比較複雜,反而會适得其反。最主要我們項目也并不是非常大,沒必要都用

Router

轉發。

對于這個問題,公司項目的架構設計是:層級架構+元件化架構,元件化架構處于層級架構的最上層,也就是業務層。采取這種結構混合的方式進行整體架構,這個對于公共元件的管理和層級劃分比較有利,符合公司業務需求。

IOS-元件化架構漫談

公司元件化架構

對于業務層級依然采用元件化架構的設計,這樣可以充分利用元件化架構的優勢,對項目元件間進行解耦。在上層和下層的調用中,下層的功能元件應該對外開放一個接口類,在接口類中聲明所有的服務,實作上層調用目前元件的一個中轉,上層直接調用接口類。這樣做的好處在于,如果下層發生改變不會對上層造成影響,而且也省去了部分

Router

轉發的工作。

在設計層級架構時,需要注意隻能上層對下層依賴,下層對上層不能有依賴,下層中不要包含上層業務邏輯。對于項目中存在的公共資源和代碼,應該将其下沉到下層中。

為什麼這麼做?

首先就像我剛才說的,我公司項目并不是很大,根本沒必要拆分的那麼徹底。

因為元件化開發有一個很重要的原因就是解耦合,如果我做到了底層不對上層依賴,這樣就已經解除了上下層的互相耦合。而且上層對下層進行調用的時候,也不是直接調用下層,通過一個接口類進行中轉,實作了下層的改變對上層無影響,這也是上層對下層解耦的表現。

是以對于第三方就不用說了,上層直接調用下層的第三方也是沒問題的,這都是解耦的。

模型類怎麼辦,放在哪合适?

casatwy對模型類的觀點是去Model化,簡單來說就是用字典代替

Model

存儲資料。這對于元件化架構來說,是解決元件之間資料傳遞的一個很好的方法。

因為模型類是關乎業務的,理論上必須放在業務層也就是業務元件這一層。但是要把模型對象從一個元件中當做參數傳遞到另一個元件中,模型類放在調用方和服務方的哪個元件都不太合适,而且有可能不隻兩個元件使用到這個模型對象。這樣的話在其他元件使用模型對象,必然會造成引用和耦合。

那麼如果把模型類放在

Router

中,這樣會造成

Router

耦合了業務,造成業務的侵入性。如果在用到這個模型對象的所有元件中,都分别維護一份相同的模型類,這樣之後業務發生改變模型類就會很麻煩。

那應該怎麼辦呢?

如果将模型類單獨拉出來,定義一個模型元件呢?這個看起來比較可行,将這個定義模型的元件下沉到下層,模型元件不包含業務,隻聲明模型對象的類。但是一般元件的模型對象都是目前元件内使用的,将模型對象傳遞給其他元件的需求非常少,那所有的模型類都定義到模型元件嗎?

對于這個問題,我建議在項目開發中将模型類還定義在目前業務元件中,在元件間傳遞模型對象時進行去Model化,傳遞字典類型的參數。

上面隻是思考,恰巧我公司持久化方案用的是

CoreData

,所有模型的定義都在

CoreData

元件中,這樣就避免了業務層元件之間因為模型類的耦合。

滴滴元件化架構

之前看過滴滴

iOS

負責人李賢輝的技術分享,分享的是滴滴

iOS

用戶端的架構發展曆程,下面簡單總結一下。

發展曆程

滴滴在最開始的時候架構較混亂。然後在2.0時期重構為

MVC

架構,使項目劃分更加清晰。在3.0時期上線了新的業務線,這時采用的遊戲開發中的狀态機機制,暫時可以滿足現有業務。

然而在後期不斷上線順風車、代駕、巴士等多條業務線的情況下,現有架構變得非常臃腫,代碼耦合嚴重。進而在2015年開始了代号為

“The One”

的方案,這套方案就是滴滴的元件化方案。

架構設計

滴滴的元件化方案,和蘑菇街方案類似,也是通過私有

CocoaPods

來管理各個元件。将整個項目拆分為業務部分和技術部分,業務部分包括專車、拼車、巴士等業務子產品,每個業務子產品就是一個單獨的元件,使用一個

pods

管理。技術部分則分為登入分享、網絡、緩存這樣的一些基礎元件,分别使用不同的

pods

管理。

元件間通信通過

ONERouter

中間件進行通信,

ONERouter

類似于

MGJRouter

,擔負起協調和調用各個元件的作用。元件間通信通過

OpenURL

方法,來進行對應的調用。

ONERouter

内部儲存一份

Class-URL

的映射表,通過

URL

找到

Class

并發起調用,

Class

的注冊放在

+load

方法中進行。

滴滴在元件内部的業務子產品中,子產品内部使用

MVVM+MVCS

混合架構,兩種架構都是

MVC

的衍生版本。其中

MVCS

中的

Store

負責資料相關邏輯,例如訂單狀态、位址管理等資料處理。通過

MVVM

中的

VM

給控制器瘦身,最後

Controller

的代碼量就很少了。

滴滴首頁分析

滴滴文章中說道首頁隻能有一個地圖執行個體,這在很多地圖導航相關應用中都是這樣做的。滴滴首頁主要制器持有導航欄和地圖,每個業務線首頁控制器都添加在主要制器上,并且業務線控制器背景都設定為透明,将透明部分響應事件傳遞到下面的地圖中,隻響應屬于自己的響應事件。

由主要制器來切換各個業務線首頁,切換頁面後根據不同的業務線來更新地圖資料。

淘寶元件化架構

本章節源自于宗心在阿裡技術沙龍上的一次分享

架構發展

淘寶

iOS

用戶端初期是單工程的普通項目,但随着業務的飛速發展,現有架構并不能承載越來越多的業務需求,導緻代碼間耦合很嚴重。後期開發團隊對其不斷進行重構,淘寶

iOS

Android

兩個平台,除了某個平台特有的一些特性或某些方案不便實施之外,大體架構都是差不多的。

發展曆程:

  1. 剛開始是普通的單工程項目,以傳統的

    MVC

    架構進行開發。随着業務不斷的增加,導緻項目非常臃腫、耦合嚴重。
  2. 2013年淘寶開啟“all in 無線”計劃,計劃将淘寶變為一個大的平台,将阿裡系大多數業務都內建到這個平台上,造成了業務的大爆發。

    淘寶開始實行插件化架構,将每個業務子產品劃分為一個元件,将元件以

    framework

    二方庫的形式內建到主工程。但這種方式并沒有做到真正的拆分,還是在一個工程中使用

    git

    進行

    merge

    ,這樣還會造成合并沖突、不好回退等問題。
  3. 迎來淘寶移動端有史以來最大的重構,将其重構為元件化架構。将每個子產品當做一個元件,每個元件都是一個單獨的項目,并且将元件打包成

    framework

    。主工程通過

    podfile

    內建所有元件

    framework

    ,實作業務之間真正的隔離,通過

    CocoaPods

    實作元件化架構。

架構優勢

淘寶是使用

git

來做源碼管理的,在插件化架構時需要盡可能避免

merge

操作,否則在大團隊中協作成本是很大的。而使用

CocoaPods

進行元件化開發,則避免了這個問題。

CocoaPods

中可以通過

podfile

很好的配置各個元件,包括元件的增加和删除,以及控制某個元件的版本。使用

CocoaPods

的原因,很大程度是為了解決大型項目中,代碼管理工具

merge

代碼導緻的沖突。并且可以通過配置

podfile

檔案,輕松配置項目。

每個元件工程有兩個

target

,一個負責編譯目前元件和運作調試,另一個負責打包

framework

。先在元件工程做測試,測試完成後再內建到主工程中內建測試。

每個元件都是一個獨立

app

,可以獨立開發、測試,使得業務元件更加獨立,所有元件可以并行開發。下層為上層提供能滿足需求的底層庫,保證上層業務層可以正常開發,并将底層庫封裝成

framework

內建到項目中。

使用

CocoaPods

進行元件內建的好處在于,在內建測試自己元件時,可以直接将本地主工程

podfile

檔案中的目前元件指向本地,就可以直接進行內建測試,不需要送出到伺服器倉庫。

淘寶四層架構

IOS-元件化架構漫談

淘寶四層架構(圖檔來自淘寶技術分享)

淘寶架構的核心思想是一切皆元件,将工程中所有代碼都抽象為元件。

淘寶架構主要分為四層,最上層是元件

Bundle

(業務元件),依次往下是容器(核心層),中間件

Bundle

(功能封裝),基礎庫

Bundle

(底層庫)。容器層為整個架構的核心,負責元件間的排程和消息派發。

總線設計

總線設計:

URL

路由+服務+消息。統一所有元件的通信标準,各個業務間通過總線進行通信。

IOS-元件化架構漫談

總線設計(圖檔來自淘寶技術分享)

URL

可以請求也可以接受傳回值,和

MGJRouter

差不多。

URL

路由請求可以被解析就直接拿來使用,如果不能被解析就跳轉

H5

頁面。這樣就完成了一個對不存在元件調用的相容,使使用者手中比較老的版本依然可以顯示新的元件。

服務提供一些公共服務,由服務方元件負責實作,通過

Protocol

實作。消息負責統一發送消息,類似于通知也需要注冊。

Bundle App

IOS-元件化架構漫談

Bundle App(圖檔來自淘寶技術分享)

淘寶提出

Bundle App

的概念,可以通過已有元件,進行簡單配置後就可以組成一個新的

app

出來。解決了多個應用業務複用的問題,防止重複開發同一業務或功能。

Bundle

App

,容器即

OS

,所有

Bundle App

被內建到

OS

上,使每個元件的開發就像

app

開發一樣簡單。這樣就做到了從巨型

app

回歸普通

app

的輕盈,使大型項目的開發問題徹底得到了解決。

總結

留個小思考

到目前為止元件化架構文章就寫完了,文章确實挺長的,看到這裡真是辛苦你了?。下面留個小思考,把下面字元串複制到微信輸入框随便發給一個好友,然後點選下面連結大概也能猜到微信的元件化方案。

1 weixin://dl/profile
總結

各位可以來我部落格評論區讨論,可以讨論文中提到的技術細節,也可以讨論自己公司架構所遇到的問題,或自己獨到的見解等等。無論是不是架構師或新入行的iOS開發,歡迎各位以一個讨論技術的心态來讨論。在評論區你的問題可以被其他人看到,這樣可能會給其他人帶來一些啟發。

本人部落格位址

現在

H5

技術比較火,好多應用都用

H5

來完成一些頁面的開發,

H5

的跨平台和實時更新等是非常大的優點,但其性能和互動也是缺點。如果以後用戶端能夠發展到可以動态部署線上代碼,不用打包上線應用市場,直接就可以做到原生應用更新,這樣就可以解決原生應用最大的痛點。這段時間公司項目比較忙,有時間我打算研究一下這個技術點?。

Demo

位址:蘑菇街和

casatwy

元件化方案,其

Github

上都給出了

Demo

,這裡就貼出其

Github

位址了。

蘑菇街-MGJRouter

casatwy-CTMediator