AppDelegate模組化歷程

Ginhhor大帥發表於2019-03-06

原始碼地址: 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;
}
複製程式碼

這裡簡單說一下訊息轉發的流程。

  1. - (BOOL)respondsToSelector:(SEL)aSelector在呼叫協議方法前,會檢測物件是否實現協議方法,如果響應則會呼叫對應的方法。
  2. - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector當呼叫的方法無法找到時,如果未實現此方法,系統就會呼叫NSObject的doesNotRecognizeSelector方法,即丟擲異常並Crash。當實現了這個方法是,系統會要求返回selector的對應方法實現,這裡就可以開啟訊息轉發。
  3. - (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。目前只通過了文件約束,後續還會進行改進。

相關文章