天天看點

iOS元件化開發方案

将一個工程分解成各個元件,然後按照某種方式任意組織成為一個擁有完整業務邏輯的工程。

大緻讨論元件化的三種方案:url-block、protocol-class(和 url-controller 類似)、target-action,以及應用這三種元件化方案的時機、步驟、利弊等。

一、為什麼需要元件化

随着公司業務的不斷發展,項目的功能越來越複雜,各個業務代碼耦合越來越多,代碼量急劇增加,傳統的 MVC 或者 MVVM 架構已經無法高效的管理工程代碼,是以需要用一種技術來更好地管理工程,而元件化是一種能夠解決代碼耦合的技術。項目經過元件化的拆分,不僅可以解決代碼耦合的問題,還可以增強代碼的複用性,工程的易管理性等。

二、什麼時候做元件開發

  • 項目管理:項目的業務線超過 2 條以上,需要獨立拆分。随着業務的拆分,對應的業務元件也就很自然的獨立出來。
  • 人員管理:過多人對同一塊代碼的進行修改,産生 bug 的可能性上升,是以需要重新配置設定人員和其維護的功能。
  • 測試次元:随着項目的業務量增大,很難做單元測試。每個小功能修改,都需要對 App 進行測試,嚴重增加測試工作量。

當 App 業務之間交叉耦合,bug 率難以下降,測試每天做大量重複工作。開發人員之間修改互相影響時,你需要考慮進行元件化。

元件化是項目架構層面的技術,不是所有項目都适合元件化,元件化一般針對的是大中型的項目,并且是多人開發。如果,項目比較小,開發人員比較少,确實不太适合元件化,因為這時的元件化可能帶來的不是便捷,而是增加了開發的工作量。另外,元件化過程也要考慮團隊的情況,總之,根據目前項目的情況作出最合适的技術選型。

三、元件化的過程

3.1 url-block#

這是蘑菇街中使用的一種頁面間調用的方式,通過在啟動時注冊元件提供的服務,把調用元件使用的 url 群組件提供的服務 block 對應起來,儲存到記憶體中。在使用元件的服務時,通過 url 找到對應的 block,然後擷取服務。

url-block 的架構圖:

iOS元件化開發方案

注冊:

[MGJRouter registerURLPattern:@"mgj://detail?id=:id" 
                    toHandler:^(NSDictionary * routerParameters) {
    NSNumber *id = routerParameters[@"id"];
    // create view controller with id. push view controller
}];
           

調用:

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

蘑菇街為了統一 iOS 和 Android 的平台差異性,專門用背景來管理 url,然後針對不同的平台生成不同類型的檔案。

使用 url-block 的方案的确可以元件間解耦,但是還是存在其它明顯的問題,比如:

  1. 需要在記憶體中維護 url-block 的表,元件多了可能會有記憶體問題;
  2. url 的參數傳遞受到限制,隻能傳遞正常的字元串參數,無法傳遞非正常參數,如 UIImage、NSData 等類型;
  3. 沒有區分本地調用和遠端調用的情況,尤其是遠端調用會因為 url 參數受限,導緻一些功能受限;
  4. 元件本身依賴了中間件,且分散注冊使的耦合較多

3.2 protocol-class#

針對方案 1 的問題,蘑菇街又提出了另一種元件化的方案,就是通過 protocol 定義服務接口,元件通過實作該接口來提供接口定義的服務,具體實作就是把 protocol 和 class 做一個映射,同時在記憶體中儲存一張映射表,使用的時就通過 protocol 找到對應的 class 來擷取需要的服務。

protocol-class 的架構圖:

iOS元件化開發方案

注冊:

[ModuleManager registerClass:ClassA forProtocol:ProtocolA]
           

調用:

[ModuleManager classForProtocol:ProtocolA]
           

蘑菇街的這種方案确實解決了方案 1 中無法傳遞非正常參數的問題,使得元件間的調用更為友善,但是它依然沒有解決元件依賴中間件、記憶體中維護映射表、元件的分散調用的問題。設計思想和方案 1 類似,都是通過給元件加了一層 wrapper,然後給使用者調用。

3.3 url-controller#

這是 LDBusMediator 的元件化方案。它是通過元件實作公共協定的接口,來對外提供服務。

具體就是通過單例來維護 url-controller 的映射關系表,根據調用者的 url,以及提供的參數(字典類型,是以參數類型不受限制)來傳回對應的 controller 來提供服務;同時,為了增強元件提供服務的多樣性,又通過服務協定定義了其它的服務。整體來看,LDBusMediator 解決了蘑菇街的這兩種元件化方案的不足,比如:通過注冊封裝件 connector 而不是 block 來降低了記憶體占用;通過字典傳遞參數,解決了 url 參數的限制性。但是,由于使用了 connector 來提供服務而不是元件本身,把 connector 作為元件的一部分,依然有元件依賴中間件的問題。

LDBusMediator 的架構圖:

iOS元件化開發方案

3.4 target-action#

target-action 的方案是通過給元件包裝一層 wrapper 來給外界提供服務,然後調用者通過依賴中間件來使用服務;其中,中間件是通過 runtime 來調用元件的服務,是真正意義上的解耦,也是該方案最核心的地方。具體實施過程是給元件封裝一層 target 對象來對外提供服務,不會對原來元件造成入侵;然後,通過實作中間件的 category 來提供服務給調用者,這樣使用者隻需要依賴中間件,而元件則不需要依賴中間件。

target-action 的架構圖:

iOS元件化開發方案
- (UIViewController *)CTMediator_viewControllerForDetail
{
    return [self performTarget:kCTMediatorTargetA 
                        action:kCTMediatorActionNativFetchDetailViewController 
                        params:@{ @"key" : @"value" }
             shouldCacheTarget:NO];
}
           

