歡迎光臨
每天分享高質量文章

iOS組件化及架構設計

作者:朱大爺12138

鏈接:https://www.jianshu.com/p/d88aef8e29a4

關於組件化

網上組件化的文章很多。很多文章一提到組件化,就會說解耦,一說到解耦就會說路由或者runtime。好像組件化 == 解耦 == 路由/Runtime,然而這是一個非常錯誤的觀念。持有這一觀點的人,沒有搞清楚在組件化中什麼是想要結果,什麼是過程。

 

組件化和解耦

 

大家不妨先思考兩個問題:

 

1、為何要進行組件化開發?

2、各個組件之間是否一定需要解耦?

 

採用組件化,是為了組件能單獨開發,能單獨開發App就能快速集成,所以單獨開發是結果。要讓組件能單獨開發,組件必須職責單一,對於代碼中已有模塊,就需要用到重構和解耦的技術,所以重構和解耦是過程。那解耦是否是必須的過程?不一定。比如UIKit,我們用這個系統組件並沒有使用任何解耦手段。問題來了,UIKit蘋果可以獨立開發,我們使用它為什麼沒用解耦手段?答案很簡單,UIKit沒有依賴我們的代碼所以不用解耦。

 

PS:我這裡不糾結組件、服務、模塊、框架的概念,網上對這些概念的定義五花八門,實際上把簡單的事說複雜了。我這裡只關心一件事,這一部分代碼能否獨立開發,能就叫組件,不能我管你叫什麼。

 

我們之所以要解耦才能獨立開發,通常是出現了迴圈依賴。這時候當然可以無腦的用路由把兩個組件的耦合解開,也可以獨立開發。然而,這樣做只是把強取用改成了弱取用,代碼還是爛代碼。站在重構的角度來說,A、B組件迴圈依賴就是設計有問題,要麼應該重構A、B讓依賴單向;要麼應該抽離一個共用組件C,讓A、B組件都只依賴於C。

 

 

如果我們每個組件都只是單向依賴其他組件,各個組件之間也就沒有必要解耦。再換個角度說,如果一個組件職責不單一,即使跟其他組件解耦了,組件依然不能很好的工作。如何解耦只是重構過程中可選手段,代碼設計的原則如依賴倒置、接口隔離、里氏替換,都可以指導我們寫出好的組件。

 

所以在組件化中重要的是讓組件職責單一,職責單一的重要標誌之一就是沒有組件間的迴圈依賴。

 

架構圖

 

一般來講,App的組件可以分為三層,上層業務組件、中層UI組件、底層SDK組件

同一層之間的組件互相獨立,上層的組件耦合下層的組件。一般來講,底層SDK組件和中層UI組件都是獨立的功能,不會出現同層耦合。

 

架構圖

 

同層耦合一般出現在上層業務組件中,常見有兩類,一類是controller之間的跳轉,這個可以用路由解決;另一類是資料獲取,資料獲取需要通過重構抽離一個資料模塊,耦合這個資料模塊可以用協議,這樣可以用依賴註入的方式隔離實現。通過協議隔離的好處是,上層業務可以不依賴其他業務的實現單獨編譯通過,這對上層業務組件的單獨開發比較好。

 

這種方法把所有資料協議放在一起,簡單粗暴,但每一個資料協議改動都會導致資料協議層重新發佈,不是很合理,可以再進一步細分資料協議層。

 

業務組件耦個協議

業務組件耦合自己需要的協議

 

這種方法把資料協議層分成了多個,業務組件只依賴自己的資料協議。管理起來更麻煩一點,不過架構上更合理。

 

建議組件化前期用第一種方案,後期細化為第二種。

 

包管理

 

要解除迴圈依賴,引入包管理技術cocoapods會讓我們更有效率。pod不允許組件間有迴圈依賴,若有pod install時就會報錯。

 

