元件化工具BeeHive(二):元件化實踐

wangzzzzz發表於2018-06-12

前言

使用BeeHive來進行專案元件化,其實是使用BeeHive來構建一箇中間層,通過中間層來解耦各個模組。在文章iOS元件化通用工具淺析有簡單介紹過BeeHive的一些元件化思路,本文將更多的從使用者的角度來分析BeeHive。

1. 用法

通過構建中間層來元件化專案,共需要三步:

  1. 建立protocol
  2. 建立impClass
  3. 儲存protocol-impClass對映關係

BeeHive-demo2為例

1.1. 建立protocol

protocol表示模組對外暴露的介面,呼叫模組時只需要依賴模組對應的protocol,就可以實現對模組的呼叫。

下列程式碼表示,模組A對應的協議BHServiceProtocol的定義,呼叫者可以通過-[getModuleAMainViewController]-[pushToModuleAOneViewController]這兩個方法來呼叫模組A。

// BHServiceProtocol.m

#import "BHServiceProtocol.h"
#import <Foundation/Foundation.h>

@protocol ModuleAServiceProtocol <NSObject, BHServiceProtocol>

- (UIViewController *)getModuleAMainViewController;

- (void)pushToModuleAOneViewController;

@end
複製程式碼

這個協議需要繼承BeeHive中的協議BHServiceProtocol,協議BHServiceProtocol中定義如下兩個可選方法+[singleton:]+[shareInstance]。 如果協議對應的響應者impClass實現了這兩個方法,並且+[singleton:]方法返回YES,則呼叫響應類的+[shareInstance]方法來建立響應者物件。否則,直接呼叫[[implClass alloc] init]來建立物件。

#import <Foundation/Foundation.h>
#import "BHAnnotation.h"
@protocol BHServiceProtocol <NSObject>

@optional

+ (BOOL)singleton;

+ (id)shareInstance;

@end
複製程式碼

1.2. 建立impClass

impClass是protocol對應的響應類,它需要遵守這個protocol協議,它可以是模組中一個已經存在的業務類,也可以是這個模組的一個封裝類。

如果模組對外暴露的方法全部來自於同一個業務類,則可以將這個業務類設定成impClass; 如果模組對外暴露的方法全部來自於多個不同的業務類,則需要給這個模組建立一個封裝類,通過這個封裝類來實現對模組的呼叫,impClass指向這個封裝類。(這種方式也叫做target-action)

第一種方式比較常用,BeeHive的官方demo基本上是使用的這種方法。

模組A的impClass是ModuleAService類,它是一個封裝類,內部實現了對模組A中兩個不同類的呼叫。

//ModuleAService.m 

#import "ModuleAOneViewController.h"
#import "ModuleAViewController.h"
#import "ModuleAService.h"

@implementation ModuleAService

- (UIViewController *)getModuleAMainViewController{
    return [ModuleAViewController new];
}

- (void )pushToModuleAOneViewController{
    UITabBarController *tab = (UITabBarController *)[UIApplication sharedApplication].delegate.window.rootViewController;
    UINavigationController *nav = tab.selectedViewController;
    ModuleAOneViewController *one = [ModuleAOneViewController new];
    
    [nav pushViewController:one animated:YES];
    
}
複製程式碼

另外,模組C對外暴露的方法只有一個,所以模組C使用的是第一種方式,它的impClass直接指向ModuleCViewController這個業務類。

1.3. 設定protocol-impClass對映關係

在BeeHive中,所有protocol-impClass的對映關係都由BHServiceManager管理,BHServiceManager主要提供了兩個方法:

- (void)registerService:(Protocol *)service implClass:(Class)implClass;
- (id)createService:(Protocol *)service;
複製程式碼

方法名中的service指的就是上文中所說的protocol,所以方法一的作用是註冊protocol-impClass的對映關係,方法二的作用是通過protocol獲取對應的響應類。

BHServiceManager類中,有一個叫做allServicesDict的屬性,它儲存了所有的protocol-impClass的對映關係,上述方法一和方法二就是根據這個屬性來執行的。 allServicesDict是一個可變字典,其中key是protocol的字串名稱,value是impClass的字串名稱。

具體註冊方式有下列三種

