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

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

但項(xiàng)目中ViewController之前沒(méi)有做任何繼承關(guān)系(無(wú)基類Controller),所以在幾十上百個(gè)Controller的項(xiàng)目里乡范,一個(gè)一個(gè)添加耗時(shí)不說(shuō)配名,也不利于后期維護(hù),那也是不推薦使用的蠢方法晋辆。

1渠脉、利用Objective-C 中的對(duì)象繼承

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

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

缺點(diǎn):繼承造成類與類之間耦合性太強(qiáng)器仗。

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

如果項(xiàng)目中沒(méi)有ViewController基類的話,重新創(chuàng)建一個(gè)基類也不難轨香。

  • 新建一個(gè)BaseViewController忽你,在所需采集統(tǒng)計(jì)的地方寫(xiě)公共方法;

  • 處理項(xiàng)目ViewController的繼承關(guān)系臂容,將項(xiàng)目中默認(rèn)繼承ViewController的替換繼承BaseViewController科雳;

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

這種方法比較考驗(yàn)?zāi)托暮图?xì)心脓杉,更換了默認(rèn)的繼承關(guān)系糟秘。會(huì)更改很多類,侵入性雖說(shuō)很大球散。但考慮到以后的維護(hù)成本尿赚,在控制器類還不龐大的情況下,建議使用此種方法。不能完成的是原項(xiàng)目已經(jīng)有繼承關(guān)系了凌净,但繼承關(guān)系比較負(fù)責(zé)悲龟。這時(shí)候就要更加小心(除了處理可能多個(gè)父類,還要處理沒(méi)繼承的控制器)泻蚊,相當(dāng)于給他們?cè)炝艘粋€(gè)祖宗~

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

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

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

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

詳見(jiàn)Demo: 多個(gè)分類重名時(shí)贪庙,方法的調(diào)用順序

  • 新建一個(gè)UIViewController 的category,引人runtime頭文件
#import <objc/runtime.h>
  • 重寫(xiě)load方法
+ (void)load {
    [super load];
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 交換VC頁(yè)面完全顯示方法
        __mmc_tracer_swizzleMethod([self class], @selector(viewDidAppear:), @selector(__mmc_tracer_viewDidAppear:));
        // 交換VC頁(yè)面完全消失方法
        __mmc_tracer_swizzleMethod([self class], @selector(viewDidDisappear:), @selector(__mmc_tracer_viewDidDisappear:));
    });
}
  • 使用runtime實(shí)現(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)用自己的方法一次翰苫,因?yàn)樽约阂惨獙?shí)現(xiàn)被交換之前方法的內(nèi)部實(shí)現(xiàn))止邮,但要注意忽略系統(tǒng)控制器類的方法。