cocoapods,提供私有pod repo,使用時把自己的組件放在私有pod repo里,然後在Podfile里直接通過pod命令集成。一個組件對應一個私有pod,每個組件依賴自己所需要的三方庫。多個組件聯合開發的時候,可以再一個podspec里配置子模塊,這樣在每個組件自己的podspec里,只需要把子模塊里的pod依賴關係拷貝過去就行了。

 

在多個組件集成時會有版本衝突的問題。比如登錄組件(L)、廣告組件(A)都依賴了埋點組件(O),L依賴O的1.1版本,A依賴O的1.2版本,這時候集成就會報錯。為瞭解決這個錯誤,在組件間依賴時,不寫版本號,版本號只在APP集成方寫。即podfile里取用所有組件,並寫上版本號,.podspec里不寫版本號。

 

這樣做既可以保證APP集成方的穩定性,也可以解決組件依賴的版本衝突問題。這樣做的壞處是,所有組件包括App集成方,在使用其他組件時,都必須使用其他組件最新的API,這會造成額外的升級工作量。如果不想接受組件升級最新api的成本,可以私有化一個三方庫自己維護。

 

組件開發完畢後告訴集成方,目前的組件穩定版本是多少,取用的三方庫穩定版本集成方自己去決定

 

推薦的組件版本號管理方式

 

另一種版本管理的方式,是在podspec里寫依賴組件的版本號,podfile里不寫組件依賴的版本,然後通過內部溝通來解決版本衝突的問題。我認為雖然也能做,但有很多弊端。

 

1、作為App集成方,沒辦法單獨控制依賴的三方庫版本。三方庫升級會更複雜

2、每個依賴的三方庫,都應該做了完整的單元測試,才能被集成到App中。所以正確的邏輯不是組件內測試過三方庫沒問題就在組件內寫死版本號,而是這個三方庫經過我們測試後,可以在我們系統中使用XX版本。

3、在工程中就沒有一個地方能完整知道所有的pod組件,而App集成方有權利知道這一點

4、溝通成本高

不推薦的方式

 

以上,就是組件化的所有東西。你可能會奇怪,解耦在組件化過程中有什麼用。答案是解耦是為了更好的實現組件的單一職責,解耦的作用在架構設計中談。需要再次強調,組件化 ≠ 解耦。

 

如果非要給組件化下一個定義,我的理解是:

 

組件化意味著重構,目的是讓每個組件職責單一

關於架構設計

在我看來,iOS客戶端架構主要為瞭解決兩個問題,一是解決大型專案分組件開發的效率的問題,二是解決單行程App的穩定性的問題。

 

設計到架構設計的都是大型App,小型App主要是業務的堆疊。很多公司在業務初期都不會考慮架構,在業務發展到一定規模的時候,才會重新審視架構混亂帶來的開發效率和業務穩定性瓶頸。這時候就會引入組件化的概念,我們常常面臨的是對已有專案的組件化,這一過程會異常困難。

 

組件拆分原則

 

對老工程的組件拆分,我的辦法是,從底層開始拆。SDK>  模塊 > 業務 。如果App沒有SDK可以抽離,就從模塊開始拆,不要為了抽離SDK而抽離。常見的誤區是,大家一拿到代碼就把公共函式提出來作為共用框架,起的名字還特別接地氣,如XXCommon。

 

事實上,這種框架型SDK,是最雞肋的組件,原因是它實用性很小,無非就是減少了點冗餘代碼。而且在架構能力不強的情況下,它很容易變成“垃圾堆”,什麼東西都想往裡面放,後面越來越龐大。所以,開始拆分架構的時候,儘量以業務優先,比如先拆分享模塊。

 

如果兩個組件中有共同的函式,前期不要想著提出來,改個名字讓它冗餘是更好的辦法。如果共同耦合的是一個靜態庫,可以利用動態庫的隔離性封裝靜態庫,具體方法可以網上找。

 

響應式

 