1.3.1. 使用BeeHive類的-[registerService:service:]

方法-[registerService:service:]的實現

- (void)registerService:(Protocol *)proto service:(Class) serviceClass
{
    [[BHServiceManager sharedManager] registerService:proto implClass:serviceClass];
}
複製程式碼

這個方法內部就是呼叫了BHServiceManager-[registerService:implClass:]方法,將傳入的protocolimpClass新增到BHServiceManager類的屬性allServicesDict中。

1.3.2. 使用巨集BeeHiveService

上文中,定義了ModuleA模組的協議ModuleAServiceProtocol和響應類ModuleAService,可以使用如下程式碼來註冊它們之間的關係:

BeeHiveService(ModuleAServiceProtocol, ModuleAService)
複製程式碼

使用巨集來註冊時,務必在本模組中呼叫巨集。如果在主工程中呼叫,且主工程沒有匯入這個模組(更準確的說是impClass對應的類沒有匯入),會導致程式crash。

上一篇文章第四節中已經講過了註冊Module類的巨集BeeHiveMod,這兩個巨集的實現原理是一樣的,都是在mach-o檔案中增加一個section來儲存資料,然後在啟動專案時取出資料,最終也是呼叫BHServiceManager-[registerService:implClass:]方法來註冊,詳細過程這裡就不在贅述。

mach-o檔案的section:__DATA:BeehiveServices中儲存的是一個json格式的字串:

"{ \"ModuleAServiceProtocol\" : \"ModuleAService\"}"
複製程式碼
1.3.3. 使用plist檔案

使用plist檔案註冊,需要在初始化BeeHive時指定plist檔案的路徑

[BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";
複製程式碼

plist檔案的格式:

元件化工具BeeHive(二):元件化實踐

需要注意的是BeeHive.bundle必須新增到專案的主工程的target上,因為BeeHive內部是在[NSBundle mainBundle]的目錄下尋找BeeHive.bundle。 當使用cocoapods來載入BeeHive時,預設情況下,BeeHive.bundle是存在於BeeHive.framework中,這個時候使用[NSBundle mainBundle]時獲取不到BeeHive.bundle的,解決辦法是改用[NSBundle bundleForClass:self.class]或將BeeHive.bundle新增到專案的主工程的target上。

2. 使用場景

一個典型的場景,當呼叫模組A時,如果當前還沒有登入,則呼叫登入模組,登入成功之後,再呼叫模組A;如果已經登入了,則直接呼叫模組A。

以專案BeeHive-demo3為例

模組A對外的協議ModuleAServiceProtocol

//BHServiceProtocol.h

#import "BHServiceProtocol.h"
#import <Foundation/Foundation.h>

@protocol ModuleAServiceProtocol <NSObject, BHServiceProtocol>

- (void)pushToModuleAViewController;

@end
複製程式碼

模組A的響應類

//ModuleAService.m

#import "ModuleAViewController.h"
#import "ModuleAService.h"

@BeeHiveService(ModuleAServiceProtocol, ModuleAService)
@implementation ModuleAService

- (void )pushToModuleAViewController{
    
    id<LoginServiceProtocol> moduleAService = [[BeeHive shareInstance] createService:@protocol(LoginServiceProtocol)];
    
    [moduleAService loginIfNeedWithCompleteBlock:^(BOOL succeed) {
        if (succeed) {
            UINavigationController *root = (UINavigationController *)[UIApplication sharedApplication].delegate.window.rootViewController;
            ModuleAViewController *moduleA = [ModuleAViewController new];

            [root pushViewController:moduleA animated:YES];
        }
    }];
}
@end
複製程式碼

不管有沒有登入,首先呼叫登入模組,具體的跳轉邏輯被儲存在block中,然後傳給登入模組,登入完成之後,執行這個block。

登入模組的協議LoginServiceProtocol

//LoginServiceProtocol.h

#import "BHServiceProtocol.h"
#import <Foundation/Foundation.h>

@protocol LoginServiceProtocol <NSObject, BHServiceProtocol>

- (void)loginIfNeedWithCompleteBlock:(void (^)(BOOL))completeBlock;

@end
複製程式碼

登入模組的響應類

//LoginService.m 

#import "LoginViewController.h"
#import "LoginService.h"

@BeeHiveService(LoginServiceProtocol, LoginService)
@implementation LoginService

- (void)loginIfNeedWithCompleteBlock:(void (^)(BOOL))completeBlock{
    if ([LoginViewController isLogined]) {
        completeBlock(YES);

    }else{
        LoginViewController *login = [LoginViewController new];
        login.completeBlock = completeBlock;
        
        UIViewController *root = [UIApplication sharedApplication].delegate.window.rootViewController;
        [root presentViewController:login animated:YES completion:nil];
    }
}

@end
複製程式碼

如果已經登入,直接執行傳入的block;如果沒有登入,則彈出登入介面,登入成功之後,執行block。

3. impClass的生命週期

通過上文可知,impClass的物件是最終是由BHServiceManager類建立的,但是BHServiceManager類並沒有持有impClass的物件,本質上,BHServiceManager相當於是一個物件工廠。

如果impClass是一個模組的封裝類,impClass的物件只在當前作用域有效,超過了這個作用域,這個物件會被釋放掉。 如果impClass是一個模組的業務類,則impClass物件的生命週期依賴於模組內部的具體實現了。

如果想長期持有這個impClass物件,通常有兩種方式:

1.在模組呼叫處,強引用被建立的impClass物件。

2.實現BeeHive中BHServiceProtocol協議的+[singleton]方法,並返回YES。這樣,被建立的impClass物件會被儲存在單例[BHContext shareInstance]中。(如果同時實現了+[shareInstance]方法,則使用這個方法來建立impClass的物件)

可以使用下列BHContext的方法來移除儲存的impClass物件

- (void)removeServiceWithServiceName:(NSString *)serviceName;
複製程式碼

4. 異常處理

BeeHive可以通過下列設定來開啟異常模式,在這個模式下,如果遇到BeeHive內部的一些錯誤,會直接丟擲異常。一般在除錯模式下,應該開啟。生產模式下,應該關閉。

[BeeHive shareInstance].enableException = YES;
[[BeeHive shareInstance] setContext:[BHContext shareInstance]];
複製程式碼

4.1 註冊時異常

註冊方式共有三種:

  1. 使用BeeHive類的-[registerService:service:]
  2. 使用巨集BeeHiveService
  3. 使用plist檔案

註冊時,可能存在下列三種情況:

  1. protocol和impClass對應的協議或類不存在
  2. protocol和impClass存在,但impClass沒有遵循對應的protocol
  3. protocol和impClass存在,且impClass遵循對應的protocol
方式一 方式二 方式三
情況一 編譯時報錯 啟動時crash 註冊成功
情況二 註冊不成功,如果是異常模式,則crash 註冊不成功,如果是異常模式,則crash 註冊成功
情況二 註冊成功 註冊成功 註冊成功

當註冊方法和被註冊的模組沒有寫在一起時,刪除了模組,而它的註冊方法沒有被刪除,這個時候就會出現情況一,比如在pod中解除了對模組的依賴。 要避免情況一中的兩個報錯,最好是將註冊方法寫在本模組中,比如Module類的-[modInit:]方法中,這樣刪除模組的時候,也刪除了對應的註冊方法。

不管plist檔案中protocol和impClass是否存在,是否匹配,只要它們的key符合格式,就會被註冊成功。

4.2. 呼叫時異常

在呼叫模組時,首先需要建立impClass,一般是通過BeeHive類的-[createService:]方法,這個方法需要一個protocol

- (id)createService:(Protocol *)proto;
複製程式碼

建立好impClass的物件之後,然後這個物件呼叫protocol中宣告的方法。

在這個呼叫過程中,可能會遇到下列三種情況:

protocol未註冊 protocol已註冊,但對應impClass的類不存在 protocol已註冊,且對應impClass的類存在,但執行的方法沒實現
處理結果 將impClass的值設定為nil,如果是異常模式,則crash。 將impClass的值設定為nil 丟擲異常

4.3. 小結

在除錯階段時,可以開啟異常模式,這樣就能檢測一些潛在的問題出來,比如impClass沒有遵循protocol、使用未註冊的protocol來建立impClass。

關於異常處理,需要注意的是,impClass必須實現被呼叫的方法。另外,將註冊方法寫在本模組中。

相關文章