前言
使用BeeHive來進行專案元件化,其實是使用BeeHive來構建一箇中間層,通過中間層來解耦各個模組。在文章iOS元件化通用工具淺析有簡單介紹過BeeHive的一些元件化思路,本文將更多的從使用者的角度來分析BeeHive。
1. 用法
通過構建中間層來元件化專案,共需要三步:
- 建立protocol
- 建立impClass
- 儲存protocol-impClass對映關係
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:]
方法,將傳入的protocol
和impClass
新增到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.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 註冊時異常
註冊方式共有三種:
- 使用
BeeHive
類的-[registerService:service:]
- 使用巨集
BeeHiveService
- 使用plist檔案
註冊時,可能存在下列三種情況:
- protocol和impClass對應的協議或類不存在
- protocol和impClass存在,但impClass沒有遵循對應的protocol
- 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必須實現被呼叫的方法。另外,將註冊方法寫在本模組中。