组件化分发生命周期

原文 : 与佳期的个人博客(gonghonglou.com)

我不要你觉得,我要我觉得,听我的,组件化分发生命周期就这么写!

是什么

组件化分发生命周期是什么?就是将主工程的生命周期分发到各个组件里去。直观些的介绍则是:AppDelegate 遵循并实现了 UIApplicationDelegate 代理,其中包括 willFinishLaunchingWithOptions:didFinishLaunchingWithOptions:applicationWillEnterForeground:applicationDidEnterBackground: 等等方法,包含了主工程的各个阶段将执行的方法,我们要做的就是在主工程的这些阶段方法被执行的时候,各个组件里相对应的阶段方法同时会被执行,这样,主工程和各个组件便共享了生命周期,

为什么

至于为什么要将主工程的生命周期分发到各个组件中,原因有以下几点:

1、替换 load 方法
因为 load 方法时机较早,所有很多时候会在 load 方法里执行注册,初始化等操作,但这也会导致 load 方法的滥用,将一些本可以靠后执行的操作提前执行了,可能引发 APP 启动耗时过长的问题,需要做 load 耗时监测,治理起来困难,所以很多团队是禁用 load 方法的。
将这些操作方法放到生命周期方法里去做显然更好,寻找合理的时机执行相应的操作,耗时能检测功能也比较好做。

2、解决 AppDelegate 臃肿问题
工程中难免有一系列的注册、初始化操作,比如:APP 性能检测、bug 收集、打点等一系列工具的注册;各种基础组件涉及的初始化或重置操作。
将这些操作放到组件自己的生命周期方法里去执行,避免了 AppDelegate 的臃肿,而且各基础组件与主工程解耦,开发维护更方便。

3、Debugger 类组件可插拔
某些 Debugger 类组件在工作前可能需要注册操作,将注册操作放在 Pod 自己的生命周期里。这样一来,对于 Debugger 类组件只需要在 Podfile 里控制加载形式,即可做到 Debug/Release 环境组件可插拔,如:

pod 'DebuggerKit', :configurations => ['Debug']

怎么做

相比于将 AppDelegate 里的所有阶段方法分发出去,先介绍两种相对轻量的做法,也能做到和分发生命周期类似的能力:

1、sunnyxx 的 Notification Once

巧妙的通知注册

+ (void)load {
    __block id observer =
    [[NSNotificationCenter defaultCenter]
     addObserverForName:UIApplicationDidFinishLaunchingNotification
     object:nil
     queue:nil
     usingBlock:^(NSNotification *note) {
         [self setup]; // Do whatever you want
         [[NSNotificationCenter defaultCenter] removeObserver:observer];
     }];
}

很巧妙的方法,优点很明显:轻量!虽然侵入了 load 方法,不过如果没有 load 的滥用的话也可以接受,毕竟只是在 load 里执行了注册行为,具体的执行时机还是 UIApplicationDidFinishLaunchingNotification

缺点是该方法的响应是在 - application:didFinishLaunchingWithOptions: 调用完成后发送,时机没法精确控制,因为有的时候因为时机问题,我们想让各种 Pod 里的注册操作在 AppDelegate 的 didFinishLaunchingWithOptions: 方法靠前执行,即先执行组件里的注册操作再执行 AppDelegate 里的操作。

参考原文:Notification Once

2、美团的 Kylin 注册函数

在编译时把数据(如函数指针)写入到可执行文件的 __DATA 段中,在运行时的某个阶段(如 willFinishLaunch)再从 __DATA 段取出数据进行相应的操作(调用函数)。

Kylin实现原理简述:Clang 提供了很多的编译器函数,它们可以完成不同的功能。其中一种就是 section() 函数,section()函数提供了二进制段的读写能力,它可以将一些编译期就可以确定的常量写入数据段。 在具体的实现中,主要分为编译期和运行时两个部分。在编译期,编译器会将标记了 attribute((section())) 的数据写到指定的数据段中,例如写一个 {key(key代表不同的启动阶段), *pointer} 对到数据段。到运行时,在合适的时间节点,在根据key读取出函数指针,完成函数的调用。