但是 target-action 方案有個問題就是在中間件的 category 裡有 hardcode,casa 的解釋是在元件間調用時,最好是去 model 化,是以不可避免的引入了 hardcode,并且所有的 hardcode 隻存在于分類中。

針對這個問題,有人提議把所有的 model 做成元件化下沉,然後讓所有的元件都可以自由的通路 model。這種方案雖然解決了元件間傳遞 model 的依賴問題,但是為了解決這個問題,直接把整個 model 層元件化後暴露給所有元件,容易造成資料洩露,付出的代價有點大。

針對這個問題,經過和網友讨論,一緻覺得元件間調用時用字典傳遞資料,元件内調用時用 model 傳遞資料,這樣既減少元件間資料對 model 的耦合,又友善了元件内使用 model 傳遞資料的便捷性。

- (UIViewController *)CTMediator_viewControllerForDetail:(NSDictionary *)dict
{
    return [self performTarget:kCTMediatorTargetA                         
                        action:kCTMediatorActionNativFetchDetailViewController
                        params:dict
             shouldCacheTarget:NO];
}
           

hardCode

  1. 官方解釋:将可變變量用一個固定值來代替的方法。用這種方法編譯後,如果以後需要更改此變量就非常困難了。
  2. hard code 是指 “寫死”,即将資料直接寫在代碼中。也就是,在程式中直接給變量指派。指的是在軟體實作上,把輸出或輸入的相關參數(例如:路徑、輸出的形式、格式)直接寫死在源代碼中,而非在運作時期由外界指定的設定、資源、資料、或者格式做出适當回應。
  3. hard code 的雙重性:

    a. 直接将資料填寫在源代碼中,資料發生變化時,并不利于資料的修改,會造成程式的品質降低;

    b. 保護一些資料,直接指派,避免其發生變化。

四、元件化實施的方式

元件化可以利用 git 的源代碼管理工具的便利性來實施,具體就是建立一個項目工程的私有化倉庫,然後把各個元件的 podspec 上傳到私有倉庫,在需要用到元件時,直接從倉庫裡面取。

  1. 殼工程
    • main
    • AppDelegate
    • 工程配置
    • Debug 頁面
  2. 封裝公共庫和基礎 UI 庫

    在具體的項目開發過程中,我們常會用到三方庫和自己封裝的 UI 庫,我們可以把這些庫封裝成元件,然後在項目裡用 pod 進行管理。其中,針對三方庫,最好再封裝一層,使我們的項目部直接依賴三方庫,友善後續開發過程中的更換。

  3. 獨立業務子產品化

    在開發過程中,對一些獨立的子產品,如:登入子產品、賬戶子產品等等,也可以封裝成元件,因為這些元件是項目強依賴的,調用的頻次比較多。另外,在拆分元件化的過程中,拆分的粒度要合适,盡量做到元件的獨立性。同時,元件化是一個漸進的過程,不可能把一個完整的工程一下子全部元件化,要分步進行,通過不停的疊代,來最終實作項目的元件化。

  4. 服務接口最小化

    在前兩步都完成的情況下,我們可以根據元件被調用的需求來抽象出元件對外的最小化接口。這時,就可以選擇具體應用哪種元件化方案來實施元件化了。

    公共元件:

    • 埋點元件
    • Common 元件(聚合工具類)
    • 啟動元件
    • 性能監控元件
    • 定位元件
    • 圖檔處理元件
    • UIKit 封裝和擴充元件
    • 業務生命周期及通信元件
    網絡元件:
    • 基于 AFNetworking 進行封裝,提供 JSON 轉 Model、緩存功能
    • DNS 加速元件
    持久化元件
    • 基于 FMDB 進行封裝元件
    第三方業務元件
    • 分享元件
    • 推送元件
    基礎業務元件
    • User 元件,儲存使用者資訊,登陸,登出狀态

五、元件化實施的方式

元件化可以利用

git

的源代碼管理工具的便利性來實施,具體就是建立一個項目工程的私有化倉庫,然後把各個元件的

podspec

上傳到私有倉庫,在需要用到元件時,直接從倉庫裡面取。

1. 封裝公共庫和基礎 UI 庫

在具體的項目開發過程中,我們常會用到三方庫和自己封裝的

UI

庫,我們可以把這些庫封裝成元件,然後在項目裡用

pod

進行管理。其中,針對三方庫,最好再封裝一層,使我們的項目部直接依賴三方庫,友善後續開發過程中的更換。

2. 獨立業務子產品化

在開發過程中,對一些獨立的子產品,如:登入子產品、賬戶子產品等等,也可以封裝成元件,因為這些元件是項目強依賴的,調用的頻次比較多。另外,在拆分元件化的過程中,拆分的粒度要合适,盡量做到元件的獨立性。同時,元件化是一個漸進的過程,不可能把一個完整的工程一下子全部元件化,要分步進行,通過不停的疊代,來最終實作項目的元件化。

3. 服務接口最小化

在前兩步都完成的情況下,我們可以根據元件被調用的需求來抽象出元件對外的最小化接口。這時,就可以選擇具體應用哪種元件化方案來實施元件化了。

總結

元件化是項目架構層面的技術,不是所有項目都适合元件化,元件化一般針對的是大中型的項目,并且是多人開發。如果,項目比較小,開發人員比較少,确實不太适合元件化,因為這時的元件化可能帶來的不是便捷,而是增加了開發的工作量。另外,元件化過程也要考慮團隊的情況,總之,根據目前項目的情況作出最合适的技術選型。我一直尊崇,沒有最好的技術,隻有最合适的技術。