iOS"添加方法"對項目代碼侵入性較少的實現(xiàn)思路

最近公司項目做用戶大數(shù)據(jù)信息采集,需要采集App端在頁面切換/交互操作的時候需要統(tǒng)計頁面顯示/頁面消失事件,要給后端統(tǒng)計系統(tǒng)發(fā)送采集logs記錄。

但項目中ViewController之前沒有做任何繼承關(guān)系(無基類Controller),所以在幾十上百個Controller的項目里,一個一個添加耗時不說,也不利于后期維護,那也是不推薦使用的蠢方法。

1、利用Objective-C 中的對象繼承

繼承在面向?qū)ο箝_發(fā)中是非常常用的。

優(yōu)點:繼承可以實現(xiàn)代碼的復(fù)用,減少代碼冗余。將所有重復(fù)的內(nèi)容合并在一起,可以使代碼有效率,簡潔,才意味著是一個成功的架構(gòu)。否則,修改代碼時需要修改多處,就很容易出錯。

缺點:繼承造成類與類之間耦合性太強。

現(xiàn)在項目工程中都會有一個BaseViewController,所有新建的ViewController都繼承BaseViewController,通過往BaseViewController中添加一些公共方法/屬性可以被他們的子類所調(diào)用;這是統(tǒng)一工程中所有視圖控制器樣式的一個主要途徑。

如果項目中沒有ViewController基類的話,重新創(chuàng)建一個基類也不難。

  • 新建一個BaseViewController,在所需采集統(tǒng)計的地方寫公共方法;

  • 處理項目ViewController的繼承關(guān)系,將項目中默認繼承ViewController的替換繼承BaseViewController;

    左側(cè)邊欄搜索替換: UIViewController,替換成: XXBaseViewController

這種方法比較考驗?zāi)托暮图毿?,更換了默認的繼承關(guān)系。會更改很多類,侵入性雖說很大。但考慮到以后的維護成本,在控制器類還不龐大的情況下,建議使用此種方法。不能完成的是原項目已經(jīng)有繼承關(guān)系了,但繼承關(guān)系比較負責(zé)。這時候就要更加小心(除了處理可能多個父類,還要處理沒繼承的控制器),相當(dāng)于給他們造了一個祖宗~

2、利用Category和Runtime交換系統(tǒng)方法并添加方法(系統(tǒng)主動調(diào)用load)

load函數(shù)調(diào)用特點如下:

???當(dāng)類被引用進項目的時候就會執(zhí)行l(wèi)oad函數(shù)(在main函數(shù)開始執(zhí)行之前),與這個類是否被用到無關(guān),每個類的load函數(shù)只會自動調(diào)用一次.由于load函數(shù)是系統(tǒng)自動加載的,因此不需要調(diào)用父類的load函數(shù),否則父類的load函數(shù)會多次執(zhí)行。

  • 1、當(dāng)父類和子類都實現(xiàn)load函數(shù)時,父類的load方法執(zhí)行順序要優(yōu)先于子類;
  • 2、當(dāng)子類未實現(xiàn)load方法時,不會調(diào)用父類load方法;
  • 3、類中的load方法執(zhí)行順序要優(yōu)先于類別(Category);
  • 4、當(dāng)有多個類別(Category)都實現(xiàn)了load方法,這幾個load方法都會執(zhí)行,但執(zhí)行順序不確定(其執(zhí)行順序與類別在Compile Sources中出現(xiàn)的順序一致);
  • 5、當(dāng)然當(dāng)有多個不同的類的時候,每個類load 執(zhí)行順序與其在Compile Sources出現(xiàn)的順序一致;

詳見Demo: 多個分類重名時,方法的調(diào)用順序

  • 新建一個UIViewController 的category,引人runtime頭文件
#import <objc/runtime.h>
  • 重寫load方法