基礎組件常常要在系統啟動時初始化,或者接受App生命周期時間。這就引出了個問題,如何給appDelegate瘦身?比如我們現在有兩個基礎組件A、B,他們都需要監聽App生命周期事件,傳統的做法是,A、B兩個組件都提供一些函式在appDelegate中呼叫。但這樣做的壞處是,如果某一天我不想引入B組件了,還得去改appDelegate代碼。理想的方式是,基礎組件的使用不需要在appDelegate里寫代碼

 

為了實現基礎組件與appDelegate分離,得對appDelegate改造。首先得提出一個觀點,蘋果的appDelegate設計的有問題,它在用代理樣式解決觀察者樣式的問題。在《設計樣式》中,代理樣式的設計意圖定義是:為其他物件提供一種代理以控制對這個物件的訪問。反過來看appDelegate你會發現,它大部分代理函式都沒有辦法控制application,如applicationDidBecomeActive。applicationDidBecomeActive這種事件常常需要多個處理者,這種場景用觀察者樣式更適合。而openURL需要傳回BOOL值,才需要使用代理樣式。App生命周期事件雖然可以用監聽通知獲取,但用起來不如響應式監聽信號方便。

 

基於響應式編程的思想,我寫了一個TLAppEventBus,提供屬性來監聽生命周期事件。我並不喜歡龐大的ReactiveObjectC,所以我通過category實現了簡單的響應式,用戶只需要監聽需要的信號即可。在TLAppEventBus里,我預設提供了8個系統事件用來監聽,如果有其他的系統事件需要監聽,可以使用擴展的方法,給TLAppEventBus添加屬性(見文末Demo)。

 

路由

 

對於Appdelegate中的openURL的事件,蘋果使用代理樣式並沒有問題,但我們常常需要在openURL裡面寫if-else區分事件的處理者,這也會造成多個URL處理模塊耦合在Appdelegate中。我認為appdelegate中的openURL應該用路由轉發的方式來解耦。

 

openURL代理需要同步傳回處理結果,但網上開源的路由框架能同步傳回結果的。所以我這邊實現了一個能同步傳回結果的路由TLRouter,同時支持了註冊scheme。註冊scheme這一特性,在第三方分享的場景下會比較有用(見文末Demo)。

 

另外,網上大部分方案都搞錯了場景。以蘑菇街的路由方案為例(好像iOS的路由就是他們提出來的?),蘑菇街認為路由主要有兩個作用,一是發送資料讓路由接收者處理,二是傳回物件讓路由發送者繼續處理。我不禁想問,這是路由嗎?不妨先回到URL的定義

 

URL: 統一資源識別符號(Uniform Resource Locator,統一資源定位符)是一個用於標識某一互聯網資源名稱的字串

 

openURL就是在訪問資源,在瀏覽器中,openURL意味著打開一個網頁,openURL的發起者並不關心打開的內容是什麼,只關心打開的結果。所以蘋果的openURL Api 就只傳回了除了結果YES/NO,沒有傳回一個物件。所以,我對openURL這一行為定義如下

 

openURL:訪問資源,傳回是否訪問成功

 

那把蘑菇街的路由,傳回的物件改成BOOL值就可以了麽?我認為還不夠。對於客戶端的路由,使用的實際上是通知的形式在解耦,帶來的問題是路由的註冊代碼散落在各地,所以路由方案必須要配路由文件,要不然開發者會不知道路由在幹嘛。

 

有沒有比文件更好的方式呢?我的思路是:用schema區分路由職責

 

系統的openURL只幹了兩件事:打開App ,比如打開微信

 

[[UIApplicationsharedApplication] openURL:[NSURLURLWithString:@"weixin://"]];

 

還有就是打開網頁,比如

 

[[UIApplicationsharedApplication] openURL:[NSURLURLWithString:@"https://www.baidu.com"]];

 

兩者的共性是頁面切換。所以我這邊設計的路由openURL,只擴充了controller跳轉的功能,比如打開登錄頁

 

[TLRouter openURL:@"innerJump://account/login"];

 

