iOS 元件通訊方案
1.
閱讀本篇文章以前,假設你已經瞭解了元件化這個概念。
最近兩年移動端的元件化特別火,但移動端元件化的概念追其溯源應該來自於 Server
端,具體來說這種概念應該是由 Java
的 Spring
框架帶來的。
Spring
最初是想替代笨重的 EJB
,在版本演進過程中又提供了諸如 AOP
、 DI
、 IoC
等功能,推動了 Java
程式員面向介面程式設計,而面向介面程式設計在面向物件的基礎上將物件又抽象了一層,對外提供的服務只提供介面類而不直接提供物件類,這就引出了一個問題,為什麼給外部提供的是一個介面類而不是物件類?
回想下我們在編寫 iOS
程式碼的過程中,我們最常採用的程式碼組織方式是 MVC
,最常使用的開發思想是面向物件程式設計,假設現在有一個控制器 AViewController
,這個控制器的 UI
由3部分組成,從上至下分別為頂部 Banner
,中間是 UICollectionView
管理著一些入口,底部是 UITableView
管理著商品列表,單一職能原則約束著我們這3部分業務邏輯最好是由3個類去管理,大家通常也是這麼做的,因此現在 AViewController
就要對這3個類進行引用( import
),假設中間部分的入口可以跳轉到10個不用的頁面( Controller
),那麼可能就會有人在 AViewController
中 import
這10個 Controller
,此時,耦合的關係就產生了,如果整個專案都按照這個流程開發,最終整個專案類與類之間的耦合關係會複雜到難以想象,當我們需要把一個類或某個功能、某條業務遷移到其他專案時,可能你就會變成這樣

what the fuck!
tmd
怎麼這麼多錯誤?
怎麼解決?
1、根據 IDE
的錯誤提示慢慢改,缺啥補啥。
2、元件化,一勞永逸。
2.
如何進行元件化,網上已經有了不少文章講解了這方面的經驗,我這裡再簡單說一說,說不全我文章寫不下去。
第一步:規劃專案整體架構
設計專案的整體架構並不是讓你決定使用 MVC
還是 MVVM
,在我看來, MVC
和 MVVM
亦或是 MVP
等等等,都屬於程式碼的組織方式,嚴格意義上來說,並不能算是專案架構,專案架構需要你站在更高的緯度去統籌、規劃專案該如何分層,這個時候就需要你根據產品來對專案劃分不通的層次,業務層的程式碼就劃分到業務層,第三方庫都是通用的,就可以把這些第三方庫劃分到通用層,那麼這個層級關係誰在上誰在下?我們可以根據對業務對程式碼的依賴程度來劃分,那麼業務層就應該在最上面,通用層的程式碼在最下面。如圖:

