iOS - 更輕量級的 AppDelegate - 面向服務設計

有沒有以爲你的 AppDelegate 太過龐大了?一個 iOS
應用可能集成了大量的服務,第三方服務、推送服務等等,大多數服務功能彼此獨立,想不想把它們完全從 AppDelegate 中拆出來?git

clipboard.png

AppDelegate 作了太多事

AppDelegate 並不遵循單一功能原則,它要負責處理不少事情,如應用生命週期回調、遠程推送、本地推送、應用跳轉(HandleOpenURL);若是集成了第三方服務,大多數還須要在應用啓動時初始化,而且須要處理應用跳轉,若是在 AppDelegate 中作這些事情,勢必讓它變得很龐大。github

不一樣服務的代碼糾纏在一塊兒,使得 AppDelegate 變得很難複用。並且若是你想要添加一個服務或者關閉一個服務,都須要去修改 AppDelegate。不少服務看起來互相獨立,並不依賴其它服務,咱們能夠把它們拆分出來,放在單獨的文件裏。微信

要實現的目標

這個面向服務,應該達成下面這兩個要求:架構

  1. 添加或者刪除一個服務的時候,不須要更改 AppDelegate 中的任何一行代碼。app

  2. AppDelegate 不實現 UIApplicationDelegate 協議中的方法,由協議去實現函數

第一點是要求實現可插拔特性。關於第二點,可能比較粗暴簡單的作法是在 AppDelegate 裏面實現全部的 UIApplicationDelegate 代理方法,而後在方法中把消息轉發給消息。這種作法有一些弊端:fetch

  1. 很明顯,AppDelegate 顯得比較笨重。ui

  2. 被空的代理實現綁架。有一些代理方法實現之後,須要在 Info.plist 中聲明支持相應的功能的,好比 backgroud remote notifications,不然可能會在控制檯看到下面的日誌:atom

    You've implemented -[<UIApplicationDelegate> application:didReceiveRemoteNotification:fetchCompletionHandler:], 
    but you still need to add "remote-notification" to the list of 
    your supported UIBackgroundModes in your Info.plist.
  3. 收到警告郵件。應用上架時蘋果還會檢查這些代理方法,好比遠程推送。假如 AppDelegate 實現了遠程推送相關的代理方法,可是並無調用註冊遠程推送的方法,也沒有申請推送證書,可能就會收到一封警告郵件。spa

既然實現全部的代理方法就只是爲了轉發消息,那有沒有方法可以聚合這些消息呢?答案是,有,具體實現請看下文。

如何實現?

+load

iOS 應用程序在執行 main 方法以前,還作了不少事情,其中包括加載類。一個類在被加載時,它的 +load 方法會被調用。重寫每一個 Service 類 +load 方法,這個方法執行時註冊 Service。那服務要如何實現,如何啓動呢?

-respondsToSelector:

一般狀況下,你須要在 AppDelegate 中實現每個須要用到的代理方法,在這些代理方法中,調用不少不一樣的服務。可是上面第二點對咱們提出要求:只能由各個服務去實現它須要的代理方法。這裏我利用了 Objective-C 的消息轉發機制,把 AppDelegate 不能處理的消息轉發給各個服務。

每個代理方法被調用前,調用者會先調用 -respondsToSelector:,檢查代理能不能響應這個方法,AppDelegate 也不例外。咱們能夠重寫 -respondsToSelector:,告訴調用者 AppDelegate 能夠響應這個方法,但實際上 AppDelegate 並無實現這個方法。

-forwardInvocation:

接下來,調用者就會調用這個並無實現的代理方法,而後進入消息轉發流程,調用 -forwardInvocation: 方法。在這個方法中,咱們能夠把這個消息轉發到實現了對應代理方法的 Service 對象上。

重寫 - (void)forwardInvocation:(NSInvocation *)anInvocation 這個方法,咱們就能夠在全部實現了 UIApplicationDelegate 協議方法的 Service 對象上執行被調用的代理方法。這樣 AppDelegate 就再也不須要真正實現 UIApplicationDelegate 協議裏的方法了。

上代碼

好了,無論怎麼說,都要落實到代碼上。爲了方便理解,我去掉了不少錯誤檢查代碼。具體實現和示例請看 github 上的這個版本

首先是 MLSOAppDelegate.h

#import <UIKit/UIKit.h>

@protocol MLAppService <UIApplicationDelegate>
@required
- (NSString *)serviceName;
@end

@interface MLSOAppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
+ (void)registerService:(id<MLAppService>)service
@end

而後是 MLSOAppDelegate.m。判斷 Service 對象可否響應代理方法的依據是,能獲取到方法的真正的實現(IMP)。由於消息轉發機制的存在,獲取一個沒真正實現的方法的 IMP 的時候,會獲得 _objc_msgForward 這個函數,所以咱們須要排除它。

@implementation MLSOAppDelegate

