組件化分發生命週期

原文 : 與佳期的我的博客(gonghonglou.com)html

我不要你以爲,我要我以爲,聽個人,組件化分發生命週期就這麼寫!ios

是什麼

組件化分發生命週期是什麼?就是將主工程的生命週期分發到各個組件裏去。直觀些的介紹則是:AppDelegate 遵循並實現了 UIApplicationDelegate 代理,其中包括 willFinishLaunchingWithOptions:didFinishLaunchingWithOptions:applicationWillEnterForeground:applicationDidEnterBackground: 等等方法,包含了主工程的各個階段將執行的方法,咱們要作的就是在主工程的這些階段方法被執行的時候,各個組件裏相對應的階段方法同時會被執行,這樣,主工程和各個組件便共享了生命週期,git

爲何

至於爲何要將主工程的生命週期分發到各個組件中,緣由有如下幾點:github

一、替換 load 方法 由於 load 方法時機較早,全部不少時候會在 load 方法裏執行註冊,初始化等操做,但這也會致使 load 方法的濫用,將一些本能夠靠後執行的操做提早執行了,可能引起 APP 啓動耗時過長的問題,須要作 load 耗時監測,治理起來困難,因此不少團隊是禁用 load 方法的。 將這些操做方法放到生命週期方法裏去作顯然更好,尋找合理的時機執行相應的操做,耗時能檢測功能也比較好作。objective-c

二、解決 AppDelegate 臃腫問題 工程中不免有一系列的註冊、初始化操做,好比:APP 性能檢測、bug 收集、打點等一系列工具的註冊;各類基礎組件涉及的初始化或重置操做。 將這些操做放到組件本身的生命週期方法裏去執行,避免了 AppDelegate 的臃腫,並且各基礎組件與主工程解耦,開發維護更方便。數組

三、Debugger 類組件可插拔 某些 Debugger 類組件在工做前可能須要註冊操做,將註冊操做放在 Pod 本身的生命週期裏。這樣一來,對於 Debugger 類組件只須要在 Podfile 裏控制加載形式,便可作到 Debug/Release 環境組件可插拔,如:緩存

pod 'DebuggerKit', :configurations => ['Debug']
複製代碼

怎麼作

相比於將 AppDelegate 裏的全部階段方法分發出去,先介紹兩種相對輕量的作法,也能作到和分發生命週期相似的能力:ruby

一、sunnyxx 的 Notification Once

巧妙的通知註冊app

+ (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

二、美團的 Kylin 註冊函數

在編譯時把數據(如函數指針)寫入到可執行文件的 __DATA 段中,在運行時的某個階段(如 willFinishLaunch)再從 __DATA 段取出數據進行相應的操做(調用函數)。

Kylin實現原理簡述:Clang 提供了不少的編譯器函數,它們能夠完成不一樣的功能。其中一種就是 section() 函數,section()函數提供了二進制段的讀寫能力,它能夠將一些編譯期就能夠肯定的常量寫入數據段。 在具體的實現中,主要分爲編譯期和運行時兩個部分。在編譯期,編譯器會將標記了 attribute((section())) 的數據寫到指定的數據段中,例如寫一個 {key(key表明不一樣的啓動階段), *pointer} 對到數據段。到運行時,在合適的時間節點,在根據key讀取出函數指針,完成函數的調用。

這種方案呢,最好去寫個專門的工具,如 Kylin,去實現 {key, *pointer} 對的註冊和調用操做。對於使用方來講,添加一種函數執行時,使用 Kylin 註冊,並在 AppDelegate 的合理階段調起方法。

參考:美團外賣iOS App冷啓動治理

除了以上方法以外還有一些比較「大型」的作法就是把 AppDelegate 的生命週期完整的分發出去:

三、手動註冊、遍歷分發

這是我以前公司的作法,提早註冊 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 類,執行相應方法,從而實現生命週期的分發。

這種作法的優勢是沒什麼騷操做(姑且算優勢吧),都是基本方法遍歷調用,就一個反射操做也算經常使用吧。弊端就顯而易見了:

1)須要註冊行爲。每添加一個 pod,想要爲該 Pod 配置生命週期管理類的話都要去配置文件裏註冊一次。雖然項目穩定下來後 pod 基本不會變更,但使用起來總歸不夠理想,並且由於配置文件的存在,這種中心化的寫法會致使代碼臃腫,閱讀維護困難。 2)侵入 AppDelegate 類。須要更改 AppDelegate 的父類,而且在 AppDelegate 的各階段裏調用 super。

四、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 的生命週期方法,起到分發的效果。

優勢便是: 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

五、消息轉發

由於上一種方案中存在的問題,我在想還有什麼更好的方案,只提供一個 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 方法裏。

六、最後的優化

我在想有什麼方法能夠去掉這個註冊操做?若是咱們讓全部組件裏控制生命週期的類都繼承自 Lifecycle 類,那麼咱們經過獲取 Lifecycle 的全部子類就可以完成註冊操做了。思路很簡單:經過 runtime 獲取全部註冊的類,遍歷這些類判斷其父類是不是 Lifecycle 類。由於註冊類的總數可能會很是大,爲了不性能問題,將這個方法的調用控制在 Debug 模式下執行,將拿到的數組存儲在本地,這樣在 Release 環境下直接獲取緩存數據便可。

照着這樣的思路又實現了一份優化過的代碼,收集註冊子類:

- (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

後記

相關文章
相關標籤/搜索