这种方案呢,最好去写个专门的工具,如 Kylin,去实现 {key, *pointer} 对的注册和调用操作。对于使用方来说,添加一种函数执行时,使用 Kylin 注册,并在 AppDelegate 的合理阶段调起方法。

参考:美团外卖iOS App冷启动治理

除了以上方法之外还有一些比较“大型”的做法就是把 AppDelegate 的生命周期完整的分发出去:

3、手动注册、遍历分发

这是我之前公司的做法,提前注册 Lifecycle 类(可实现 AppDelegate 的各阶段方法),在 AppDelegate 各阶段方法执行的同时遍历 lifecycle 类执行相应方法。具体的做法是,

1)项目中存在一份配置文件,文件里配置着各个 pod 的 Lifecycle 类名,该类里实现了 AppDelegate 的某几个阶段方法。

2)项目启动的时候加载这份配置文件,根据类名反射成 Lifecycle 类,将所有的类添加到一个数组中(LifecycleArray)。

3)在 AppDelegate 和 UIResponder 的继承中间加一个 MyAppDelegate 类(GHLAppDelegate : MyAppDelegate : UIResponder),该类拥有 AppDelegate 的所有方法,在每个阶段的方法里遍历 LifecycleArray 数组,调用各个 Lifecycle 类的本阶段方法。

4)在 AppDelegate 的各阶段方法里首先调用一下 super 方法。

这样,在 AppDelegate 各阶段执行的时候就会执行父类方法,遍历所有 pod 里的 Lifecycle 类,执行相应方法,从而实现生命周期的分发。


image

这种做法的优点是没什么骚操作(姑且算优点吧),都是基本方法遍历调用,就一个反射操作也算常用吧。弊端就显而易见了:

1)需要注册行为。每添加一个 pod,想要为该 Pod 配置生命周期管理类的话都要去配置文件里注册一次。虽然项目稳定下来后 pod 基本不会变动,但使用起来总归不够理想,而且因为配置文件的存在,这种中心化的写法会导致代码臃肿,阅读维护困难。
2)侵入 AppDelegate 类。需要更改 AppDelegate 的父类,并且在 AppDelegate 的各阶段里调用 super。

4、Category 覆盖、追加方法⚠️

因为上一种方案中存在的问题,所以我在想怎么做既可以不用注册,又不用侵入 AppDelegate 呢?Category!我想到这种方案:

1)新建 Lifecycle 类,用于向相应的 pod(第三步提到的建过分类的 Pod) 分发生命周期。该类拥有 AppDelegate 的所有生命周期方法。

2)为了不侵入 AppDelegate,给 AppDelegate 添加分类(AppDelegate+Lifecycle),用于在相应阶段调用 Lifecycle 相应方法。在该分类里重写 AppDelegate 的各阶段方法,在各个方法里分别调用 Lifecycle 类的对应方法(这里其实也可以在分类里 hook 本类的方法来实现,但是为了写法方便 Demo 里的做法是使用了第四步提供的方法:遍历方法列表找到最后一个方法执行)。

3)对使用方来说,只需要在自己的 pod 里新建 Lifecycle 类的分类(如:Lifecycle + Home、Lifecycle + Deatil 等),复写本类的方法,即 AppDelegate 的生命周期方法,这些 pod 里的这些分类的这些方法会被 Lifecycle 类全部执行。

4)怎么全部执行呢?多个分类会根据加载顺序互相覆盖方法,正常情况下只执行最后加载的那个分类的方法,因为最后加载的分类的方法被最后加到方法列表里,消息发送过程中最先被找到。解决思路是:找到那些被覆盖的方法去执行对应的 IMP:

4.1)在 Lifecycle 本类里去遍历本类的方法列表,为了避免无限循环,除了本类的方法(即方法列表的最后一个),对其他的同名 SEL 都执行对应 IMP:

+ (void)execCategorySelector:(SEL)selector forClass:(Class)class withParam1:(id)param1 param2:(id)param2 {
    BOOL isFirst = NO;
    unsigned int methodCount = 0;
    Method *methods = class_copyMethodList(class, &methodCount);
    for (int i = methodCount - 1; i >= 0; i--) {
        Method method = methods[i];
        SEL sel = method_getName(method);
        if ([NSStringFromSelector(sel) isEqualToString:NSStringFromSelector(selector)]) {
            if (!isFirst) {
                isFirst = YES;
            } else {
                IMP imp = method_getImplementation(method);
                void (*func)(id, SEL,id,id) = (void *)imp;
                func(self, sel, param1, param2);
            }
        }
    }
    free(methods);
}