/** 只忽略了部分常用系統(tǒng)原生contrlloer
 * 通過(guò)繼承父類來(lái)實(shí)現(xiàn) 相對(duì)于hook來(lái)說(shuō) 是較為準(zhǔn)確的,因?yàn)樾枰唤y(tǒng)計(jì)的頁(yè)面都是繼承于這個(gè)父類的控制器,而其他的如UINavigationController,系統(tǒng)自帶的UIAlertController等則不會(huì)誤入統(tǒng)計(jì)數(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)用的實(shí)際上是viewDidAppear:方法
    if ([UIViewController isIgnoreSystemViewController:self]) {
        //TODO: 實(shí)現(xiàn)頁(yè)面顯示的打點(diǎn)采集方法
    }
}

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

3奏窑、利用Aspects實(shí)行方法hook (被動(dòng)調(diào)用导披,類似通知監(jiān)聽(tīng))

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

  • OOP針對(duì)業(yè)務(wù)處理過(guò)程的實(shí)體及其屬性和行為進(jìn)行抽象封裝止毕,以獲得更加清晰高效的邏輯單元?jiǎng)澐郑?/li>
  • AOP則是針對(duì)業(yè)務(wù)處理過(guò)程中的切面進(jìn)行提取,它所面對(duì)的是處理過(guò)程中的某個(gè)步驟或階段巍实,以獲得邏輯過(guò)程中各部分之間低耦合性的隔離效果滓技。

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

常用的兩個(gè)方法
  • 寫(xiě)一個(gè)內(nèi)部私有方法hook所有UIViewController的所有實(shí)例的顯示與消失方法纬朝,在采集類初始化的時(shí)候調(diào)用(建議:初始化方法中應(yīng)該有一個(gè)是否由該采集SDK主動(dòng)采集頁(yè)面顯示與隱藏的BOOL參數(shù)收叶,在該參數(shù)下初始化為最佳時(shí)機(jī))。
/** 只忽略了部分常用系統(tǒng)原生contrlloer
 * 通過(guò)繼承父類來(lái)實(shí)現(xiàn) 相對(duì)于hook來(lái)說(shuō) 是較為準(zhǔn)確的,因?yàn)樾枰唤y(tǒng)計(jì)的頁(yè)面都是繼承于這個(gè)父類的控制器,而其他的如UINavigationController,系統(tǒng)自帶的UIAlertController等則不會(huì)誤入統(tǒng)計(jì)數(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方案有一個(gè)好處就是可以避免代碼入侵共苛,做到更加廣泛的通用性判没。通過(guò)swizzling我們可以將原method與自己加入的method相結(jié)合,即不需要在原有工程中加入代碼隅茎,又能做到全局覆蓋澄峰。

三種方案對(duì)比:

1、通過(guò)繼承父類來(lái)實(shí)現(xiàn)辟犀,相對(duì)于hook來(lái)說(shuō)俏竞,是較為準(zhǔn)確的。因?yàn)樾枰唤y(tǒng)計(jì)的頁(yè)面都是繼承于這個(gè)父類的控制器堂竟,而其他的如UINavigationController魂毁、UIAlertController等則不會(huì)誤入統(tǒng)計(jì)數(shù)據(jù)當(dāng)中。

2出嘹、上面提到 hook方案是通過(guò)hook UIViewController viewDidLoad/viewDidAppear等方法席楚,而這些方法實(shí)際上每個(gè)Controller 都會(huì)調(diào)用,那么就會(huì)出現(xiàn)不該出現(xiàn)的Controller 也出現(xiàn)在這里(如上面說(shuō)到的UINavigationControllerUIAlertController)疚漆。但hook方案一個(gè)比較好的特點(diǎn)是無(wú)代碼入侵酣胀,在不修改項(xiàng)目代碼的前提下完成工作。

? 3娶聘、兩種hook的對(duì)比:分類的方法只要分類被load則就開(kāi)始hook, 時(shí)機(jī)并不能自己控制闻镶,而且也不能自己開(kāi)關(guān)控制是否hook或者終止hook操作。一個(gè)由程序員主動(dòng)調(diào)用(Aspects)丸升,一個(gè)由系統(tǒng)調(diào)用(分類)铆农。由于可控性最后還是選擇了Aspects來(lái)進(jìn)行hook。

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

最后推薦兩篇針對(duì)Aspects個(gè)人覺(jué)得寫(xiě)得非常棒的文章

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末骡苞,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子楷扬,更是在濱河造成了極大的恐慌解幽,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毅否,死亡現(xiàn)場(chǎng)離奇詭異亚铁,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)螟加,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)吞琐,“玉大人捆探,你說(shuō)我怎么就攤上這事≌舅冢” “怎么了黍图?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)奴烙。 經(jīng)常有香客問(wèn)我助被,道長(zhǎng),這世上最難降的妖魔是什么切诀? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任揩环,我火速辦了婚禮,結(jié)果婚禮上幅虑,老公的妹妹穿的比我還像新娘丰滑。我一直安慰自己,他們只是感情好倒庵,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布褒墨。 她就那樣靜靜地躺著,像睡著了一般擎宝。 火紅的嫁衣襯著肌膚如雪郁妈。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,692評(píng)論 1 305
  • 那天绍申,我揣著相機(jī)與錄音噩咪,去河邊找鬼锄奢。 笑死,一個(gè)胖子當(dāng)著我的面吹牛剧腻,可吹牛的內(nèi)容都是我干的拘央。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼书在,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼灰伟!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起儒旬,我...
    開(kāi)封第一講書(shū)人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤栏账,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后栈源,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體挡爵,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年甚垦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了茶鹃。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡艰亮,死狀恐怖闭翩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情迄埃,我是刑警寧澤疗韵,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站侄非,受9級(jí)特大地震影響蕉汪,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜逞怨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一者疤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧骇钦,春花似錦宛渐、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至鳞仙,卻和暖如春寇蚊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背棍好。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工仗岸, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留允耿,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓扒怖,卻偏偏與公主長(zhǎng)得像较锡,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子盗痒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容