AppDelegate模塊化歷程

源碼地址: GHApplicationMediatorgit

爲何AppDelegate不容易維護

AppDelegate控制着App的主要生命週期,好比App初始化完成後構建主視圖,App接收到遠程消息回調,Url-Scheme回調,第三方SDK初始化,數據庫初始化等等。github

基於這個緣由,隨着App的版本迭代,AppDelegate中的代碼量會愈來愈大。當AppDelegate的代碼量到達必定程度時,咱們就該開始考慮將AppDelegate中的代碼進行模塊化封裝。數據庫

1.0版本

在考慮這個方案的時候,咱們的項目剛剛度過了原型期,使用的SDK並很少,業務需求也尚未起來。瀏覽器

在這個背景,我選擇用Category封裝AppDelegate的方案。網絡

建立一個AppDelegate+XXX的Category,好比下面這個AppDelegate+CEReachabilityapp

#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文件的方法實現。ui

優勢:atom

  • 初步具有模塊化,不一樣模塊的註冊方法由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。目前只經過了文檔約束,後續還會進行改進。
相關文章
相關標籤/搜索