組件生命週期管理和通訊方案

隨着移動互聯網的快速發展,項目的迭代速度愈來愈快,需求改變愈來愈頻繁,傳統開發方式的工程所面臨的一些,如代碼耦合嚴重、維護效率低、開發不夠敏捷等問題就凸現了出來。因而愈來愈多的公司開始推行"組件化",經過對原有業務或新業務進行組件(或模塊)拆分來提升並行開發效率。html

在筆者面試過程當中發現,不少同窗口中的"組件化"也只是把代碼分庫,而後在主項目中使用 CocoaPods 把各個子庫聚合起來。對於怎樣合理地對組件分層、如何管理組件(主要包括組件的生命週期管理和組件的通訊管理),如何管理不一樣版本的依賴,以及是否有整套集成和發佈工具,這類問題的知之甚少。若是徹底不瞭解這些問題,那麼只是簡單的對主項目進行組件拆分,並不能提升多少開發效率。git

筆者認爲合理地進行組件拆分和管理各個組件之間的通訊是組件化過程當中最大的難點。合理地進行組件拆分是爲了解耦,而且各個組件能更容易地獨立變化。而對於一個完整的應用來講,每一個組件不可能孤零零地存在,一定會互相調用。這樣不一樣組件之間必須能進行通訊而又沒有編譯期的依賴github

組件生命週期管理

可能不少同窗在實施組件化的過程當中知道要解決組件通訊的問題,卻不多關注組件的生命週期。這裏的生命週期主要是指 AppDelegate 中的生命週期方法。有時候一些組件須要在這些鉤子方法中作一些事情,這時候就須要一個可以管理組件的工具,並在適當的時機執行組件相應的邏輯。面試

好比筆者在項目中是這樣作的:swift

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [[Ant shareInstance] application:application didFinishLaunchingWithOptions:launchOptions];
    return YES;
}

- (void)applicationWillResignActive:(UIApplication *)application
{
    [[Ant shareInstance] applicationWillResignActive:application];
}

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    [[Ant shareInstance] applicationDidEnterBackground:application];
}

- (void)applicationWillEnterForeground:(UIApplication *)application
{
    [[Ant shareInstance] applicationWillEnterForeground:application];
}
複製代碼

全部註冊的組件(模塊)會在 AppDelegate 相應的生命週期方法調用時自動調用。例若有以下組件定義:緩存

ANT_MODULE_EXPORT(Module1App)

@interface Module1App() <ATModuleProtocol> {
    NSInteger state;
}
@end

@implementation Module1App
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    state = 0;
    NSLog(@"Module A state: %zd", state);
    return YES;
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
    state += 1;
    NSLog(@"Module A state: %zd", state);
}
@end
複製代碼

上面示例代碼中第一行的 ANT_MODULE_EXPORT(Module1App) 是導出組件。Ant 會在 dyld 加載完 image 後將導出的組件進行註冊。當應用生命週期方法被調用時,會實例化全部註冊過的組件,調用組件相應的方法,並進行緩存,以後再次調用就會從緩存中取出組件的實例對象。app

通常擁有完整生命週期的組件通常稱爲一個模塊,一個模塊其實也是一個獨立的組件,它通常是包含一個完整的業務,列如:登陸模塊,外賣模塊,消息模塊等。工具

組件的生命週期管理並不複雜,實現方案都沒有太大區別,但它也是組件化中必不可少的部分。組件化

組件通訊

業界關於組件通訊的方案比較多,主要有:url-block, target-action, protocol-class。下面筆者會對這三種方案作個簡單的介紹。學習

URL-Block

這是蘑菇街在組件化過程當中使用的一種組件間通訊方式,在應用啓動時註冊組件提供的服務,把調用組件使用的url和組件提供的服務block對應起來,保存到內存中。在使用組件的服務時,經過url找到對應的block,而後獲取服務。

[MGJRouter registerURLPattern:@"mgj://foo/bar" toHandler:^(NSDictionary *routerParameters) {
    NSLog(@"routerParameterUserInfo:%@", routerParameters[MGJRouterParameterUserInfo]);
}];

[MGJRouter openURL:@"mgj://foo/bar"];
複製代碼

筆者是在15年開始學習組件化,那個時候就是使用的蘑菇街的這種發案。不過筆者歷來沒有在實際項目中使用這種方案。casa 在這篇文章中批判了這種方案。筆者對 case 的觀點非常贊同。