4.2)为了写法方便和统一,这里给第二步也提供了执行 AppDelegate 本类方法的实现。在 AppDelegate 的方法列表里寻找同名的的最后一个 SEL,执行对应的 IMP。

+ (void)execClassSelector:(SEL)selector forClass:(Class)class withParam1:(id)param1 param2:(id)param2 {
    unsigned int methodCount = 0;
    Method *methods = class_copyMethodList(class, &methodCount);
    for (int i = methodCount - 1; i >= 0; i--) {
        Method method = methods[i];
        SEL sel = method_getName(method);
        if ([NSStringFromSelector(sel) isEqualToString:NSStringFromSelector(selector)]) {
            IMP imp = method_getImplementation(method);
            void (*func)(id, SEL,id,id) = (void *)imp;
            func(self, sel, param1, param2);
            break;
        }
    }
    free(methods);
}

这样就能调用到所有 Category 的生命周期方法,起到分发的效果。


image

优点即是:
1)对使用方来说不必注册,只需创建 Lifecycle 的分类
2)不侵入 AppDelegate 代码

缺点是:
1)要手动在主工程创建 AppDelegate 分类。
2)所添加的分类里的同名方法不会被覆盖,有反常识。
3)还有个缺点,也是为什么在标题上加一个 ⚠️ 的原因,是因为给一个类添加多个 Category,并分别覆盖本类方法时 Xcode 会提示 warning

Category is implementing a method which will also be implemented by its primary class

5、消息转发

因为上一种方案中存在的问题,我在想还有什么更好的方案,只提供一个 Pod 组件,就可以完成所有操作的方案。然后找到了青木同学的 组件化之组件生命周期管理 这篇文章,实现方案在文章里已经讲的很详细了,思路就是:

1)新建 Module 类,提供注册功能,并且可以设置优先级。使用方在自己的 Pod 继承该类创建自己的生命周期管理类,并且在 load 方法调用 Module 类的注册方法。

2)新建 UIApplication 的分类:UIApplication (Module),hook 掉 setDelegate: 方法,将代理设置给自己创建的类:ApplicationDelegateProxy。同时对包含所有注册类的数组根据优先级进行排序。

3)ApplicationDelegateProxy 类里不会实现 AppDelegate 里的那些方法,所以当系统来调用这些方法的时候,因为找不到 SEL 会进入消息转发过程:

3.1) -respondsToSelector::系统内部会调用这个方法。
判断是否实现了对应的 UIApplicationDelegate 代理方法。重写该方法结合 AppDelegate 以及所有注册的 Module 判断是否有相应实现。

3.2)-forwardingTargetForSelector:-respondsToSelector: 返回 YES ,便进入消息转发阶段,消息转发的第二步就是该方法。
判断要转发的方法是否为 UIApplicationDelegate 的代理方法,如果不是,并且 AppDelegate 能响应,把消息转发给 AppDelegate 去处理。

3.3)-methodSignatureForSelector:-forwardInvocation: :如果消息没有发给 AppDelegate,由自己来处理,将会这执行这些方法。
在这一步首先根据协议直接返回代理方法的签名,然后在 -forwardInvocation: 方法中,按照优先级,依次把消息转发给注册的模块。

3.4)消息转发中处理返回值为 BOOL 类型的情况。

这样就通过消息转发完成了生命周期的分发。已经是很不错的实现了,对外部文件没有侵入,唯一的缺点就是需要一个注册操作,而且还是在 load 方法里。

6、最后的优化

我在想有什么方法可以去掉这个注册操作?如果我们让所有组件里控制生命周期的类都继承自 Lifecycle 类,那么我们通过获取 Lifecycle 的所有子类就能够完成注册操作了。思路很简单:通过 runtime 获取所有注册的类,遍历这些类判断其父类是否是 Lifecycle 类。因为注册类的总数可能会非常大,为了避免性能问题,将这个方法的调用控制在 Debug 模式下执行,将拿到的数组存储在本地,这样在 Release 环境下直接获取缓存数据即可。


