原始碼地址: GHApplicationMediator
為什麼AppDelegate不容易維護
AppDelegate控制著App的主要生命週期,比如App初始化完成後構建主檢視,App接收到遠端訊息回撥,Url-Scheme回撥,第三方SDK初始化,資料庫初始化等等。
基於這個原因,隨著App的版本迭代,AppDelegate中的程式碼量會越來越大。當AppDelegate的程式碼量到達一定程度時,我們就該開始考慮將AppDelegate中的程式碼進行模組化封裝。
1.0版本
在考慮這個方案的時候,我們的專案剛剛度過了原型期,使用的SDK並不多,業務需求也還沒有起來。
在這個背景,我選擇用Category封裝AppDelegate的方案。
建立一個AppDelegate+XXX的Category,比如下面這個AppDelegate+CEReachability
#import "AppDelegate.h"
@interface AppDelegate (CEReachability)
- (void)setupReachability;
@end
@implementation AppDelegate (CEReachability)
- (void)setupReachability
{
// Allocate a reachability object
Reachability *reach = [Reachability reachabilityWithHostname:kServerBaseUrl];
// Set the blocks
reach.reachableBlock = ^(Reachability *reach) {
if (reach.currentReachabilityStatus == ReachableViaWWAN) {
BLYLogInfo(@"ReachabilityStatusChangeBlock--->蜂窩資料網");
[CESettingsManager sharedInstance].needNoWifiAlert = YES;
} else if (reach.currentReachabilityStatus == ReachableViaWiFi) {
BLYLogInfo(@"ReachabilityStatusChangeBlock--->WiFi網路");
[CESettingsManager sharedInstance].needNoWifiAlert = NO;
}
};
reach.unreachableBlock = ^(Reachability *reach) {
BLYLogInfo(@"ReachabilityStatusChangeBlock--->未知網路狀態");
};
// Start the notifier, which will cause the reachability object to retain itself!
[reach startNotifier];
}
複製程式碼
然後在AppDelegate中註冊這個模組
#import "AppDelegate+CEReachability.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[self setupReachability];
return YES;
}
複製程式碼
有同學可能會問,為什麼不直接在Category中實現UIApplicationDelegate的方法。
同時import多個Category,並且多個Category都實現了同一個方法(例如 :- (void)applicationWillResignActive:(UIApplication *)application
),在呼叫該方法時選用哪個實現是由Category檔案的編譯順序來決定(在Build Phases > Complie Sources中指定),最後一個編譯的Category檔案的方法實現將被使用。與import順序無關,實際上,當有兩個Category實現了同一個方法,無論你imprt的是那個Category,方法的實際實現永遠是編譯順序在最後的Category檔案的方法實現。
優點:
- 初步具備模組化,不同模組的註冊方法由Category指定。
缺點:
- 各個Category之間是互斥關係,相同的方法不能在不同的Category中同時實現。
- 需要在AppDelegate中維護不同功能模組的實現邏輯。
2.0版本
隨著業務需求的增加,第三方支付、IM、各種URL-Scheme配置逐漸增加,特別是Open Url和Push Notifications需要有依賴關係,方案一很快就不能滿足需求了,各種奇怪的註冊方式交織在一起。
迫於求生欲,我決定第二次重構。
這次重構初始動機是由於Category之間的互斥關係,有依賴流程的流程就必須寫在AppDelegate中。(比如Open Url,第三方支付用到了,瀏覽器跳轉也用到了)
於是,我增加了ApplicationMediator來管理AppDelegate與模組的通訊,實現訊息轉發到模組的邏輯。
ApplicationMediator
ApplicationMediator是一個單例,用於管理模組的註冊與移除。
@interface CEApplicationMediator : UIResponder<UIApplicationDelegate, UNUserNotificationCenterDelegate>
@property (nonatomic, strong) NSHashTable *applicationModuleDelegates;
+ (instancetype)sharedInstance;
+ (void)registerAppilgationModuleDelegate:(id<UIApplicationDelegate>)moduleDelegate;
+ (void)registerNotificationModuleDelegate:(id<UIApplicationDelegate,UNUserNotificationCenterDelegate>)moduleDelegate;
+ (BOOL)removeModuleDelegateByClass:(Class)moduleClass;
@property (nonatomic, assign) UNNotificationPresentationOptions defaultNotificationPresentationOptions;
@end
複製程式碼
Module
模組根據需要實現UIApplicationDelegate與UNUserNotificationCenterDelegate就可以加入到UIApplication的生命週期中。
@implementation CEAMWindowDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
window.backgroundColor = [UIColor whiteColor];
// 需要將Window賦值給AppDelegate,有多時候會用全域性AppDelegate去獲取Window。
[UIApplication sharedApplication].delegate.window = window;
CELaunchPageViewController *launchVC = [[CELaunchPageViewController alloc] init];
window.rootViewController = launchVC;
[window makeKeyAndVisible];
return YES;
}
@end
複製程式碼
@implementation CEAMReachabilityDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Allocate a reachability object
Reachability *reach = [Reachability reachabilityWithHostname:kServerBaseUrl];
// Set the blocks
reach.reachableBlock = ^(Reachability *reach) {
if (reach.currentReachabilityStatus == ReachableViaWWAN) {
BLYLogInfo(@"ReachabilityStatusChangeBlock--->蜂窩資料網");
} else if (reach.currentReachabilityStatus == ReachableViaWiFi) {
BLYLogInfo(@"ReachabilityStatusChangeBlock--->WiFi網路");
}
};
reach.unreachableBlock = ^(Reachability *reach) {
BLYLogInfo(@"ReachabilityStatusChangeBlock--->未知網路狀態");
};
[reach startNotifier];
return YES;
}
@end
複製程式碼
模組註冊
當模組建立完成後,進行註冊後即可生效。
@implementation AppDelegate
+ (void)load
{
// CoreData
[CEApplicationMediator registerAppilgationModuleDelegate:[[CEAMCoreDataDelegate alloc] init]];
// ...
}
@end
複製程式碼
這裡有兩種方式進行註冊
- 在AppDelegate的+ (void)load中進行註冊
- 在ApplicationMediator的+ (void)load中進行註冊。
兩種方式都可以,各有利弊
- 在AppDelegate中註冊,delegate與AppDelegate耦合,但ApplicationMediator與delegate進行解耦,ApplicationMediator則可以作為元件抽離出來,作為中介軟體使用。
- 在ApplicationMediator中註冊,則與上面正好相反,這樣模組的維護就只需要圍繞ApplicationMediator進行,程式碼比較集中。
我採用的是AppDelegate中註冊的方式,主要是準備將ApplicationMediator作為元件使用。
訊息轉發
作為一個鍵盤俠,我的打字速度還是很快的,不出五分鐘我已經寫完了五個UIApplicationDelegate中主要生命週期函式的手動轉發,但是當我開啟UIApplicationDelegate標頭檔案後,我就矇蔽了,delegate的方法多到讓我頭皮發麻。
嗯,是的,所以訊息轉發機制就在這種時候排上了大用處。
AppDelegate
AppDelegate的所有方法都轉由ApplicationMediator處理,模組轉發邏輯後面介紹。
@implementation AppDelegate
+ (void)load
{
//註冊模組
}
- (BOOL)respondsToSelector:(SEL)aSelector
{
return [[CEApplicationMediator sharedInstance] respondsToSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
return [[CEApplicationMediator sharedInstance] methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
[[CEApplicationMediator sharedInstance] forwardInvocation:anInvocation];
}
@end
複製程式碼
這樣AppDelegate就只需要處理註冊模組就可以了。
ApplicationMediator
#pragma mark- Handle Method
/**
無法通過[super respondsToSelector:aSelector]來檢測物件是否從super繼承了方法。
因此呼叫[super respondsToSelector:aSelector],相當於呼叫了[self respondsToSelector:aSelector]
**/
- (BOOL)respondsToSelector:(SEL)aSelector
{
BOOL result = [super respondsToSelector:aSelector];
if (!result) {
result = [self hasDelegateRespondsToSelector:aSelector];
}
return result;
}
/**
此方法還被用於當NSInvocation被建立的時候,比如在訊息傳遞的時候。
如果當前Classf可以處理未被直接實現的方法,則必須覆寫此方法。
*/
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
id delegate = [self delegateRespondsToSelector:aSelector];
if (delegate) {
return [delegate methodSignatureForSelector:aSelector];
}
return [super methodSignatureForSelector:aSelector];
}
/**
無法識別的訊息處理
*/
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
__block BOOL isExec = NO;
NSMethodSignature *methodSignature = anInvocation.methodSignature;
const char *returnType = methodSignature.methodReturnType;
// 沒有返回值,或者預設返回YES
if (0 == strcmp(returnType, @encode(void)) ||
anInvocation.selector == @selector(application:didFinishLaunchingWithOptions:)) {
[self notifySelectorOfAllDelegates:anInvocation.selector nofityHandler:^(id delegate) {
[anInvocation invokeWithTarget:delegate];
isExec = YES;
}];
} else if (0 == strcmp(returnType, @encode(BOOL))) {
// 返回值為BOOL
[self notifySelectorOfAllDelegateUntilSuccessed:anInvocation.selector defaultReturnValue:NO nofityHandler:^BOOL(id delegate) {
[anInvocation invokeWithTarget:delegate];
// 獲得返回值
NSUInteger returnValueLenth = anInvocation.methodSignature.methodReturnLength;
BOOL *retValue = (BOOL *)malloc(returnValueLenth);
[anInvocation getReturnValue:retValue];
BOOL result = *retValue;
return result;
}];
} else {
// 等同於[self doesNotRecognizeSelector:anInvocation.selector];
[super forwardInvocation:anInvocation];
}
}
- (BOOL)hasDelegateRespondsToSelector:(SEL)selector
{
__block BOOL result = NO;
[self.applicationModuleDelegates enumerateObjectsUsingBlock:^(id _Nonnull delegate, NSUInteger idx, BOOL * _Nonnull stop) {
if ([delegate respondsToSelector:selector]) {
result = YES;
*stop = YES;
}
}];
return result;
}
- (id)delegateRespondsToSelector:(SEL)selector
{
__block id resultDelegate;
[self.applicationModuleDelegates enumerateObjectsUsingBlock:^(id _Nonnull delegate, NSUInteger idx, BOOL * _Nonnull stop) {
if ([delegate respondsToSelector:selector]) {
resultDelegate = delegate;
*stop = YES;
}
}];
return resultDelegate;
}
/**
通知所有delegate響應方法
@param selector 響應方法
@param nofityHandler delegated處理呼叫事件
*/
- (void)notifySelectorOfAllDelegates:(SEL)selector nofityHandler:(void(^)(id delegate))nofityHandler
{
if (_applicationModuleDelegates.count == 0) {
return;
}
[self.applicationModuleDelegates enumerateObjectsUsingBlock:^(id _Nonnull delegate, NSUInteger idx, BOOL * _Nonnull stop) {
if ([delegate respondsToSelector:selector]) {
if (nofityHandler) {
nofityHandler(delegate);
}
}
}];
}
/**
通知所有的delegate,當有delegate響應為成功後,中斷通知。
@param selector 響應方法
@param defaultReturnValue 預設返回值(當設定為YES時,即使沒有響應物件也會返回YES。)
@param nofityHandler delegate處理呼叫事件
@return delegate處理結果
*/
- (BOOL)notifySelectorOfAllDelegateUntilSuccessed:(SEL)selector defaultReturnValue:(BOOL)defaultReturnValue nofityHandler:(BOOL(^)(id delegate))nofityHandler
{
__block BOOL success = defaultReturnValue;
if (_applicationModuleDelegates.count == 0) {
return success;
}
[self.applicationModuleDelegates enumerateObjectsUsingBlock:^(id _Nonnull delegate, NSUInteger idx, BOOL * _Nonnull stop) {
if ([delegate respondsToSelector:selector]) {
if (nofityHandler) {
success = nofityHandler(delegate);
if (success) {
*stop = YES;
}
}
}
}];
return success;
}
複製程式碼
這裡簡單說一下訊息轉發的流程。
- (BOOL)respondsToSelector:(SEL)aSelector
在呼叫協議方法前,會檢測物件是否實現協議方法,如果響應則會呼叫對應的方法。- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
當呼叫的方法無法找到時,如果未實現此方法,系統就會呼叫NSObject的doesNotRecognizeSelector方法,即丟擲異常並Crash。當實現了這個方法是,系統會要求返回selector的對應方法實現,這裡就可以開啟訊息轉發。- (void)forwardInvocation:(NSInvocation *)anInvocation
當方法完成轉發設定後,會進入這個方法,由我們來控制方法的執行。
在步驟三裡,實現了自定義的轉發方案:
- 無返回值的delegate方法,以及
application:didFinishLaunchingWithOptions:
這種只返回YES的方法,轉發的時候,進行輪詢通知。 - BOOL返回值的delegate方法,先開啟輪詢通知,同時獲取每次執行的結果,當結果為YES時,表示有模組完成了處理,則結束輪詢。這裡需要注意的是,輪詢順序與註冊順序有關,需要注意註冊順序。
- 有completionHandler的方法,主要是推送訊息模組,由於competitionHandler只能呼叫一次,並且方法還沒有BOOL返回值,所以這類方法只能實現在ApplicationMediator中,每個方法手動轉發,具體實現請看原始碼。
還未開始的3.0版本
實現了2.0版本後,新增模組已經比較方便了,不過還有很多值得改進的地方。
- 比如在AppDelegate中註冊模組是根據程式碼的編寫順序來決定模組之間的依賴關係的,只能是單項依賴。實際使用過程中還是出現過由於依賴模組關係,導致初始化混亂的問題。設計的時候為了減少類繼承和協議繼承,用的都是系統現有的方案,後續可能會按照責任鏈的設計思路將這個元件設計的更完善。
- AppDelegate有一個預設的UIWindow,大量的第三方庫都通過
[UIApplication sharedApplication].delegate.window.bounds.size
來獲取螢幕尺寸,所以在建立或更改Window的時候,需要牢記將Window賦值給AppDelegate。目前只通過了文件約束,後續還會進行改進。