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

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)

    分享創造快樂