+ (void)load {
    [super load];
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 交換VC頁面完全顯示方法
        __mmc_tracer_swizzleMethod([self class], @selector(viewDidAppear:), @selector(__mmc_tracer_viewDidAppear:));
        // 交換VC頁面完全消失方法
        __mmc_tracer_swizzleMethod([self class], @selector(viewDidDisappear:), @selector(__mmc_tracer_viewDidDisappear:));
    });
}
  • 使用runtime實現(xiàn)方法交換
void __mmc_tracer_swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector){
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    BOOL didAddMethod =
    class_addMethod(class,
                    originalSelector,
                    method_getImplementation(swizzledMethod),
                    method_getTypeEncoding(swizzledMethod));
    
    if (didAddMethod) {
        class_replaceMethod(class,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}
  • 替換的交換方法中添加自己需要的采集方法(注意:要調(diào)用自己的方法一次,因為自己也要實現(xiàn)被交換之前方法的內(nèi)部實現(xiàn)),但要注意忽略系統(tǒng)控制器類的方法。
/** 只忽略了部分常用系統(tǒng)原生contrlloer
 * 通過繼承父類來實現(xiàn) 相對于hook來說 是較為準(zhǔn)確的,因為需要被統(tǒng)計的頁面都是繼承于這個父類的控制器,而其他的如UINavigationController,系統(tǒng)自帶的UIAlertController等則不會誤入統(tǒng)計數(shù)據(jù)當(dāng)中
 */
+ (BOOL)isIgnoreSystemViewController:(id)instance {
    return
    [instance isKindOfClass:[UITabBarController class]] ||
    [instance isKindOfClass:[UINavigationController class]] ||
    [instance isKindOfClass:[UISearchController class]] ||
    [instance isKindOfClass:[UIAlertController class]] ||
    [instance isKindOfClass:[UISearchController class]] ||
    [instance isKindOfClass:[UIActivityViewController class]];
}


- (void)__mmc_tracer_viewDidAppear:(BOOL)animated {
    [self __mmc_tracer_viewDidAppear:animated];  //由于方法已經(jīng)被交換,這里調(diào)用的實際上是viewDidAppear:方法
    if ([UIViewController isIgnoreSystemViewController:self]) {
        //TODO: 實現(xiàn)頁面顯示的打點采集方法
    }
}

- (void)__mmc_tracer_viewDidDisappear:(BOOL)animated {
    [self __mmc_tracer_viewDidDisappear:animated];  //由于方法已經(jīng)被交換,這里調(diào)用的實際上是viewDidDisappear:方法
    if ([UIViewController isIgnoreSystemViewController:self]) {
        //TODO: 實現(xiàn)頁面消失的打點采集方法
    }
}

3、利用Aspects實行方法hook (被動調(diào)用,類似通知監(jiān)聽)

Aspects是AOP(面向切面編程)思想在iOS下OC的實現(xiàn)。Aspects可以用于hook函數(shù),讓函數(shù)執(zhí)行一些副操作。為嵌入不同函數(shù)中的功能相同的操作,每類功能相同的操作可以抽取出一個切面。

  • OOP針對業(yè)務(wù)處理過程的實體及其屬性和行為進行抽象封裝,以獲得更加清晰高效的邏輯單元劃分;
  • AOP則是針對業(yè)務(wù)處理過程中的切面進行提取,它所面對的是處理過程中的某個步驟或階段,以獲得邏輯過程中各部分之間低耦合性的隔離效果。

核心原理:當(dāng)被 hook 的 selector 被執(zhí)行的時候,首先根據(jù) selector找到了 objc_msgForward / _objc_msgForward_stret,而這個會觸發(fā)消息轉(zhuǎn)發(fā),從而進入 forwardInvocation。同時由于forwardInvocation 的指向也被修改了,因此會轉(zhuǎn)入新的 forwardInvocation函數(shù),在里面執(zhí)行需要嵌入的附加代碼,完成之后,再轉(zhuǎn)回原來的 IMP。

常用的兩個方法
  • 寫一個內(nèi)部私有方法hook所有UIViewController的所有實例的顯示與消失方法,在采集類初始化的時候調(diào)用(建議:初始化方法中應(yīng)該有一個是否由該采集SDK主動采集頁面顯示與隱藏的BOOL參數(shù),在該參數(shù)下初始化為最佳時機)。
/** 只忽略了部分常用系統(tǒng)原生contrlloer
 * 通過繼承父類來實現(xiàn) 相對于hook來說 是較為準(zhǔn)確的,因為需要被統(tǒng)計的頁面都是繼承于這個父類的控制器,而其他的如UINavigationController,系統(tǒng)自帶的UIAlertController等則不會誤入統(tǒng)計數(shù)據(jù)當(dāng)中
 */
- (BOOL)isIgnoreSystemViewController:(id)instance {
    return
    [instance isKindOfClass:[UITabBarController class]] ||
    [instance isKindOfClass:[UINavigationController class]] ||
    [instance isKindOfClass:[UISearchController class]] ||
    [instance isKindOfClass:[UIAlertController class]] ||
    [instance isKindOfClass:[UISearchController class]] ||
    [instance isKindOfClass:[UIActivityViewController class]];
}

- (void)hookAllViewControllerAppearAndDisappear {
    /// hook 控制器的顯示和消失 分別打log
    // 顯示
    [UIViewController aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {

        if (![self isIgnoreSystemViewController:aspectInfo.instance]) {
            // 不是忽略VC則采集
            [[self class] addBeginLogPageView:NSStringFromClass([aspectInfo.instance class])];
        }
    } error:NULL];
    
    // 消失
    [UIViewController aspect_hookSelector:@selector(viewDidDisappear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
        if (![self isIgnoreSystemViewController:aspectInfo.instance]) {
            // 不是忽略VC則采集
            [[self class] addEndLogPageView:NSStringFromClass([aspectInfo.instance class])];
        }
    } error:NULL];
}

hook方案有一個好處就是可以避免代碼入侵,做到更加廣泛的通用性。通過swizzling我們可以將原method與自己加入的method相結(jié)合,即不需要在原有工程中加入代碼,又能做到全局覆蓋。

三種方案對比:

1、通過繼承父類來實現(xiàn),相對于hook來說,是較為準(zhǔn)確的。因為需要被統(tǒng)計的頁面都是繼承于這個父類的控制器,而其他的如UINavigationController、UIAlertController等則不會誤入統(tǒng)計數(shù)據(jù)當(dāng)中。

2、上面提到 hook方案是通過hook UIViewController viewDidLoad/viewDidAppear等方法,而這些方法實際上每個Controller 都會調(diào)用,那么就會出現(xiàn)不該出現(xiàn)的Controller 也出現(xiàn)在這里(如上面說到的UINavigationControllerUIAlertController)。但hook方案一個比較好的特點是無代碼入侵,在不修改項目代碼的前提下完成工作。

? 3、兩種hook的對比:分類的方法只要分類被load則就開始hook, 時機并不能自己控制,而且也不能自己開關(guān)控制是否hook或者終止hook操作。一個由程序員主動調(diào)用(Aspects),一個由系統(tǒng)調(diào)用(分類)。由于可控性最后還是選擇了Aspects來進行hook。

???但要做的是通用的大數(shù)據(jù)采集類,以后公司內(nèi)部所有App都可能會用到,在不知道是哪個App有什么Controller的情況下,hook顯然成了最好的方法。當(dāng)然要注意篩選掉系統(tǒng)的Controller,避免重復(fù)采集無用的數(shù)據(jù)。 如果要做內(nèi)部封裝的話顯然Aspects hook的方式好一點,不然的話你就要暴露出API提供給分類,或者將分類寫入封裝類內(nèi)部。這樣代碼比較長不利于后期維護。還有最重要的一點就是:使用Aspects可以留一個開關(guān)給外部,是否需要sdk幫助采集所有界面的出現(xiàn)和消失,或者交給使用者自己采集界面信息。

最后推薦兩篇針對Aspects個人覺得寫得非常棒的文章

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容