- (BOOL)respondsToSelector:(SEL)aSelector {
    __block IMP imp = [self methodForSelector:aSelector];
    BOOL canResponse = (imp != NULL && imp != _objc_msgForward);
    if (! canResponse) {
        [_servicesMap enumerateKeysAndObjectsUsingBlock:
        ^(NSString * _Nonnull key, id<MLAppService> _Nonnull obj, BOOL * _Nonnull stop) 
        {
            if ([obj respondsToSelector:aSelector]) {
                imp = [(id)obj methodForSelector:aSelector];
                *stop = YES;
            }
        }];
        canResponse = (imp != NULL && imp != _objc_msgForward);
    }
    return canResponse;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    [self.servicesMap enumerateKeysAndObjectsUsingBlock:
    ^(NSString * _Nonnull key, id<MLAppService> _Nonnull service, BOOL * _Nonnull stop) 
    {
        if ( ! [service respondsToSelector:anInvocation.selector]) {
            return;
        }
        [anInvocation invokeWithTarget:service];
    }];
}

@end

如何使用?

上面講了如何實現 SOAppDelegate,那在項目中要怎麼使用呢?

集成 MLSOAppDelegate

首先,MLSOAppDelegate 能夠直接在 main 函數中使用:

#import <MLSOAppDelegate/MLSOAppDelegate.h>
    
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([MLSOAppDelegate class]));
    }
}

手動初始化 window

可是 MLSOAppDelegate 並無實現 -application:didFinishLaunchingWithOptions: 方法,應用在哪兒手動初始化 UI 呢?咱們能夠新建一個類 RootUIService:

#import "MLSOAppDelegate.h"
@interface RootUIService : NSObject <MLAppService>
@end

@implementation RootUIService

+ (void)load {
    [MLSOAppDelegate registerService:[[RootUIService alloc] init]];
}
- (NSString *)serviceName {
    return @"rootUI";
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    application.delegate.window = window;
    
    ViewController* dvc = [[ViewController alloc] init];
    UINavigationController* nav = [[UINavigationController alloc] initWithRootViewController:dvc];
    window.rootViewController = nav;
    [window makeKeyAndVisible];
    return YES;
}

@end

再來看一個複雜點的服務

前面介紹了一下簡單的服務的實現,如今再來看一個稍微複雜點的服務的實現:遠程推送服務。

#import "MLSOAppDelegate.h"
@interface NotificationService : NSObject <MLAppService>
@end

@implementation NotificationService

+ (void)load {
    [MLSOAppDelegate registerService:[[NotificationService alloc] init]];
}

- (NSString *)serviceName {
    return @"notifcation";
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) {
        NSLog(@"App was launched by remote notification.");
    } else if (launchOptions[UIApplicationLaunchOptionsLocalNotificationKey]) {
        NSLog(@"App was launched by local notification.");
    }
    [self registerUserNotifications];
    return YES;
}

- (void)registerUserNotifications {
    UIUserNotificationType types = (UIUserNotificationTypeBadge|
                                    UIUserNotificationTypeSound|
                                    UIUserNotificationTypeAlert);
    UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
    [[UIApplication sharedApplication] registerUserNotificationSettings:settings];
}

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    NSLog(@"%@ %@", NSStringFromSelector(_cmd), deviceToken);
    // upload the deviceToken to your servers
}
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
    NSLog(@"%@ %@", NSStringFromSelector(_cmd), error);
}
- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings {
    NSLog(@"%@ %@", NSStringFromSelector(_cmd), notificationSettings);
    [application registerForRemoteNotifications];
}

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
    NSLog(@"%@ %@", NSStringFromSelector(_cmd), userInfo);
}

@end

上面的代碼可能會讓你感到疑惑:RootUIService 和 NotificationService 兩個類都實現了 application:didFinishLaunchingWithOptions: 方法,程序在運行的時候究竟調用哪個?

答案是,都會調用,可是調用順序是不肯定的

繼承 MLSOAppDelegate

有的應用中有一些啓動代碼必須放在其它代碼前執行,你可能會想到下面這個解決方法,繼承 MLSOAppDelegate:

@interface AppDelegate : MLSOAppDelegate
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 在其它服務執行前,作一些事情
    // ...
    // 這裏調用 super 方法,是爲了服務的代理實現可以正常執行
    if ([super respondsToSelector:@selector(application:didFinishLaunchingWithOptions:)]) {
        [super application:application didFinishLaunchingWithOptions:launchOptions];
    }
    return YES;
}
@end

注意調用 super 方法的方式。 有些服務可能會實現 application:didFinishLaunchingWithOptions: 這個方法,調用 super 方法,能夠保證這些服務的代理方法可以正常執行。

最後

這個方案使得開啓一些服務(好比遠程推送)變得簡單,只須要把 NotificationService 這個類加到工程中就能夠,不須要修改 AppDelegate 任何一行代碼,重用 NotificationService 也變得簡單。可是還存在一些問題,在執行服務實現的代理方法的時候,順序不可控。

最後再貼一下源碼連接:https://github.com/alexsun/ML...

相關文章
Objective-C Runtime 之動態方法解析實踐
使用 FlowControllers 改進iOS應用架構


做者信息
原文做者系力譜宿雲 LeapCloud 旗下MaxLeap團隊_UX成員:孫進【原創】
首發地址:https://blog.maxleap.cn/archi...
孫進,現任職於 MaxLeap UX 團隊,負責 MaxLeap iOS 端 SDK 開發,爲開發者提供好用,穩定的產品。此前作過兩年 iOS 應用開發,如今正嘗試 React Native 開發。


活動預告

clipboard.png

報名連接:http://t.cn/Rt9ooRw


對咱們的技術乾貨/活動有興趣的小夥伴,請掃一下二維碼,關注咱們的微信公衆號!

clipboard.png

相關文章
相關標籤/搜索