只擴充了controller跳轉的功能好處是讓路由的職責更單一,同時也更符合蘋果對openURL的定義。工程師在看到url schema的時候就知道他的作用,避免反覆查看文件。

 

對於資料的傳遞,我認為不應該用路由的方式。相比路由,響應式會是更好的選擇。用響應式的思想,提供屬性給相關組件去監聽,代碼邏輯會更清晰。至於響應式會帶來耦合屬性這個問題,我在組件化的部分已經討論過。

 

依賴註入

 

依賴註入用於上層業務解耦中,有時多個業務需要用到同一個資料組件。這時可以使用IoC(Inversion of Control)的思想,讓上層業務依賴IoC容器,IoC容器創建資料組件,並註入到容器里。具體實現可以通過iOS的協議來實現IoC容器和物件的接口一致,註入物件的獲取通過runtime實體化。這樣做有個缺點是類名會寫死在IoC容器中,如果要動態配置,可以把類名放在plist中讓IoC容器去讀它。(見Demo TLUserModuleContainer類)

 

App配置

 

有時候我們需要組件的跨App復用,在App集成組件時,能夠不改代碼只改配置是最理想的方式。使用組件+plist配置是一個方案,具體做法是把A組件的配置放在A.plist中,在A組件內寫死要讀取A.plist。

 

以配置代替硬編碼,防止對代碼的侵入,是一個很好的思路。設想一下,如果我們可以通過配置在決定App是否使用組件、也可通過配置來改變組件和app所需的引數,那運維可以代替app開發來出包,這對效率和穩定性都會有提升。為了實現這一效果,我使用了OC的runtime來動態註冊組件。需要在didfinishLaunch初始化的組件,可以實現代理 – (void)initializeWhenLaunch; 這樣,自動初始化函式,就可以通過runtime+plist里配置的class name自動初始化。組件需要初始化的代碼,可以在自己的initializeWhenLaunch里做。

 

由於路由只擴充了controller跳轉的功能,所以路由註冊這一行為也可進行一次抽象,把不同的部分放在plist配置檔案,相同的放到runtime里做。這樣做還有個好處是,程式內的路由跳轉在一個plist里可以都可以看到

 

appdelegate改造後示例

iOS解耦工具Tourelle

Tourelle,是根據上面的思路寫的一個開源專案 https://github.com/zhudaye12138/Tourelle,可以通過pod集成  pod ‘Tourelle’。下麵介紹一下他的使用方式

 

TLAppEventBus

 

TLAppEventBus通過接收系統通知來獲取app生命周期事件,收到生命周期事件後改變對應屬性的值。預設提供了didEnterBackground等八個屬性,可以使用響應式函式來監聽 

 

- (void)observeWithBlock:(TLObservingBlock)block; 

    [TLAppEventBus.shared.didBecomeActive observeWithBlock:^(idnewValue) {

        //do some thing

    }];

 

需要註意,如果在其它地方使用observeWithBlock,需要設置屬性的owner,否則沒有辦法監聽到。這裡不用單獨設置是因為在TLAppEventBus里已設置好

 

TLAppEventBus使用前需要呼叫 – (void)start; 如果需要監聽更多的事件,可以呼叫

 

- (void)startWithNotificationMap:(NSDictionary *)map; 

  NSMutableDictionary *defaultMap = [NSMutableDictionary dictionaryWithDictionary:[TLAppEventBus defaultNotificationMap]]; //獲取預設map

    [defaultMapsetObject:KDidChangeStatusBarOrientation forKey:UIApplicationWillChangeStatusBarOrientationNotification]; //添加新的事件

    [TLAppEventBus.shared startWithNotificationMap:defaultMap];//開啟EventBus

 

添加新事件需要用分類添加TLAppEventBus的屬性,添加後就可正常使用了

 

