前言
BeeHive是阿里巴巴公司開源的一個iOS框架,這個框架是App模組化程式設計的框架一種實現方案,吸收了Spring框架Service的理念來實現模組間的API解耦。
BeeHive這個名字靈感來源於蜂窩。蜂窩是世界上高度模組化的工程結構,六邊形的設計能帶來無限擴張的可能。所以就用了這個名字作為開源專案的名字。
在前一篇文章iOS 元件化 —— 路由設計思路分析中,我們分析了App元件之間可以通過路由來解除耦合。那麼這篇文章就來看看利用模組化的思想如何解除耦合的。
(看到這裡一定會很多人有疑問,那就看看這篇文章元件和模組的區別)
說明:本文是基於BeeHive v1.2.0版本進行解析的。
目錄
- 1.BeeHive概述
- 2.BeeHive模組註冊
- 3.BeeHive模組事件
- 4.BeeHive模組呼叫
- 5.其他的一些輔助類
- 6.可能還在完善中的功能
一. BeeHive概述
由於BeeHive是基於Spring的Service理念,雖然可以使模組間的具體實現與介面解耦,但無法避免模組對介面類的依賴關係。
暫時BeeHive沒有采用invoke和performSelector:action withObject: params的方法。主要原因還是考慮學習成本難度以及動態呼叫實現無法在編譯檢查階段檢測介面引數變更等問題。
目前BeeHive v1.2.0 全部是利用Protocol的方式,實現了模組間解耦的目的:
1.各個模組以外掛的形式存在。每個都可獨立,相互解耦。 2.各個模組具體實現與介面呼叫分離 3.各個模組也有生命週期,也可以進行管理。
官方也給出了一個架構圖:
接下來就依次分析模組註冊,模組事件,模組呼叫是如何實現解耦的。
二. BeeHive模組註冊
先從模組的註冊開始分析,來看看BeeHive是如何給各個模組進行註冊的。
在BeeHive中是通過BHModuleManager來管理各個模組的。BHModuleManager中只會管理已經被註冊過的模組。
註冊Module的方式總共有三種:
1. Annotation方式註冊
通過BeeHiveMod巨集進行Annotation標記。
BeeHiveMod(ShopModule)
複製程式碼
BeeHiveMod巨集定義如下:
#define BeeHiveMod(name) \
char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name"";
複製程式碼
BeeHiveDATA又是一個巨集:
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
複製程式碼
最終BeeHiveMod巨集會在預編譯結束會完全展開成下面的樣子:
char * kShopModule_mod __attribute((used, section("__DATA,""BeehiveMods"" "))) = """ShopModule""";
複製程式碼
注意雙引號的總對數。
到這裡__attribute((used,section("segmentname,sectionname")))就需要先說明2個地方。
__attribute第一個引數used很有用。這個關鍵字是用來修飾函式的。被used修飾以後,意味著即使函式沒有被引用,在Release下也不會被優化。如果不加這個修飾,那麼Release環境連結器下會去掉沒有被引用的段。具體的描述可以看這個gun的官方文件。
Static靜態變數會按照他們申明的順序,放到一個單獨的段中。我們通過使用__attribute__((section("name")))來指明哪個段。資料則用__attribute__((used))來標記,防止連結器會優化刪除未被使用的段。
再來具體說說section的作用。
編譯器編譯原始碼後生成的檔案叫目標檔案,從檔案結構上來說,它已經是編譯後可執行的檔案格式,只是還沒有經過連結的過程。可執行檔案(Executable)主要是Windows下的PE(Portable Executable)和Linux的ELF(Executable Linkable Format),它們也都是COFF(Common file format)格式的變種。程式源程式程式碼被編譯之後會主要分成兩個段:程式指令和程式資料。程式碼段屬於程式指令,資料段和.bss段屬於資料段。
具體的例子見上圖,可見.data資料段裡面儲存的都是初始化過的全域性靜態變數和區域性靜態變數。.rodata段存放的是隻讀資料,一般都是const修飾的變數和字串常量。.bss段存放的是未初始化的全域性變數和區域性靜態變數。程式碼段就在.text段。
有時候我們需要指定一個特殊的段,來存放我們想要的資料。這裡我們就把資料存在data資料段裡面的"BeehiveMods"段中。
當然還有其他的Attributes的修飾關鍵字,詳情見官方文件
回到程式碼上來:
char * kShopModule_mod __attribute((used, section("__DATA,""BeehiveMods"" "))) = """ShopModule""";
複製程式碼
也就相當於:
char * kShopModule_mod = """ShopModule""";
複製程式碼
只不過是把kShopModule_mod字串放到了特殊的段裡面。
Module被這樣存到了特殊的段中,那怎麼取出來的呢?
static NSArray<NSString *>* BHReadConfiguration(char *section)
{
NSMutableArray *configs = [NSMutableArray array];
Dl_info info;
dladdr(BHReadConfiguration, &info);
#ifndef __LP64__
// const struct mach_header *mhp = _dyld_get_image_header(0); // both works as below line
const struct mach_header *mhp = (struct mach_header*)info.dli_fbase;
unsigned long size = 0;
// 找到之前儲存的資料段(Module找BeehiveMods段 和 Service找BeehiveServices段)的一片記憶體
uint32_t *memory = (uint32_t*)getsectiondata(mhp, "__DATA", section, & size);
#else /* defined(__LP64__) */
const struct mach_header_64 *mhp = (struct mach_header_64*)info.dli_fbase;
unsigned long size = 0;
uint64_t *memory = (uint64_t*)getsectiondata(mhp, "__DATA", section, & size);
#endif /* defined(__LP64__) */
// 把特殊段裡面的資料都轉換成字串存入陣列中
for(int idx = 0; idx < size/sizeof(void*); ++idx){
char *string = (char*)memory[idx];
NSString *str = [NSString stringWithUTF8String:string];
if(!str)continue;
BHLog(@"config = %@", str);
if(str) [configs addObject:str];
}
return configs;
}
複製程式碼
Dl_info是一個Mach-O裡面的一個資料結構。
typedef struct dl_info {
const char *dli_fname; /* Pathname of shared object */
void *dli_fbase; /* Base address of shared object */
const char *dli_sname; /* Name of nearest symbol */
void *dli_saddr; /* Address of nearest symbol */
} Dl_info;
複製程式碼
這個資料結構的資料預設就是通過
extern int dladdr(const void *, Dl_info *);
複製程式碼
dladdr這個函式來獲取Dl_info裡面的資料。
dli_fname:路徑名,例如
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
複製程式碼
dli_fbase:共享物件的的起始地址(Base address of shared object,比如上面的 CoreFoundation)
dli_saddr :符號的地址 dli_sname:符號的名字,即下面的第四列的函式資訊
Thread 0:
0 libsystem_kernel.dylib 0x11135810a __semwait_signal + 94474
1 libsystem_c.dylib 0x1110dab0b sleep + 518923
2 QYPerformanceMonitor 0x10dda4f1b -[ViewController tableView:cellForRowAtIndexPath:] + 7963
3 UIKit 0x10ed4d4f4 -[UITableView _createPreparedCellForGlobalRow:withIndexPath:willDisplay:] + 1586420
複製程式碼
通過呼叫這個static函式BHReadConfiguration,我們就可以拿到之前註冊到BeehiveMods特殊段裡面的各個Module的類名,都用字串裝在資料裡。
+ (NSArray<NSString *> *)AnnotationModules
{
static NSArray<NSString *> *mods = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
mods = BHReadConfiguration(BeehiveModSectName);
});
return mods;
}
複製程式碼
這是一個單例陣列,裡面裝的都是之前放在特殊段裡面的Module名字對應的字串陣列。
拿到這個陣列以後,就可以註冊所有的Module了。
- (void)registedAnnotationModules
{
NSArray<NSString *>*mods = [BHAnnotation AnnotationModules];
for (NSString *modName in mods) {
Class cls;
if (modName) {
cls = NSClassFromString(modName);
if (cls) {
[self registerDynamicModule:cls];
}
}
}
}
- (void)registerDynamicModule:(Class)moduleClass
{
[self addModuleFromObject:moduleClass];
}
複製程式碼
最後還需要把所有已經註冊的Module新增到BHModuleManager裡面。
- (void)addModuleFromObject:(id)object
{
Class class;
NSString *moduleName = nil;
if (object) {
class = object;
moduleName = NSStringFromClass(class);
} else {
return ;
}
if ([class conformsToProtocol:@protocol(BHModuleProtocol)]) {
NSMutableDictionary *moduleInfo = [NSMutableDictionary dictionary];
// basicModuleLevel 這個方法如果預設不實現,Level預設是Normal
BOOL responseBasicLevel = [class instancesRespondToSelector:@selector(basicModuleLevel)];
// Level是BHModuleNormal,就是1
int levelInt = 1;
// 如果實現了basicModuleLevel方法,那麼Level就是BHModuleBasic
if (responseBasicLevel) {
// Level是Basic,BHModuleBasic就是0
levelInt = 0;
}
// @"moduleLevel" 為Key,Level為Value
[moduleInfo setObject:@(levelInt) forKey:kModuleInfoLevelKey];
if (moduleName) {
// @"moduleClass"為Key,moduleName為Value
[moduleInfo setObject:moduleName forKey:kModuleInfoNameKey];
}
[self.BHModules addObject:moduleInfo];
}
}
複製程式碼
一些需要說明已經在上述程式碼裡面新增了註釋。BHModules是一個NSMutableArray,裡面存的都是一個個的字典,字典裡面有兩個Key,一個是@"moduleLevel",另一個是@"moduleClass"。儲存已經註冊的Module的時候都要判斷Level。還有一點需要說明的,所有需要註冊的Module必須遵循BHModuleProtocol協議,否則不能被儲存。
2. 讀取本地Pilst檔案
要讀取本地的Plist檔案之前,需要先設定好路徑。
[BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive";//可選,預設為BeeHive.bundle/BeeHive.plist
複製程式碼
BeeHive所有的配置都可以寫在BHContext進行傳遞。
Plist檔案的格式也要是陣列裡麵包一個個的字典。字典裡面有兩個Key,一個是@"moduleLevel",另一個是@"moduleClass"。注意根的陣列的名字叫@“moduleClasses”。
- (void)loadLocalModules
{
NSString *plistPath = [[NSBundle mainBundle] pathForResource:[BHContext shareInstance].moduleConfigName ofType:@"plist"];
if (![[NSFileManager defaultManager] fileExistsAtPath:plistPath]) {
return;
}
NSDictionary *moduleList = [[NSDictionary alloc] initWithContentsOfFile:plistPath];
NSArray *modulesArray = [moduleList objectForKey:kModuleArrayKey];
[self.BHModules addObjectsFromArray:modulesArray];
}
複製程式碼
從Plist裡面取出陣列,然後把陣列加入到BHModules陣列裡面。
3. Load方法註冊
最後一種註冊Module的方法就是在Load方法裡面註冊Module的類。
+ (void)load
{
[BeeHive registerDynamicModule:[self class]];
}
複製程式碼
呼叫BeeHive裡面的registerDynamicModule:完成Module的註冊。
+ (void)registerDynamicModule:(Class)moduleClass
{
[[BHModuleManager sharedManager] registerDynamicModule:moduleClass];
}
複製程式碼
BeeHive裡面的registerDynamicModule:的實現還是呼叫的BHModuleManager的註冊方法registerDynamicModule:
- (void)registerDynamicModule:(Class)moduleClass
{
[self addModuleFromObject:moduleClass];
}
複製程式碼
最後還是呼叫到了BHModuleManager裡面的addModuleFromObject:方法,這個方法上面分析過了,不再贅述。
Load方法還可以用一個巨集BH_EXPORT_MODULE來完成。
#define BH_EXPORT_MODULE(isAsync) \
+ (void)load { [BeeHive registerDynamicModule:[self class]]; } \
-(BOOL)async { return [[NSString stringWithUTF8String:#isAsync] boolValue];}
複製程式碼
BH_EXPORT_MODULE巨集裡面可以傳入一個引數,代表是否非同步載入Module模組,如果是YES就是非同步載入,如果是NO就是同步載入。
註冊的三種方式就完成了。最後BeeHive還會對這些Module的Class進行一下操作。
首先在BeeHive初始化setContext:的時候,會分別載入Modules和Services。這裡先談Modules。
-(void)setContext:(BHContext *)context
{
_context = context;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self loadStaticServices];
[self loadStaticModules];
});
}
複製程式碼
看看loadStaticModules方法裡面做了什麼事情。
- (void)loadStaticModules
{
// 讀取本地plist檔案裡面的Module,並註冊到BHModuleManager的BHModules陣列中
[[BHModuleManager sharedManager] loadLocalModules];
// 讀取特殊段裡面的標記資料,並註冊到BHModuleManager的BHModules陣列中
[[BHModuleManager sharedManager] registedAnnotationModules];
[[BHModuleManager sharedManager] registedAllModules];
}
複製程式碼
這裡雖然我們只看到了兩種方式,但是實際上BHModules陣列裡面還會包括通過Load方法註冊進來的Module。那麼BHModules陣列實際上是包含了3種註冊方式加進來的Module。
最後一步,registedAllModules比較關鍵。
- (void)registedAllModules
{
// 根絕優先順序從大到小進行排序
[self.BHModules sortUsingComparator:^NSComparisonResult(NSDictionary *module1, NSDictionary *module2) {
NSNumber *module1Level = (NSNumber *)[module1 objectForKey:kModuleInfoLevelKey];
NSNumber *module2Level = (NSNumber *)[module2 objectForKey:kModuleInfoLevelKey];
return [module1Level intValue] > [module2Level intValue];
}];
NSMutableArray *tmpArray = [NSMutableArray array];
//module init
[self.BHModules enumerateObjectsUsingBlock:^(NSDictionary *module, NSUInteger idx, BOOL * _Nonnull stop) {
NSString *classStr = [module objectForKey:kModuleInfoNameKey];
Class moduleClass = NSClassFromString(classStr);
if (NSStringFromClass(moduleClass)) {
// 初始化所有的Module
id<BHModuleProtocol> moduleInstance = [[moduleClass alloc] init];
[tmpArray addObject:moduleInstance];
}
}];
[self.BHModules removeAllObjects];
[self.BHModules addObjectsFromArray:tmpArray];
}
複製程式碼
BHModules陣列在進行registedAllModules方法之前,裝的都是一個個的字典,再執行完registedAllModules方法之後,裡面裝的就都是一個個的Module的例項了。
registedAllModules方法會先按照Level的優先順序從大到小進行排序,然後再按照這個順序依次初始化所有的Module的例項,存入陣列中。最終BHModules陣列裡面裝的是所有的Module例項物件。
注意,這裡有兩點需要額外說明:
- 限制住了所有的Module的物件都要是遵守BHModuleProtocol協議的。至於為何要遵守BHModuleProtocol協議,下一章節會有詳細說明。
- Module不能在任何其他地方alloc建立出來,即使建立一個新的Module例項出來,它也並不在BHModuleManager的管理下,是無法接收BHModuleManager分發的系統事件,建立出來是沒有任何意義的。
三. BeeHive模組事件
BeeHive會給每個模組提供生命週期事件,用於與BeeHive宿主環境進行必要資訊互動,感知模組生命週期的變化。
BeeHive各個模組會收到一些事件。在BHModuleManager中,所有的事件被定義成了BHModuleEventType列舉。
typedef NS_ENUM(NSInteger, BHModuleEventType)
{
BHMSetupEvent = 0,
BHMInitEvent,
BHMTearDownEvent,
BHMSplashEvent,
BHMQuickActionEvent,
BHMWillResignActiveEvent,
BHMDidEnterBackgroundEvent,
BHMWillEnterForegroundEvent,
BHMDidBecomeActiveEvent,
BHMWillTerminateEvent,
BHMUnmountEvent,
BHMOpenURLEvent,
BHMDidReceiveMemoryWarningEvent,
BHMDidFailToRegisterForRemoteNotificationsEvent,
BHMDidRegisterForRemoteNotificationsEvent,
BHMDidReceiveRemoteNotificationEvent,
BHMDidReceiveLocalNotificationEvent,
BHMWillContinueUserActivityEvent,
BHMContinueUserActivityEvent,
BHMDidFailToContinueUserActivityEvent,
BHMDidUpdateUserActivityEvent,
BHMDidCustomEvent = 1000
};
複製程式碼
上面BHModuleEventType列舉主要分為三種,一種是系統事件,另外一種是應用事件,最後一種是業務自定義事件。
1. 系統事件。
上圖是官方給出的一個系統事件基本工作流。
系統事件通常是Application生命週期事件,例如DidBecomeActive、WillEnterBackground等。
一般做法是把BHAppDelegate接管原來的AppDelegate。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[[BHModuleManager sharedManager] triggerEvent:BHMSetupEvent];
[[BHModuleManager sharedManager] triggerEvent:BHMInitEvent];
dispatch_async(dispatch_get_main_queue(), ^{
[[BHModuleManager sharedManager] triggerEvent:BHMSplashEvent];
});
return YES;
}
#if __IPHONE_OS_VERSION_MAX_ALLOWED > 80400
-(void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL))completionHandler
{
[[BHModuleManager sharedManager] triggerEvent:BHMQuickActionEvent];
}
#endif
- (void)applicationWillResignActive:(UIApplication *)application
{
[[BHModuleManager sharedManager] triggerEvent:BHMWillResignActiveEvent];
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
[[BHModuleManager sharedManager] triggerEvent:BHMDidEnterBackgroundEvent];
}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
[[BHModuleManager sharedManager] triggerEvent:BHMWillEnterForegroundEvent];
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
[[BHModuleManager sharedManager] triggerEvent:BHMDidBecomeActiveEvent];
}
- (void)applicationWillTerminate:(UIApplication *)application
{
[[BHModuleManager sharedManager] triggerEvent:BHMWillTerminateEvent];
}
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
[[BHModuleManager sharedManager] triggerEvent:BHMOpenURLEvent];
return YES;
}
#if __IPHONE_OS_VERSION_MAX_ALLOWED > 80400
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString *,id> *)options
{
[[BHModuleManager sharedManager] triggerEvent:BHMOpenURLEvent];
return YES;
}
#endif
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application
{
[[BHModuleManager sharedManager] triggerEvent:BHMDidReceiveMemoryWarningEvent];
}
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
[[BHModuleManager sharedManager] triggerEvent:BHMDidFailToRegisterForRemoteNotificationsEvent];
}
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
[[BHModuleManager sharedManager] triggerEvent:BHMDidRegisterForRemoteNotificationsEvent];
}
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
[[BHModuleManager sharedManager] triggerEvent:BHMDidReceiveRemoteNotificationEvent];
}
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
[[BHModuleManager sharedManager] triggerEvent:BHMDidReceiveRemoteNotificationEvent];
}
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification
{
[[BHModuleManager sharedManager] triggerEvent:BHMDidReceiveLocalNotificationEvent];
}
#if __IPHONE_OS_VERSION_MAX_ALLOWED > 80000
- (void)application:(UIApplication *)application didUpdateUserActivity:(NSUserActivity *)userActivity
{
if([UIDevice currentDevice].systemVersion.floatValue > 8.0f){
[[BHModuleManager sharedManager] triggerEvent:BHMDidUpdateUserActivityEvent];
}
}
- (void)application:(UIApplication *)application didFailToContinueUserActivityWithType:(NSString *)userActivityType error:(NSError *)error
{
if([UIDevice currentDevice].systemVersion.floatValue > 8.0f){
[[BHModuleManager sharedManager] triggerEvent:BHMDidFailToContinueUserActivityEvent];
}
}
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler
{
if([UIDevice currentDevice].systemVersion.floatValue > 8.0f){
[[BHModuleManager sharedManager] triggerEvent:BHMContinueUserActivityEvent];
}
return YES;
}
- (BOOL)application:(UIApplication *)application willContinueUserActivityWithType:(NSString *)userActivityType
{
if([UIDevice currentDevice].systemVersion.floatValue > 8.0f){
[[BHModuleManager sharedManager] triggerEvent:BHMWillContinueUserActivityEvent];
}
return YES;
}
複製程式碼
這樣所有的系統事件都可以通過呼叫BHModuleManager的triggerEvent:來處理。
在BHModuleManager中有2個事件很特殊,一個是BHMInitEvent,一個是BHMTearDownEvent。
先來說說BHMInitEvent事件。
- (void)handleModulesInitEvent
{
[self.BHModules enumerateObjectsUsingBlock:^(id<BHModuleProtocol> moduleInstance, NSUInteger idx, BOOL * _Nonnull stop) {
__weak typeof(&*self) wself = self;
void ( ^ bk )();
bk = ^(){
__strong typeof(&*self) sself = wself;
if (sself) {
if ([moduleInstance respondsToSelector:@selector(modInit:)]) {
[moduleInstance modInit:[BHContext shareInstance]];
}
}
};
[[BHTimeProfiler sharedTimeProfiler] recordEventTime:[NSString stringWithFormat:@"%@ --- modInit:", [moduleInstance class]]];
if ([moduleInstance respondsToSelector:@selector(async)]) {
BOOL async = [moduleInstance async];
if (async) {
dispatch_async(dispatch_get_main_queue(), ^{
bk();
});
} else {
bk();
}
} else {
bk();
}
}];
}
複製程式碼
Init事件就是初始化Module模組的事件。遍歷BHModules陣列,依次對每個Module例項呼叫modInit:方法。這裡會有非同步載入的問題。如果moduleInstance重寫了async方法,那麼就會根據這個方法返回的值來進行是否非同步載入的判斷。
modInit:方法裡面幹很多事情。比如說對環境的判斷,根據環境的不同初始化不同的方法。
-(void)modInit:(BHContext *)context
{
switch (context.env) {
case BHEnvironmentDev:
//....初始化開發環境
break;
case BHEnvironmentProd:
//....初始化生產環境
default:
break;
}
}
複製程式碼
再比如在初始化的時候註冊一些協議:
-(void)modInit:(BHContext *)context
{
[[BeeHive shareInstance] registerService:@protocol(UserTrackServiceProtocol) service:[BHUserTrackViewController class]];
}
複製程式碼
總之這裡可以幹一些初始化需要做的事情。
再來說說BHMTearDownEvent事件。這個事件是拆除Module的。
- (void)handleModulesTearDownEvent
{
//Reverse Order to unload
for (int i = (int)self.BHModules.count - 1; i >= 0; i--) {
id<BHModuleProtocol> moduleInstance = [self.BHModules objectAtIndex:i];
if (moduleInstance && [moduleInstance respondsToSelector:@selector(modTearDown:)]) {
[moduleInstance modTearDown:[BHContext shareInstance]];
}
}
}
複製程式碼
由於Module是有優先順序Level,所以拆除的時候需要從低優先順序開始拆,即陣列逆序迴圈。對每個Module例項傳送modTearDown:事件即可。
2. 應用事件
官方給出的應用事件工作流如上:
在系統事件的基礎之上,擴充套件了應用的通用事件,例如modSetup、modInit等,可以用於編碼實現各外掛模組的設定與初始化。
所有的事件都可以通過呼叫BHModuleManager的triggerEvent:來處理。
- (void)triggerEvent:(BHModuleEventType)eventType
{
switch (eventType) {
case BHMSetupEvent:
[self handleModuleEvent:kSetupSelector];
break;
case BHMInitEvent:
//special
[self handleModulesInitEvent];
break;
case BHMTearDownEvent:
//special
[self handleModulesTearDownEvent];
break;
case BHMSplashEvent:
[self handleModuleEvent:kSplashSeletor];
break;
case BHMWillResignActiveEvent:
[self handleModuleEvent:kWillResignActiveSelector];
break;
case BHMDidEnterBackgroundEvent:
[self handleModuleEvent:kDidEnterBackgroundSelector];
break;
case BHMWillEnterForegroundEvent:
[self handleModuleEvent:kWillEnterForegroundSelector];
break;
case BHMDidBecomeActiveEvent:
[self handleModuleEvent:kDidBecomeActiveSelector];
break;
case BHMWillTerminateEvent:
[self handleModuleEvent:kWillTerminateSelector];
break;
case BHMUnmountEvent:
[self handleModuleEvent:kUnmountEventSelector];
break;
case BHMOpenURLEvent:
[self handleModuleEvent:kOpenURLSelector];
break;
case BHMDidReceiveMemoryWarningEvent:
[self handleModuleEvent:kDidReceiveMemoryWarningSelector];
break;
case BHMDidReceiveRemoteNotificationEvent:
[self handleModuleEvent:kDidReceiveRemoteNotificationsSelector];
break;
case BHMDidFailToRegisterForRemoteNotificationsEvent:
[self handleModuleEvent:kFailToRegisterForRemoteNotificationsSelector];
break;
case BHMDidRegisterForRemoteNotificationsEvent:
[self handleModuleEvent:kDidRegisterForRemoteNotificationsSelector];
break;
case BHMDidReceiveLocalNotificationEvent:
[self handleModuleEvent:kDidReceiveLocalNotificationsSelector];
break;
case BHMWillContinueUserActivityEvent:
[self handleModuleEvent:kWillContinueUserActivitySelector];
break;
case BHMContinueUserActivityEvent:
[self handleModuleEvent:kContinueUserActivitySelector];
break;
case BHMDidFailToContinueUserActivityEvent:
[self handleModuleEvent:kFailToContinueUserActivitySelector];
break;
case BHMDidUpdateUserActivityEvent:
[self handleModuleEvent:kDidUpdateContinueUserActivitySelector];
break;
case BHMQuickActionEvent:
[self handleModuleEvent:kQuickActionSelector];
break;
default:
[BHContext shareInstance].customEvent = eventType;
[self handleModuleEvent:kAppCustomSelector];
break;
}
}
複製程式碼
從上述程式碼可以看出,除去BHMInitEvent初始化事件和BHMTearDownEvent拆除Module事件這兩個特殊事件以外,所有的事件都是呼叫的handleModuleEvent:方法。上述的switch-case裡面,除去系統事件以外的,和default裡面的customEvent以外,剩下的事件都是BHMTearDownEvent事件。
- (void)handleModuleEvent:(NSString *)selectorStr
{
SEL seletor = NSSelectorFromString(selectorStr);
[self.BHModules enumerateObjectsUsingBlock:^(id<BHModuleProtocol> moduleInstance, NSUInteger idx, BOOL * _Nonnull stop) {
if ([moduleInstance respondsToSelector:seletor]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[moduleInstance performSelector:seletor withObject:[BHContext shareInstance]];
#pragma clang diagnostic pop
[[BHTimeProfiler sharedTimeProfiler] recordEventTime:[NSString stringWithFormat:@"%@ --- %@", [moduleInstance class], NSStringFromSelector(seletor)]];
}
}];
}
複製程式碼
handleModuleEvent:方法的實現就是遍歷BHModules陣列,呼叫performSelector:withObject:方法實現對應方法呼叫。
注意這裡所有的Module必須是遵循BHModuleProtocol的,否則無法接收到這些事件的訊息。
3. 業務自定義事件
如果覺得系統事件、通用事件不足以滿足需要,我們還將事件封裝簡化成BHAppdelgate,你可以通過繼承 BHAppdelegate來擴充套件自己的事件。
自定義的事件的type就是BHMDidCustomEvent = 1000 。
在BeeHive裡面有一個tiggerCustomEvent:方法就是用來處理這些事件的,尤其是處理自定義事件的。
- (void)tiggerCustomEvent:(NSInteger)eventType
{
if(eventType < 1000) {
return;
}
[[BHModuleManager sharedManager] triggerEvent:eventType];
}
複製程式碼
這個方法只會把自定義事件透傳給BHModuleManager進行處理,其他一切的事件都不會做任何相應。
四. BeeHive模組呼叫
在BeeHive中是通過BHServiceManager來管理各個Protocol的。BHServiceManager中只會管理已經被註冊過的Protocol。
註冊Protocol的方式總共有三種,和註冊Module是一樣一一對應的:
1. Annotation方式註冊
通過BeeHiveService巨集進行Annotation標記。
BeeHiveService(HomeServiceProtocol,BHViewController)
複製程式碼
BeeHiveService巨集定義如下:
#define BeeHiveService(servicename,impl) \
char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}";
複製程式碼
BeeHiveDATA又是一個巨集:
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
複製程式碼
最終BeeHiveService巨集會在預編譯結束會完全展開成下面的樣子:
char * kHomeServiceProtocol_service __attribute((used, section("__DATA,""BeehiveServices"" "))) = "{ \"""HomeServiceProtocol""\" : \"""BHViewController""\"}";
複製程式碼
這裡類比註冊Module,也是把資料存在特殊的段內,具體原理上面已經分析過了,這裡不再贅述。
同理,通過呼叫static函式BHReadConfiguration,我們就可以拿到之前註冊到BeehiveServices特殊段裡面的各個Protocol協議對應Class字典的字串。
"{ \"HomeServiceProtocol\" : \"BHViewController\"}"
複製程式碼
陣列裡面存的都是這樣的一些Json字串。
+ (NSArray<NSString *> *)AnnotationServices
{
static NSArray<NSString *> *services = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
services = BHReadConfiguration(BeehiveServiceSectName);
});
return services;
}
複製程式碼
這是一個單例陣列,裡面裝的都是之前放在特殊段裡面的Protocol協議對應Class字典的字串陣列,即為Json字串陣列。
拿到這個陣列以後,就可以註冊所有的Protocol協議了。
- (void)registerAnnotationServices
{
NSArray<NSString *>*services = [BHAnnotation AnnotationServices];
for (NSString *map in services) {
NSData *jsonData = [map dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (!error) {
if ([json isKindOfClass:[NSDictionary class]] && [json allKeys].count) {
NSString *protocol = [json allKeys][0];
NSString *clsName = [json allValues][0];
if (protocol && clsName) {
[self registerService:NSProtocolFromString(protocol) implClass:NSClassFromString(clsName)];
}
}
}
}
}
複製程式碼
由於services陣列裡面存的都是Json字串,所以先轉換成字典,然後再依次取出protocol和className。最後呼叫registerService:implClass:方法。
- (void)registerService:(Protocol *)service implClass:(Class)implClass
{
NSParameterAssert(service != nil);
NSParameterAssert(implClass != nil);
// impClass 是否遵循了 Protocol 協議
if (![implClass conformsToProtocol:service] && self.enableException) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"%@ module does not comply with %@ protocol", NSStringFromClass(implClass), NSStringFromProtocol(service)] userInfo:nil];
}
// Protocol 協議是否已經註冊過了
if ([self checkValidService:service] && self.enableException) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"%@ protocol has been registed", NSStringFromProtocol(service)] userInfo:nil];
}
NSMutableDictionary *serviceInfo = [NSMutableDictionary dictionary];
[serviceInfo setObject:NSStringFromProtocol(service) forKey:kService];
[serviceInfo setObject:NSStringFromClass(implClass) forKey:kImpl];
[self.lock lock];
[self.allServices addObject:serviceInfo];
[self.lock unlock];
}
複製程式碼
在註冊registerService:implClass:之前會有2個檢查,一是檢查impClass 是否遵循了 Protocol 協議,二是檢查Protocol 協議是否已經註冊過了。如果有一個檢查出現問題,都會丟擲異常。
如果檢查都過了,那麼就加入Key為@"service"的,Value為Protocol的名字,和Key為@“impl”的,Value為Class名字的兩個鍵值對。最後把這個字典存入allServices陣列中。
在儲存allServices陣列的時候,是要加鎖的。這裡的lock是NSRecursiveLock。防止出現遞迴引起的執行緒安全問題。
2. 讀取本地Pilst檔案
要讀取本地的Plist檔案之前,需要先設定好路徑。
[BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";
複製程式碼
BeeHive所有的配置都可以寫在BHContext進行傳遞。
Plist檔案的格式也要是陣列裡麵包一個個的字典。字典裡面有兩個Key,一個是@"service",另一個是@"impl"。
- (void)registerLocalServices
{
NSString *serviceConfigName = [BHContext shareInstance].serviceConfigName;
NSString *plistPath = [[NSBundle mainBundle] pathForResource:serviceConfigName ofType:@"plist"];
if (!plistPath) {
return;
}
NSArray *serviceList = [[NSArray alloc] initWithContentsOfFile:plistPath];
[self.lock lock];
[self.allServices addObjectsFromArray:serviceList];
[self.lock unlock];
}
複製程式碼
從Plist裡面取出陣列,然後把陣列加入到allServices陣列裡面。
3. Load方法註冊
最後一種註冊Protocol的方法就是在Load方法裡面註冊Protocol協議。
+ (void)load
{
[[BeeHive shareInstance] registerService:@protocol(UserTrackServiceProtocol) service:[BHUserTrackViewController class]];
}
複製程式碼
呼叫BeeHive裡面的registerService:service:完成Module的註冊。
- (void)registerService:(Protocol *)proto service:(Class) serviceClass
{
[[BHServiceManager sharedManager] registerService:proto implClass:serviceClass];
}
複製程式碼
BeeHive裡面的registerService:service:的實現還是呼叫的BHServiceManager的註冊方法registerService:implClass:。這個方法上面分析過了,不再贅述。
至此,3種註冊Protocol的方式就完成了。
在之前分析註冊Module的時候,我們知道在BeeHive在setContext:的時候會呼叫loadStaticServices方法。
-(void)loadStaticServices
{
// 是否開啟異常檢測
[BHServiceManager sharedManager].enableException = self.enableException;
// 讀取本地plist檔案裡面的Protocol,並註冊到BHServiceManager的allServices陣列中
[[BHServiceManager sharedManager] registerLocalServices];
// 讀取特殊段裡面的標記資料,並註冊到BHServiceManager的allServices陣列中
[[BHServiceManager sharedManager] registerAnnotationServices];
}
複製程式碼
這裡雖然我們只看到了兩種方式,但是實際上allServices陣列裡面還會包括通過Load方法註冊進來的Protocol。那麼allServices陣列實際上是包含了3種註冊方式加進來的Protocol。
這裡就沒有註冊Module的最後一步初始化例項的過程。
但是Protocol比Module多一個方法,返回能相應Protocol例項物件的方法。
在BeeHive中有這樣一個方法,呼叫這個方法就可以返回一個能相應Protocol的例項物件。
- (id)createService:(Protocol *)proto;
- (id)createService:(Protocol *)proto;
{
return [[BHServiceManager sharedManager] createService:proto];
}
複製程式碼
實質是呼叫了BHServiceManager的createService:方法。createService:方法具體實現如下:
- (id)createService:(Protocol *)service
{
id implInstance = nil;
// Protocol 協議是否已經註冊過了
if (![self checkValidService:service] && self.enableException) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"%@ protocol does not been registed", NSStringFromProtocol(service)] userInfo:nil];
}
Class implClass = [self serviceImplClass:service];
if ([[implClass class] respondsToSelector:@selector(shareInstance)])
implInstance = [[implClass class] shareInstance];
else
implInstance = [[implClass alloc] init];
if (![implInstance respondsToSelector:@selector(singleton)]) {
return implInstance;
}
NSString *serviceStr = NSStringFromProtocol(service);
// 是否需要快取
if ([implInstance singleton]) {
id protocol = [[BHContext shareInstance] getServiceInstanceFromServiceName:serviceStr];
if (protocol) {
return protocol;
} else {
[[BHContext shareInstance] addServiceWithImplInstance:implInstance serviceName:serviceStr];
}
} else {
[[BHContext shareInstance] addServiceWithImplInstance:implInstance serviceName:serviceStr];
}
return implInstance;
}
複製程式碼
這個方法也會先檢查Protocol協議是否是註冊過的。然後接著取出字典裡面對應的Class,如果實現了shareInstance方法,那麼就生成一個單例出來,如果沒有,那麼就隨便生成一個物件出來。如果還實現了singleton,就能進一步的把implInstance和serviceStr對應的加到BHContext的servicesByName字典裡面快取起來。這樣就可以隨著上下文傳遞了。
id<UserTrackServiceProtocol> v4 = [[BeeHive shareInstance] createService:@protocol(UserTrackServiceProtocol)];
if ([v4 isKindOfClass:[UIViewController class]]) {
[self registerViewController:(UIViewController *)v4 title:@"埋點3" iconName:nil];
}
複製程式碼
上面是官方給的例子,Module之間的呼叫就用這種方式,就可以得到很好的解耦了。
五. 其他的一些輔助類
還有一些輔助類,在上面沒有提到的,這裡就來一個彙總,一起分析了。
BHConfig這也是一個單例,裡面儲存了一個config的NSMutableDictionary字典。字典維護了一些動態的環境變數,作為BHContext的補充存在。
BHContext也是一個單例,裡面有2個NSMutableDictionary字典,一個是modulesByName,另一個是servicesByName。BHContext主要就是用來儲存各種上下文環境的。
@interface BHContext : NSObject
//global env
@property(nonatomic, assign) BHEnvironmentType env;
//global config
@property(nonatomic, strong) BHConfig *config;
//application appkey
@property(nonatomic, strong) NSString *appkey;
//customEvent>=1000
@property(nonatomic, assign) NSInteger customEvent;
@property(nonatomic, strong) UIApplication *application;
@property(nonatomic, strong) NSDictionary *launchOptions;
@property(nonatomic, strong) NSString *moduleConfigName;
@property(nonatomic, strong) NSString *serviceConfigName;
//3D-Touch model
#if __IPHONE_OS_VERSION_MAX_ALLOWED > 80400
@property (nonatomic, strong) BHShortcutItem *touchShortcutItem;
#endif
//OpenURL model
@property (nonatomic, strong) BHOpenURLItem *openURLItem;
//Notifications Remote or Local
@property (nonatomic, strong) BHNotificationsItem *notificationsItem;
//user Activity Model
@property (nonatomic, strong) BHUserActivityItem *userActivityItem;
@end
複製程式碼
在application:didFinishLaunchingWithOptions:的時候,就可以初始化大量的上下文資訊。
[BHContext shareInstance].application = application;
[BHContext shareInstance].launchOptions = launchOptions;
[BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive";//可選,預設為BeeHive.bundle/BeeHive.plist
[BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";
複製程式碼
BHTimeProfiler就是用來進行計算時間效能方面的Profiler。
BHWatchDog是可以開一個執行緒,設定好handler,每隔一段時間就執行一個handler。
六. 可能還在完善中的功能
BeeHive通過處理Event編寫各個業務模組可以實現外掛化程式設計,各業務模組之間沒有任何依賴,core與module之間通過event互動,實現了外掛隔離。但有時候需要模組間的相互呼叫某些功能來協同完成功能。
1. 功能還有待完善
通常會有三種形式的介面訪問形式:
- 基於介面的實現Service訪問方式(Java spring框架實現)
- 基於函式呼叫約定實現的Export Method(PHP的extension,ReactNative的擴充套件機制)
- 基於跨應用實現的URL Route模式(iPhone App之間的互訪)
BeeHive目前只實現了第一種方式,後兩種方式還需要繼續完善。
2. 解耦還不夠徹底
基於介面Service訪問的優點是可以編譯時檢查發現介面的變更,從而及時修正介面問題。缺點是需要依賴介面定義的標頭檔案,通過模組增加得越多,維護介面定義的也有一定工作量。
3. 設計思路還可以繼續改進和優化
BHServiceManager內部維護了一個陣列,陣列中的一個個字典,Key為@"service"的,Value為Protocol的名字,和Key為@“impl”的,Value為Class名字的兩個鍵值對。與其這樣設計,還不如直接使用NSMutableDictionary,Key使用Protocol,Value為Class呢?搜尋的時候減少了手動迴圈過程。
結尾
BeeHive作為阿里開源的一套模組間的解耦方案,思路還是很值得我們學習的。目前版本是v1.2.0,相信在後面的版本迭代更新中,功能會更加的完善,做法會更加的優雅,值得期待!
如有任何智慧財產權、版權問題或理論錯誤,還請指正。
轉載請註明原作者及以上資訊。