image

照着这样的思路又实现了一份优化过的代码,收集注册子类:

- (instancetype)init {
    self = [super init];
    if (self) {
#if DEBUG
        NSArray *stringArray = [self _findAllSubClass:[GHLLifecycle class]];
        self.subClasses = [self _classArrayWithStringArray:stringArray];
        [[NSUserDefaults standardUserDefaults] setObject:stringArray forKey:kGHLLifecycleClass];
#else
        NSArray *stringArray = [[NSUserDefaults standardUserDefaults] objectForKey:kGHLLifecycleClass];
        self.subCalsses = [self _classArrayWithStringArray:stringArray];
#endif
    }
    return self;
}

- (NSArray *)_classArrayWithStringArray:(NSArray *)stringArray {
    NSMutableArray *classArray = [NSMutableArray new];
    [stringArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        Class cls = NSClassFromString(obj);
        if (cls) [classArray addObject:[cls new]];
    }];
    return [classArray copy];
}

- (NSArray *)_findAllSubClass:(Class)class {
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    NSMutableArray *array = [NSMutableArray new];
    // 获取所有已注册的类
    Class *classes = (Class *)malloc(sizeof(Class) * count);
    objc_getClassList(classes, count);
    
    for (int i = 0; i < count; i++) {
        if (class == class_getSuperclass(classes[i])) {
            [array addObject:[NSString stringWithFormat:@"%@", classes[i]]];
        }
    }
    free(classes);
    return array;
}

消息转发过程:

- (BOOL)_containsProtocolMethod:(SEL)selector {
    
    unsigned int outCount = 0;
    struct objc_method_description *methodDesc = protocol_copyMethodDescriptionList(@protocol(UIApplicationDelegate), NO, YES, &outCount);
    for (int idx = 0; idx < outCount; idx++) {
        if (selector == methodDesc[idx].name) {
            free(methodDesc);
            return YES;
        }
    }
    free(methodDesc);
    return NO;
}

- (BOOL)respondsToSelector:(SEL)aSelector {
    if ([self.realDelegate respondsToSelector:aSelector]) {
        return YES;
    }
    
    for (GHLLifecycle *module in self.subClasses) {
        if ([self _containsProtocolMethod:aSelector] && [module respondsToSelector:aSelector]) {
            return YES;
        }
    }
    
    return [super respondsToSelector:aSelector];
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (![self _containsProtocolMethod:aSelector] && [self.realDelegate respondsToSelector:aSelector]) {
        return self.realDelegate;
    }
    return self;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    struct objc_method_description methodDesc = protocol_getMethodDescription(@protocol(UIApplicationDelegate), aSelector, NO, YES);
    
    if (methodDesc.name == NULL && methodDesc.types == NULL) {
        return [[self class] instanceMethodSignatureForSelector:@selector(doNothing)];
    }
    
    return [NSMethodSignature signatureWithObjCTypes:methodDesc.types];;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSMutableArray *allModules = [NSMutableArray arrayWithObjects:self.realDelegate, nil];
    [allModules addObjectsFromArray:self.subClasses];
    
    // BOOL 型返回值特殊处理
    if (anInvocation.methodSignature.methodReturnType[0] == 'B') {
        BOOL realReturnValue = NO;
        
        for (GHLLifecycle *module in allModules) {
            if ([module respondsToSelector:anInvocation.selector]) {
                [anInvocation invokeWithTarget:module];
                
                BOOL returnValue = NO;
                [anInvocation getReturnValue:&returnValue];
                
                realReturnValue = returnValue || realReturnValue;
            }
        }
        
        [anInvocation setReturnValue:&realReturnValue];
    } else {
        for (GHLLifecycle *module in allModules) {
            if ([module respondsToSelector:anInvocation.selector]) {
                [anInvocation invokeWithTarget:module];
            }
        }
    }
}

- (void)doNothing {
    
}

无侵入、无注册,个人感觉还是比较完美的。虽然最后只是基于青木的方案做了免注册的优化,但是思考过程中的其他方案也是值得分享的!

以上,就是总结的所有组件化分发生命周期的方案了。如果你还有其他更好方案,欢迎讨论!
所有的实践都在 Demo 里了: GHLShareLifecycle

后记

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容