ABCD
使用的業務類的程式碼;中間層的作用是協調和解耦的作用,協調元件間的通訊,解除元件間的耦合,它要做的也就是這篇文章的標題所要講的,中間層就是元件通訊方案。
第二步:管理基礎元件
第一類基礎元件:
一個 iOS
專案可能會依賴很多第三方開源庫,比如 AFNetworking
、 SDWebImage
, FMDB
等,這些開源框架服務全球上百萬個專案,它們是對系統 API
的封裝,並且不依賴於業務,我們可以將他們歸到基礎元件裡,很多專案使用 cocoapod
來管理這些庫,也有直接把庫檔案直接拖到專案裡來的,我這裡假設使用 cocoapod
進行管理。
第二類基礎元件:
而在一些比較大的專案裡或要求比較高的公司往往會將這些第三方開源框架進行二次封裝,以滿足一些使用上的需要或彌補一些先天的缺陷,那麼這些進行二次封裝的庫同樣也屬於基礎元件,我們可以將自己二次封裝的庫也放到通用層這一層,那怎麼管理這些二次封裝的庫呢?推薦使用本地的私有庫,利用 cocoapod
進行管理。
第三類基礎元件:
在開發業務時,我們也可以從業務程式碼中抽取一些庫出來,比如很多新聞 App
首頁的橫向滾動頁面就可以抽取出一套 UI
框架, UITabbarController
也可以抽取成一套 UI
框架,高效的切一個 UI
控制元件的圓角我們也可以抽取成一套小的 UI
框架,自定義彈窗、 loading
動效等都可以抽取成單獨的框架。
在整理這些基礎元件的同時,勢必要改很多業務層的程式碼,這會讓你感覺很噁心,但做這些事情的同時也是在為我們的業務元件化鋪路,也就是說,抽取基礎元件會推進我們進行業務元件化。
第三步:業務元件化
既然我們封裝的基礎元件可以使用私有 pod
進行管理,業務層程式碼可以用私有 pod
進行管理嗎?答案是可以,業務元件化也可以通過私有 pod
庫來解決。
我們在第一步中劃分好了專案的架構層次,最頂層的是業務層,業務層根據業務屬性劃分好了若干條業務線,那麼每條業務線就對應著一個 pod
私有庫,在我們打包私有庫的時候,私有 repo
對程式碼的檢查可是相當嚴格的,像引用了一個本 repo
中不存在的類, repo
的校驗都是通不過的,所以這就逼你把各業務線的程式碼進行歸類,屬於哪條業務線的程式碼就劃分到相應的業務線中,這樣做下來,各業務線最後只保留了和本業務線相關的程式碼,感覺結構上和程式碼上都清晰了不少。
但還有一個新問題,業務 A
的程式碼呼叫業務 B
的程式碼怎麼辦?難道要在業務 A
的程式碼中 import
業務 B
的程式碼,那不又耦合了嗎?而且即便可以這樣做,私有 pod
也不允許我們這樣做,因為在校驗私有 repo
的時候,這樣的做法根本校驗不通過,為了解決這個問題,我們引入了中間層,讓中間層來解決這個問題,有句話說的好: 沒有什麼問題是一箇中間件解決不了的,有就用兩個
,這就引出了接下來要講的,元件間的通訊方案。
3.
iOS
端通用的元件間通訊方案有如下3種:
- URL Router
- Target-Action
- 面向介面程式設計(Protocol - Class)
接下來說這3種方案的具體實現原理。
URL Router
在前端,一個 url
表示一個 web
頁面。
在後端,一個 url
表示一個請求介面。
在 iOS
,我們要在 App
中跳轉到手機系統設定中的某個功能時,方式是通過 UIApplication
開啟一個官方提供的 url
,相當於一個 url
也是一個頁面。
所以,參考以上幾種場景,我們也可以用一個 url
表示一個頁面( Controller
),不止可以表示頁面,還可以表示一個檢視( UI
控制元件),甚至是任意一個類的物件。
知道可以這麼做,我們就可以建立一個字典, key
是 url
, value
是相應的物件,這個字典由路由類去管理,典型的方案就是 MGJRouter
。
這種方案的優點是能解決元件間的依賴,並且方案成熟,有很多知名公司都在用這種方案;缺點是編譯階段無法發現潛在 bug
,並且需要去註冊 &
維護路由表。
程式碼示例:
註冊路由 [[Router sharedInstance] registerURL:@"myapp://good/detail" with:^UIViewController *{ return [GoodDetailViewController new]; }]; 通過url獲取 UIViewController *vc = [[Router sharedInstance] openURL:@"myapp://good/detail"]
Target-Action
Target-Action
可直接譯為目標-行為,在 Object-C
中 Target
就是訊息接收者物件, Action
就是訊息,比如我們要呼叫 Person
物件的 play
方法我們會說向 Person
物件傳送了一個 play
訊息,此時 Target
就是 person
物件, Action
就是 play
這個方法。
到了專案中,如何利用 Target-Action
機制進行解耦?別忘了, Object-C
這項高階語言同樣支援反射。
之前我們在 AViewController
中 push
到 BViewController
,需要在 AViewController
類檔案中 import
進 BViewController
,這樣二者就會產生耦合,現在利用 Target-Action
機制,我們不再直接 import
進 BViewController
,而是利用 NSClassFromString(<#NSString * _Nonnull aClassName#>)
這個 api
將 BViewController
這個字串反射成 BViewController
這個類,這樣我們就可以根據反射後的類進行例項化,再呼叫例項化物件的各種方法。
利用 Target-Action
機制,我們可以實現各種靈活的解耦,將任意類的例項化過程封裝到任意一個 Target
類中,同時,相比於 URL Router
, Target-Action
也不需要註冊和記憶體佔用,但缺點是,編譯階段無法發現潛在的 BUG
,而且,開發者所建立的類和定義的方法必須要遵守 Target-Action
的命名規則,呼叫者可能會因為硬編碼問題導致呼叫失敗。
這種方案對應的開源框架是 CTMediator
和阿里 BeeHive
中的 Router
,二者都是通過反射機制拿到最終的目標類和所需要呼叫的方法(對應的api是 NSSelectorFromString(<#NSString * _Nonnull aSelectorName#>)
),最終通過 runtime
或 performSelector:
執行 target
的 action
,在 action
中進行類的例項化操作,根據具體的使用場景來決定是否將例項物件作為當前 action
的返回值。
這裡不再列舉 demo
, CTMediator
和 BeeHive
在 github
中都可以搜到。
面向介面程式設計
我們在第 1
部分囉嗦了一大堆就是為了給面向介面程式設計這一部分做鋪墊,傳統的 MVC
+ 面向物件程式設計
的程式設計方式引出的問題我們在第 1
部分簡單闡述了一些,而除了這些問題之外,還會產生哪些問題?接下來會講述一些例子。
在 Java
中,介面是 Interface
,在 Object-C
中,介面是 Protocol
,所以在 Object-C
中,面向介面程式設計又被稱為面向協議程式設計,在 Swift
中, Apple
強化了面向介面程式設計這一思想,而這一思想,早已稱為其他語言的主流程式設計思想。
什麼是面向介面程式設計?面向介面程式設計強調我們再設計一個方法或函式時,應該更關注介面而不是具體的實現。
舉個具體的業務需求作為例子:
彈窗幾乎在所有 App
中都存在,大廠 App
中的彈窗相對來說比較剋制,除了升級之外的彈窗幾乎見不到其他型別,中小型 App
中的彈窗就比較多,比如升級彈窗、活動彈窗、廣告彈窗等等,當然,需求複雜的時候,產品還會要求彈窗時機以及彈窗的優先順序等條件。
當我們使用面向物件程式設計思想時,解決方案大概是下面這樣的:
PS:以下程式碼示例基於下面兩個條件
1、如果彈窗介面來自於多個 Service
。
2、如果專案大,彈窗這個業務需求也可能來自於不同的業務線,有時候你無法強制要求其他業務線的開發人員必須使用你定製好的類進行開發,可能你覺得你定義的類能適用很多場景,但人家未必這樣認為。
- 需求第1期:升級彈窗
資料型別 @interface UpgradePopUps : NSObject @property(nonatomic, copy) NSString *content;//內容 @property(nonatomic, copy) NSString *url; //AppStore連結 @property(nonatomic, assign) BOOL must; //是否強制升級 @end 升級彈窗 @interface UpgradView : UIView - (void)pop; @end
- 需求第2期:廣告彈窗
資料型別 typedef NS_ENUM(NSUInteger, AdType) { AdTypeImage,//圖片 AdTypeGif,//GIF AdTypeVideo,//視訊 }; @interface AdPopUps : NSObject @property(nonatomic, copy) AdType type; //廣告型別 @property(nonatomic, copy) NSString *content;//內容 @property(nonatomic, copy) NSString *url; //路由url(可能是native頁面也可能是H5) @end 廣告彈窗 @interface AdView : UIView - (void)pop; @end
- 需求第3期,彈窗太多了,給加個優先順序,根據優先順序彈窗。
- 需求第4期,加個活動彈窗,定個優先順序。
- 需求第5期,加個XX彈窗,定個優先順序。
估計此刻的你應該是這樣的:

現在使用面向介面程式設計思想對業務進行改造,我們抽象出一個介面如下:
@protocol PopUpsProtocol <NSObject> //活動型別(識別符號) @property(nonatomic, copy) NSString *type; //跳轉url @property(nonatomic, copy) NSString *url; //文字內容 @property(nonatomic, copy) NSString *content; @required //開啟執行,在這個方法中展示出彈窗 - (void)execute; @end
一個簡單的介面就抽象完了,下次如果有新的彈窗需要接入,只需要讓新的彈窗類遵守這個 PopUpsProtocol
就可以了,例項化一個彈窗物件的方法如下所示:
id<PopUpsProtocol> popUps = [[AdPopUps alloc] init]; popUps.url = @"..."; popUps.content = @"..."; popUps.type = @"...": //show [popUps execute];
在 AdPopUps
中程式碼如下:
@interface AdPopUps : NSObject <PopUpsProtocol> @property(nonatomic, copy) NSString *type; @property(nonatomic, copy) NSString *url; @property(nonatomic, copy) NSString *content; @end @implementation AdPopUps - (void)execute { AdView *adView = [AdView alloc] init]; [adView show]; } @end
現在我們把這些彈窗事件封裝到 Task
(任務)物件中,這個自定義物件可以設定優先順序,然後當把這個任務加入到任務佇列後,佇列會根據任務的優先順序進行排序,整個需求就搞定了。下面來看一下 Task
類:
typedef NS_ENUM(NSUInteger, PopUpsTaskPriority) { PopUpsTaskPriorityLow,//低 PopUpsTaskPriorityDefault,//預設 PopUpsTaskPriorityHigh,//高 }; @interface MSPopUpsTask : NSObject //任務的唯一識別符號 @property(nonatomic, copy) NSString *identifier; //優先順序 @property(nonatomic, assign) PopUpsTaskPriority priority; //任務對應的活動 @property(nonatomic, strong) id<PopUpsProtocol> activity; //初始化方法 - (instancetype)initWithPriority:(PopUpsTaskPriority)priority activity:(id<PopUpsProtocol>)activity identifier:(NSString *)identifier; //執行任務 - (void)handle; @end @implementation MSPopUpsTask - (void)handle { if ([_activity respondsToSelector:@selector(execute)]) { [_activity execute]; } } @end
大家看到了, Task
沒有直接依賴任何 PopUps
類,而是直接依賴介面 PopUpsProtocol
。
一個面向介面程式設計的小例子這裡就講述完了,這個例子中的對於介面的使用方法只是其中一種,在實際應用中,還有其他使用方法,大家可自行搜尋。
接下來說採用面向介面程式設計思想輸出的程式碼會帶來的哪些好處?
1.介面比物件更直觀
讓程式設計師看一個介面往往比看一個物件及其屬性要直觀和簡單,抽象介面往往比定義屬性更能描述想做的事情,呼叫者只需要關注介面而不用關注具體實現。
2.依賴介面而不是依賴物件
剛才我們使用面向介面程式設計的方式建立了一個物件:
id<PopUpsProtocol> popUps = [[AdPopUps alloc] init];
現在我們除了要引用 AdPopUps
這個類外,還要引用 PopUpsProtocol
,一下引用了兩個,好像又把問題複雜化了,所以我們想辦法只引用 protocol
而不引用類,這個時候就需要把 protcol
及這個 protocol
的具體實現類繫結在一起( protocol
- class
),當我們通過 protocol
獲取物件的時候,實際上獲取的是遵守了這個 protocol
協議的物件,那如果一個 protocol
對應多個實現類怎麼辦?別忘了有工廠模式。
所以,我們需要將 Protocol
和 Class
繫結到一起,程式碼大概是這種形式的:
[self bindBlock:^(id objc){ AdPopUps *ad = [[AdPopUps alloc] init]; ad.url = @"..."; return (id<PopUpsProtocol >)ad; } toProtocol:@protocol(PopUpsProtocol)];
獲取方式就是這樣的:
id<PopUpsProtocol> popUps = [self getObject:@protocol(PopUpsProtocol)];
呼叫方法:
[popUps execute];
這樣就把問題解決了。
好了,我們就可以將這個彈窗管理系統作為一個元件去釋出了,所以,為了實現基於元件的開發,必須有一種機制能同時滿足下面兩個要求:
(1)解除 Task
對具體彈窗類的強依賴(編譯期依賴)。
(2)在執行時為 Task
提供正確的彈窗例項,使彈窗管理系統可以正確展示相應的彈窗。
換句話說,就是將 Task
和 PopUps
的依賴關係從編譯期推遲到了執行時,所以我們需要把這種依賴關係在一個合適的時機(也就是 Task
需要用到 PopUps
的時候)注入到執行時,這就是依賴注入( DI
)的由來。
需要注意的是, Task
和 PopUps
的依賴關係是解除不掉的,他們倆的依賴關係依然存在,所以我們總說,解除的是強依賴,解除強依賴的手段就是將依賴關係從編譯期推遲到執行時。
其實不管是哪種程式設計模式,為了實現鬆耦合(服務呼叫者和提供者之間的或者框架和外掛之間的),都需要在必要的位置實現面向介面程式設計,在此基礎之上,還應該有一種方便的機制實現具體型別之間的執行時繫結,這就是依賴注入( DI
)所要解決的問題。
如何簡單理解依賴注入?
我們可以將執行中的專案當做是主系統,這些介面及其背後的具體實現就是一個個的外掛,主系統並不依賴任何一個外掛,當外掛被主系統載入的時候,主系統就可以準確呼叫適當外掛的功能。
下面,就要開始分享 Object-C
對 DI
的具體實現了,這裡需要引入一個框架 Objection
, github
上可以搜尋到。
4.
DI
往往和 IoC
聯絡到一起的, IoC
更多指 IoC
容器。
IoC
即控制反轉,該怎麼理解 IoC
這個概念?
簡單理解,從前,我們使用一個物件,除了銷燬之外( iOS
有 ARC
進行記憶體管理),這個物件的控制權在我們開發人員手裡,這個控制權體現在物件的初始化、對屬性賦值操作等,因為物件的控制權在我們手裡,所以我們可以把這種情況稱為“控制正轉”。
那麼控制反轉就是將控制權交出去,交給 IoC
容器,讓 IoC
容器去建立物件,給物件的屬性賦值,這個物件的初始化過程是依賴於 DI
的,通過 DI
(依賴注入)實現 IOC/">IOC
(控制反轉)
DI
提供了幾種注入方式,這裡說幾個最常用的:
- (1)構造器注入
也就是通過我們指定的初始化方法進行注入,比如針對於 Task
這個類,它的構造器就是:
- (instancetype)initWithPriority:(PopUpsTaskPriority)priority activity:(id<PopUpsProtocol>)activity identifier:(NSString *)identifier;
IoC
容器會根據這個構造器的引數將依賴的屬性注入進來,並完成最終的初始化操作。
(2)屬性注入
也叫setter方法注入,即當前物件只需要為其依賴物件所對應的屬性新增setter方法, IoC
容器通過此 setter
方法將相應的依賴物件設定到被注入物件的方式即 setter
方法注入。在 Java Spring
中,可以在 XML
檔案中配置屬性注入的預設值,比如:
<beans> <bean id="Person" class="com.package.Person"> <property name="name"> <value>張三</value> </property> </bean> </beans>
在 iOS
中可以通過 plist
檔案來儲存這些預設值。
- (3)介面注入
介面注入和以上兩種注入方式差不多,但首先你要告訴 IoC
容器這個介面對應哪個實現類,否則光注入一個介面有什麼用呢?所以我們需要在專案內給每一個介面建立一個實現類,使介面與類是一一對應的關係( protocol-class
)。
在上面的例子中,因為 Task
有個屬性實現了這個 PopUpsProtocol
介面,所以 IoC
注入的是這個介面的實現類,所以從這個角度來說,介面注入實際上與 setter
注入是等價的。
在 Java Spring
中,介面注入同樣是通過 XML
檔案進行配置的,但現在更多的是用註解來替代 XML
注入。
5.
ofollow,noindex"> Objection
原始碼分析地址