-(void)setDidChangeStatusBarOrientation:(NSNotification*)didChangeStatusBarOrientation {

    objc_setAssociatedObject(self, (__bridge const void *)KDidChangeStatusBarOrientation , didChangeStatusBarOrientation, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

-(NSNotification*)didChangeStatusBarOrientation {

    returnobjc_getAssociatedObject(self, (__bridge const void *)KDidBecomeActive);

}

 

TLRouter

 

路由支持兩種註冊方式,一種只寫schema,一種寫url路徑

 

[TLRouter registerURL:@"wx1234567://" hander:^(TLRouterURL *routeURL, void (^callback)(BOOL result)) {       

        //do something     

}]//註冊schema

[TLRouter registerURL:@"InnerJump://account/login" hander:^(TLRouterURL *routeURL, void (^callback)(BOOL result)) {

                //do something

 }]//註冊url路徑

 

支持同步 & 異步獲取傳回值,其中異步轉同步內部通過semaphore實現

 

+(void)openURL:(NSString*)url callback:(void(^)(BOOLresult))callback;

+(BOOL)openURL:(NSString*)url;

 

另外openURL除了支持url中帶引數,也支持引數放在字典中

 

+(BOOL)openURL:(NSString*)url param:(NSDictionary *)param;

 

TLAppLaunchHelper

 

TLAppLaunchHelper有兩個函式,一個用來初始化組件。該函式會讀取AutoInitialize.plist中的classes,通過runtime + 自動初始化協議完成初始化

 

-(void)autoInitialize;

AutoInitialize.plist

 

另一個函式用來自動註冊路由,該函式會讀取AutoRegistURL.plist完成路由註冊。其中controller代表類名,params代表預設引數,如果openURL傳的引數與預設引數不符合,路由會報錯

 

-(void)autoRegistURL;

 

 AutoRegistURL.plist

 

路由註冊時,並不決定controller跳轉的方式。註冊者只是呼叫presentingSelf方法,跳轉方式由controller中presentingSelf方法決定。

 

-(BOOL)presentingSelf {

    UINavigationController *rootVC = (UINavigationController *) APPWINDOW.rootViewController;

    if(rootVC) {

        [rootVCpushViewController:self animated:YES];

        returnYES;

   }

    return NO;

}

 

耦合檢測工具

 

針對既有代碼的組件化重構,我這邊開發了一個耦合檢測工具,目前只支持OC。

耦合檢測工具的原理是這樣:工具認為工程中一級檔案夾由組件構成,比如A工程下麵有aa、bb、cc三個檔案夾,aa、bb、cc就是三個待檢測的組件。耦合檢測分三步,第一步通過正則找到組件內.h檔案中所有關鍵字(包括函式、宏定義和類)。第二步通過找到的組件內關鍵字,再通過正則去其它組件的.m中找是否使用了該組件的關鍵字,如果使用了,兩個組件就有耦合關係。第三步,輸出耦合檢測報告

 

代碼:開源中….

總結

本文給出了組件化的定義:組件化意味著重構,目的是讓每個組件職責單一以提升集成效率。包管理技術Pod是組件化常用的工具,iOS組件依賴及組件版本號確定,都可以用pod實現。整個iOS工程的組件通常分為3層,業務組件、模塊組件和SDK組件。在老工程重構時,優先抽離SDK組件,切記不要寫XXCommon讓它變成垃圾堆。

 

關於解耦的技術,appldegate適合用觀察者樣式替換代理樣式,路由只用來做controller之間的跳轉,上層業務組件的解耦更多是靠重構而不是全用路由。工程的組件和路由都可通過runtime + 配置的形式自動註冊,這樣做維護和集成都會很方便。

 

Demo地址:https://github.com/zhudaye12138/Tourelle

 


編號325,輸入編號直達本文

●輸入m獲取文章目錄

推薦↓↓↓

 

Web開發

更多推薦25個技術類微信公眾號

涵蓋:程式人生、演算法與資料結構、黑客技術與網絡安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。

    赞(0)

    分享創造快樂