若是項目中須要不少組件的服務,那麼就須要在內存中維護大量的 url-block項,形成內存問題,對於服務註冊的代碼應該放在什麼地方也是一個問題。筆者一直認爲 url-block 註冊是一種很粗暴的方式,好比某個應用在啓動時註冊了100個服務,但某些服務在用戶使用過程當中根本就沒有觸發,這就形成了內存浪費。好比咱們點擊應用中的按鈕跳轉到某個頁面,若是用戶沒有點擊按鈕,下個頁面就永遠不會建立,咱們通常不會提早建立這個頁面的。筆者更傾向於在須要服務的時候才進行服務對象的建立,在特定場景下也提供服務對象的緩存。

使用 url 傳參也是一個不可忽略的問題,對於一些基礎數據類型,使用這種方案卻是沒有問題,可是對於一些很是規對象就無能爲力了,如 UIImage, NSData 等類型。

還有一個問題是 casa 在文章中沒有指出的,這個問題在他的 target-action 方案中也存在。下面用一個例子來講明一下。

好比在一個組件 A 中提供了一個服務:

[MGJRouter registerURLPattern:@"mgj://foo/bar" toHandler:^(NSDictionary *routerParameters) {
    NSLog(@"routerParameterUserInfo:%@", routerParameters[MGJRouterParameterUserInfo]);
}];
複製代碼

而後在一個組件 B 中使用了服務:

[MGJRouter openURL:@"mgj://foo/bar"];
複製代碼

從上面示例代碼中能夠看到,兩個不一樣組件能通訊實際上是經過一個字符串來定義的。若是服務使用方在寫代碼時寫錯了一個字符,那麼使用方根本就不可能調起正確的服務,一旦出現這個問題,在開發過程當中很難被發現。若是咱們對組件多,註冊的服務多,那麼在使用時就存在很大的溝通問題,提供方和接入方可能會在每一個字符串所表明的意義上浪費大量的時間。而這些問題均可以在工程設計上避免的。雖然說咱們在寫代碼時要低耦合,但並不表明不要耦合,有時候須要一些耦合來提升代碼的健壯性和可維護性。

在 Swift 中可使用枚舉來解決上面的問題,咱們能夠像下面這樣作:

protocol URLPatternCompatible {
    var URLPattern: String { get }
}

enum SomeService {
    case orderDetail
    case others
}

enum SomeService: URLPatternCompatible {
    var URLPattern: String {
        switch self {
        case .orderDetail:
            return "mgj://foo/bar/orderdetail"
        case .others:
            return "mgj://foo/bar/others"
        }
    }
}

// 組件 A (服務提供方)
MGJRouter.register(.orderDetail) { ... }

// 組件 B (服務使用方)
MGJRouter.open(.orderDetail)
複製代碼

SomeService 的定義能夠放到一個專門的組件中,服務提供方和使用方都依賴這個專門的組件。咱們這裏不只將字符串放到了一個統一的地方進行維護,並且還將一些在運行期才能發現的問題提早暴露到編譯器。這裏咱們經過耦合來達到提升代碼的健壯性和可維護性的目的。

Target-Action

Target-actin 是 casa 在批判蘑菇街的方案時提出的一種方案。它解決了 url-block 方案中內存問題、url 傳參問題、沒有區分本地調用和遠程調用等問題。其核心就是使用了 NSObject 的 - (id)performSelector:(SEL)aSelector withObject:(id)object; 方法。

在本地應用調用中,本地組件A在某處調用 [[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{...}]CTMediator 發起跨組件調用,CTMediator 根據得到的 target 和 action 信息,經過 objective-C 的 runtime 轉化生成 target 實例以及對應的 action 選擇子,而後最終調用到目標業務提供的邏輯,完成需求。

casa 在文章中也給出了 demo,在具體的項目中,咱們能夠這樣使用:

// CTMediator+SomeAction.h
- (UIViewController *)xx_someAction:(NSDictionary *)params;

// CTMediator+SomeAction.m 
- (UIViewController *)xx_someAction:(NSDictionary *)params {
	return [self performTarget:@"A" action:@"someAction" params:params shouldCacheTarget:NO]
}
複製代碼

上面是提供給服務調用方的一個簡潔的接口。其實就是對 CTMediator 方法的封裝。咱們通常將 CTMediator 的這個分類放到一個獨立的組件中。調用方依賴這個獨立的組件就能夠了。

在某個組件中調用服務:

// 組件 A 中
UIViewController *vc = [CTMediator sharedInstance] xx_someAction:@{@"key": value}];
複製代碼

針對上面服務的定義,服務提供方的定義就必須是下面這樣:

// TargetA.h
@interface Target_A : NSObject
- (UIViewController *)someAction:(NSDictionary *)params;
@end

// TargetA.m
- (UIViewController *)someAction:(NSDictionary *)params { ... }

複製代碼

