AppDelegate模块化历程

简介: AppDelegate控制着App的主要生命周期,比如App初始化完成后构建主视图,App接收到远程消息回调,Url-Scheme回调,第三方SDK初始化,数据库初始化等等。基于这个原因,随着App的版本迭代,AppDelegate中的代码量会越来越大。当AppDelegate的代码量到达一定程度时,我们就该开始考虑将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。目前只通过了文档约束,后续还会进行改进。
目录
相关文章
|
4月前
|
设计模式 前端开发 测试技术
Flutter 项目架构技术指南
探讨Flutter项目代码组织架构的关键方面和建议。了解设计原则SOLID、Clean Architecture,以及架构模式MVC、MVP、MVVM,如何有机结合使用,打造优秀的应用架构。
141 1
Flutter 项目架构技术指南
|
14天前
|
C# 开发者 前端开发
揭秘混合开发新趋势:Uno Platform携手Blazor,教你一步到位实现跨平台应用,代码复用不再是梦!
【8月更文挑战第31天】随着前端技术的发展,混合开发日益受到开发者青睐。本文详述了如何结合.NET生态下的两大框架——Uno Platform与Blazor,进行高效混合开发。Uno Platform基于WebAssembly和WebGL技术,支持跨平台应用构建;Blazor则让C#成为可能的前端开发语言,实现了客户端与服务器端逻辑共享。二者结合不仅提升了代码复用率与跨平台能力,还简化了项目维护并增强了Web应用性能。文中提供了从环境搭建到示例代码的具体步骤,并展示了如何创建一个简单的计数器应用,帮助读者快速上手混合开发。
25 0
|
14天前
|
前端开发 Java UED
JSF 面向组件开发究竟藏着何种奥秘?带你探寻可复用 UI 组件设计的神秘之路
【8月更文挑战第31天】在现代软件开发中,高效与可维护性至关重要。JavaServer Faces(JSF)框架通过其面向组件的开发模式,提供了构建复杂用户界面的强大工具,特别适用于设计可复用的 UI 组件。通过合理设计组件的功能与外观,可以显著提高开发效率并降低维护成本。本文以一个具体的 `MessageComponent` 示例展示了如何创建可复用的 JSF 组件,并介绍了如何在 JSF 页面中使用这些组件。结合其他技术如 PrimeFaces 和 Bootstrap,可以进一步丰富组件库,提升用户体验。
26 0
|
4月前
|
Dart 前端开发 测试技术
【Flutter前端技术开发专栏】Flutter开发中的代码质量与重构实践
【4月更文挑战第30天】随着Flutter在跨平台开发的普及,保证代码质量成为开发者关注的重点。优质代码能确保应用性能与稳定性,提高开发效率。关键策略包括遵循最佳实践,编写可读性强的代码,实施代码审查和自动化测试。重构实践在项目扩展时尤为重要,适时重构能优化结构,降低维护成本。开发者应重视代码质量和重构,以促进项目成功。
63 0
【Flutter前端技术开发专栏】Flutter开发中的代码质量与重构实践
|
4月前
|
API Android开发 UED
UniApp 项目中的生命周期详解:从诞生到逝去
UniApp 项目中的生命周期详解:从诞生到逝去
174 4
|
4月前
|
设计模式 JavaScript 开发者
Flutter笔记:聊一聊Flutter中委托的设计方法
Flutter笔记:聊一聊Flutter中委托的设计方法
147 0
|
存储 SQL 前端开发
借一个项目谈Android应用软件架构,你还在套用MVP 或MVVM吗
借一个项目谈Android应用软件架构,你还在套用MVP 或MVVM吗
|
缓存 API Android开发
|
安全 JavaScript Unix
19条跨端cpp开发有效经验总结
细想,专门从事跨多端开发已两年有余,前段时间因为组里跨桌面端项目需要回归windows下开发了整整2个月,怎么形容这两个月呢,嘿嘿,各种“肆无忌惮”的写法,终于不用在写一行代码考虑后面n个端的行为了,"劳动力"、"效率"得到大幅度解放,但是随着windows发版结束后,我负责mac的适配相关工作,在这个阶段,发现很多不"合规"的奇技淫巧(原定2个工作日的适配quota,大概进行了一周),作为一个略有想法的cpp程序员,遂产生了想写一个跨多端开发避坑指南的想法,想起过去看的Scott Meyers的《Effective C++》....努力写"xx条有效使用cpp开发跨端的经验",期望看完此文可
204 0
19条跨端cpp开发有效经验总结