在這整個過程當中能夠看到,服務的調用方只須要依賴 CTMediator 這個中間件及其分類(定義服務)。服務提供方和調用方沒有任何依賴。確實作到了組件解耦。能夠確定的是 target-action 方案確實解決了 url-block 方案的一些問題。可是仔細一看,也是存在一些問題的。

跟 url-block 方案同樣,兩個不一樣組件能通訊其實仍然是經過一個字符串來定義的。爲何這麼說呢,咱們能夠看一下下面的代碼:

// CTMediator+SomeAction.m 
- (UIViewController *)xx_someAction:(NSDictionary *)params {
    return [self performTarget:@"A" action:@"someAction" params:params shouldCacheTarget:NO]
}

// TargetA.h
@interface Target_A : NSObject
- (UIViewController *)someAction:(NSDictionary *)params;
@end
複製代碼

從上面的代碼中能夠看到,服務能調起主要是調用了 CTMediator 的

- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget; 方法。這裏不論是 targetName 仍是 action 都是字符串,在實現中 CTMediator 會示例化一個 Target_targetName 類的對象,而且建立一個 Action_actionName 的 selector,全部咱們在服務提供的組件中的 Target 以及 Action 是不能隨便定義的。Target 必須是以 Target_開頭,方法必須以 Action_開頭。這種強制要求感受不是一種工程師的思惟。這裏想去耦合,卻以一種不是很正確的方式形成了隱式的耦合。這也是讓我拋棄 CTMediator 轉而去開發本身的組件化通訊方案的緣由之一。

Protocol-Class

Protocol-Class 方案也是經常使用的組件化通訊方式之一。這裏把它放到最後,確定是由於筆者使用的是這種方案咯(笑)。

Protocol-Class 方案就是經過 protocol 定義服務接口,服務提供方經過實現該接口來提供接口定義的服務。具體實現就是把 protocol 和 class 作一個映射,同時在內存中保存一張映射表,使用的時候,就經過 protocol 找到對應的 class 來獲取須要的服務。這種方案的優缺點先不說,能夠先看一下具體的實踐:

示例圖:

示例代碼:

// TestService.h (定義服務)
@protocol TestService <NSObject>
/// 測試
- (void)service1;

@end

// 組件 A (服務提供方)
ANT_REGISTER_SERVICE(TestServiceImpl, TestService)
@interface TestServiceImpl() <TestService> @end

@implementation TestServiceImpl

- (void)service1 {
    NSLog(@"Service test from Impl");
}

@end

// 組件 B (服務使用方)
id <TestService> obj = [Ant serviceImplFromProtocol:@protocol(TestService)];
[obj service1];
複製代碼

像上面的方案同樣,咱們會將服務的定義放到獨立的組件中。這個組件僅僅只包含了服務的聲明。不論是服務提供方仍是服務使用方都依賴這個獨立的組件,服務提供方仍是服務使用方互不依賴。

這裏將系統提供的服務定義爲協議,經過耦合提升了代碼的健壯性和可維護性。這裏定義服務的 protocol 對服務提供方作了一個限定:你能夠提供哪些服務,同時也給服務使用方作了限定:你可使用哪些服務。這種設計將系統有哪些服務都交代的清清楚楚,經過服務的 protocol 咱們就知道了每一個服務的功能,調用須要的參數,返回值等。這裏的定義服務的同時也能夠做爲系統服務的接口文檔,這節省了服務提供方和使用方不少的溝通時間,讓其能關注業務的開發。這在大型項目,多團隊開發中優點尤其明顯。

固然 protocol-class 這種方案缺點也很明顯,須要在內存中保存 protocol 到 Class 的映射關係。可是咱們能夠經過將服務分類,讓系統註冊的 protocol-class 項儘可能少一些,不要一個服務定義一個實現。對於一個有100個服務的系統,定義10個服務實現,每一個實現提供10個服務,確定要比100個服務實現佔用的內存少不少。這就要求咱們在實踐過程當中能對系統中的服務能作好劃分。

總結

以上就是筆者對組件化的一些思考,不少觀點可能也不太成熟,若是有什麼不合理的地方,也歡迎各位同窗提出建議。組件解耦在 iOS 中其實有多種解決方案,各位同窗能夠根據項目實際狀況選擇適合本身的方案。

上面代碼中的 Ant 是筆者最近開發的一個負責組件生命週期管理和通訊的開源工具。由於筆者公司從17年開始就一直使用 Swift 進行開發,原來的工具是用 Swift 編寫的,使用了不少 Swift 的特性,在 OC 中使用就顯得不三不四了,就針對 OC 進行了從新設計,因而就有了 Ant

相關文章
相關標籤/搜索