戴銘(iOS開發(fā)課)讀書筆記:09章節(jié)-無(wú)侵入埋點(diǎn)

原文鏈接:無(wú)侵入的埋點(diǎn)方案如何實(shí)現(xiàn)菩貌?


前言:

原文中介紹了iOS開發(fā)常見的埋點(diǎn)方式:代碼埋點(diǎn)、可視化埋點(diǎn)和無(wú)埋點(diǎn)拉队。其中具體的區(qū)別我會(huì)整理在此篇文章的最后秆乳。
我們可以把可視化埋點(diǎn)和無(wú)埋點(diǎn)歸類為無(wú)侵入埋點(diǎn),它的主要實(shí)現(xiàn)原理就是通過(guò)運(yùn)行時(shí)方法替換進(jìn)行埋點(diǎn)纪铺。
使用無(wú)侵入埋點(diǎn)的方式相速,配合事件唯一標(biāo)志,可以區(qū)分和記錄項(xiàng)目中絕大多數(shù)的事件鲜锚。

正文:

在iOS開發(fā)中和蚪,埋點(diǎn)對(duì)代碼的侵入是非常嚴(yán)重的止状。而且在早起做埋點(diǎn)的工作時(shí),往往都是采用最原始的方式---手寫代碼在需要埋點(diǎn)的代碼處攒霹。記錄和分析的工作要借助一些第三方工具(例如我們之前做埋點(diǎn)使用的友盟)怯疤。手寫代碼埋點(diǎn)的過(guò)程十分痛苦,版本迭代的時(shí)候催束,舊的埋點(diǎn)代碼也很難維護(hù)和更新集峦,非常痛苦。

使用運(yùn)行時(shí)方法替換事件的實(shí)現(xiàn)抠刺,可以很大程度降低對(duì)代碼的侵入塔淤,下面簡(jiǎn)單介紹一下生命周期點(diǎn)擊事件速妖、cell點(diǎn)擊事件高蜂、手勢(shì)事件的method_exchange方式。
你也可以直接查看這個(gè) BuryDemo 中的代碼罕容。

首先备恤,先寫一個(gè)工具類用來(lái)方法的 hook:

+ (void)hookClass:(Class)classObject fromSelector:(SEL)fromSelector toSelector:(SEL)toSelector {
    Class cls = classObject;
    Method fromMethod = class_getInstanceMethod(cls, fromSelector);
    Method toMethod = class_getInstanceMethod(cls, toSelector);
    
    if (class_addMethod(cls, fromSelector, method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
        class_replaceMethod(cls, toSelector, method_getImplementation(fromMethod), method_getTypeEncoding(fromMethod));
    } else {
        method_exchangeImplementations(fromMethod, toMethod);
    }
}

執(zhí)行這個(gè)方法會(huì)將目標(biāo) 中的 兩個(gè)方法 進(jìn)行交換。

上面的方法中需要傳入三個(gè)參數(shù):
1 fromSelector 和 toSelector锦秒,這是兩個(gè)被交換的方法選擇器SEL露泊。
1 classObject,這是上面兩個(gè)方法選擇器SEL所在的類旅择。

這個(gè)方法的執(zhí)行流程:
1 通過(guò)方法選擇器獲取類中的實(shí)例方法 class_getInstanceMethod惭笑。
2 判斷類中 fromSelector 所對(duì)應(yīng)的方法是否存在,如果不存在就創(chuàng)建一個(gè) class_addMethod生真。
3 如果創(chuàng)建成功沉噩,調(diào)用 class_replaceMethod 方法將 fromSelector 替換成 toSelector。
4 如果創(chuàng)建失敗柱蟀,調(diào)用 method_exchangeImplementations 交換上面兩個(gè)方法川蒙。
這是一個(gè)標(biāo)準(zhǔn)的流程。這段代碼可以保存使用产弹。

1 監(jiān)聽頁(yè)面創(chuàng)建和銷毀派歌、停留時(shí)間等

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL fromSelectorAppear = @selector(viewWillAppear:);
        SEL toSelectorAppear = @selector(yy_viewWillAppear:);
        [YYHook hookClass:self fromSelector:fromSelectorAppear toSelector:toSelectorAppear];
        
        SEL fromSelectorDisappear = @selector(viewWillDisappear:);
        SEL toSelectorDisappear = @selector(yy_viewWillDisappear:);
        [YYHook hookClass:self fromSelector:fromSelectorDisappear toSelector:toSelectorDisappear];
    });
}

- (void)yy_viewWillAppear:(BOOL)animated {
    [self yy_viewWillAppear:animated];
    NSLog(@"%@ 啟動(dòng)", NSStringFromClass([self class]));
}

- (void)yy_viewWillDisappear:(BOOL)animated {
    [self yy_viewWillDisappear:animated];
    NSLog(@"%@ 銷毀", NSStringFromClass([self class]));
}

監(jiān)聽頁(yè)面的創(chuàng)建和銷毀只要 hook 住控制器的viewWillAppear:viewWillDisappear:方法即可。其中可以記錄頁(yè)面的打開次數(shù)和停留時(shí)間等等痰哨。

甚至你可以通過(guò)查看控制器是否被銷毀判斷頁(yè)面是否發(fā)生內(nèi)存泄漏胶果,很多檢測(cè)內(nèi)存泄漏的第三方庫(kù)大概就是這個(gè)監(jiān)聽生命周期的原理。

2 點(diǎn)擊事件
iOS中有很多控件的基類均是UIControl斤斧,例如UISwitch開關(guān)早抠、UIButton按鈕、UISegmentedControl分段控件撬讽、UISlider滑塊蕊连、UITextField文本字段控件悬垃、UIPageControl分頁(yè)控件等等。
我們可以通過(guò)監(jiān)聽 UIControl 中的 sendAction:to:forEvent: 方法做很多事甘苍。

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL fromSelector = @selector(sendAction:to:forEvent:);
        SEL toSelector = @selector(yy_sendAction:to:forEvent:);
        [YYHook hookClass:self fromSelector:fromSelector toSelector:toSelector];
    });
}

- (void)yy_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    [self yy_sendAction:action to:target forEvent:event];
    NSLog(@"點(diǎn)擊buryTag:%@", self.buryTag);
}

這里我只對(duì) UIButton 進(jìn)行了測(cè)試尝蠕,可以成功監(jiān)聽到 UIButton 的點(diǎn)擊事件。但是這里也遇到了 唯一標(biāo)志 的問(wèn)題载庭。因?yàn)橐粋€(gè)頁(yè)面中可能會(huì)有很多個(gè) button 看彼,那么你在埋點(diǎn)的時(shí)候如何區(qū)分到底是點(diǎn)擊了哪個(gè) button 呢?

原文中老師給出的方案是通過(guò) 控件的視圖樹結(jié)構(gòu) 來(lái)作為唯一標(biāo)志囚聚。通過(guò)視圖的 superviewsubviews 的屬性系宫。

但是到此為止只解決了不同頁(yè)面中的 button 的區(qū)分牲阁,但是同頁(yè)面的 button 依舊是同一索引,解決這個(gè)問(wèn)題捂襟,我們可以在剛剛的分類中添加一個(gè)屬性標(biāo)簽:

@property (nonatomic, copy) NSString *buryTag;

- (NSString *)buryTag {
    return objc_getAssociatedObject(self, @selector(buryTag));
}

- (void)setBuryTag:(NSString *)buryTag {
    objc_setAssociatedObject(self, @selector(buryTag), buryTag, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

通過(guò)這個(gè)字符串屬性沃暗,你可以標(biāo)記你想要的信息作為標(biāo)志符漩勤。
注意:分類中添加屬性需要runtime動(dòng)態(tài)關(guān)聯(lián)

3 cell點(diǎn)擊事件
UITableView 是日常開發(fā)中最為常用的控件焕妙,其中監(jiān)聽cell的點(diǎn)擊事件盯腌,我們需要通過(guò) hook setDelegate 方法來(lái)實(shí)現(xiàn)。

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL fromSelector = @selector(setDelegate:);
        SEL toSelector = @selector(yy_setDelegate:);
        [YYHook hookClass:self fromSelector:fromSelector toSelector:toSelector];
    });
}

- (void)yy_setDelegate:(id<UITableViewDelegate>)delegate {
    [self yy_setDelegate:delegate];
    SEL fromSelector = @selector(tableView:didSelectRowAtIndexPath:);
    SEL toSelector = @selector(yy_tableView:didSelectRowAtIndexPath:);

    // 檢查 Controller 中是否實(shí)現(xiàn)了 tableView:didSelectRowAtIndexPath: 代理方法
    if (![self conformSel:fromSelector inClz:[delegate class]]) {
        return;
    }
    
    //        [YYHook hookClass:[delegate class] fromSelector:fromSelector toSelector:toSelector];
    //    Method method = class_getInstanceMethod([self class], toSelector);
    //        class_replaceMethod([delegate class], toSelector, method_getImplementation(method), method_getTypeEncoding(method));
    
    Method method = class_getInstanceMethod([self class], toSelector);

    /**
      1 給 Controller 添加替換方法 yy_tableView:didSelectRowAtIndexPath:
      2 把 Controller 中添加的方法實(shí)現(xiàn)在此分類中
     */
    if (class_addMethod([delegate class], toSelector, method_getImplementation(method), method_getTypeEncoding(method))) {
        [YYHook hookClass:[delegate class] fromSelector:fromSelector toSelector:toSelector];
    }
}

- (void)yy_tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [self yy_tableView:tableView didSelectRowAtIndexPath:indexPath];
    /**
      這個(gè)方法聲明在 tableView 所在的 Controller 中毒返!
      所以通過(guò) [self class] 獲取的是 controller 名稱
     */
    NSString *controller = NSStringFromClass([self class]);
    NSLog(@"在%@租幕,點(diǎn)擊第%ld個(gè)cell", controller, indexPath.row);
}

#pragma mark --- tools
- (BOOL)conformSel:(SEL)sel inClz:(Class)class {
    unsigned int count = 0;
    Method *methods = class_copyMethodList(class, &count);
    for (int i = 0; i < count; i++) {
        Method method = methods[i];
        NSString *selString = [NSString stringWithUTF8String:sel_getName(method_getName(method))];
        if ([selString isEqualToString:NSStringFromSelector(sel)]) {
            return YES;
        }
    }
    return NO;
}

思路解釋:
監(jiān)聽 cell的點(diǎn)擊事件 最終目的肯定是hook didSelectRowAtIndexPath 方法舷手。
1 首先 hook setDelegate 方法拧簸,頁(yè)面調(diào)用這個(gè)方法則說(shuō)明實(shí)現(xiàn)了 UITableViewDelegate 代理,接下來(lái)我們就可以從中 hook didSelectRowAtIndexPath 方法了男窟。
2 因?yàn)?UITableViewDelegate 中的 didSelectRowAtIndexPath 方法并不是強(qiáng)制要求實(shí)現(xiàn)的盆赤。所以在 hook 它之前要先判斷頁(yè)面有沒(méi)有實(shí)現(xiàn)這個(gè)代理方法。
3 在頁(yè)面中添加替換方法 yy_tableView:didSelectRowAtIndexPath:歉眷,但是方法實(shí)現(xiàn)寫在此分類中牺六。
4 交換兩個(gè)方法。

思考:
1 如果先進(jìn)行方法交換汗捡,再利用 class_replaceMethod 直接替換頁(yè)面中的 yy_tableView:didSelectRowAtIndexPath: 是否可行淑际?

4 手勢(shì)事件
對(duì)于iOS中的手勢(shì)事件,我們可以 hook initWithTarget:action: 方法來(lái)實(shí)現(xiàn)無(wú)侵入埋點(diǎn)扇住。但是這其中也會(huì)有一些需要注意的地方春缕,直接看代碼吧。

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL fromSelector = @selector(initWithTarget:action:);
        SEL toSelector = @selector(yy_initWithTarget:action:);
        [YYHook hookClass:self fromSelector:fromSelector toSelector:toSelector];
    });
}

- (instancetype)yy_initWithTarget:(id)target action:(SEL)action {
    SEL fromSelector = action;
    SEL toSelector = @selector(yy_action:);
    UIGestureRecognizer *originGesture = [self yy_initWithTarget:target action:action];
 
    // 1 過(guò)濾 target 和 action 為null的情況
    if (!target || !action) {
        return originGesture;
    }
    
    // 2 過(guò)濾 系統(tǒng)類 調(diào)用的 initWithTarget:action: 方法
    NSBundle *mainB = [NSBundle bundleForClass:[target class]];
    if (mainB != [NSBundle mainBundle]) {
        return originGesture;
    }
    
    // 3
    Method method = class_getInstanceMethod([self class], toSelector);
    if (class_addMethod([target class], toSelector, method_getImplementation(method), method_getTypeEncoding(method))) {
        [YYHook hookClass:[target class] fromSelector:fromSelector toSelector:toSelector];
    }
    NSLog(@"---->>>target: %@", [target class]);
    NSLog(@"----<<<action: %@", NSStringFromSelector(action));
    self.clazzName = NSStringFromClass([target class]);
    self.actionName = NSStringFromSelector(action);
    return originGesture;
}

- (void)yy_action:(UIGestureRecognizer *)gesture {
    [self yy_action:gesture];
    NSLog(@"點(diǎn)擊了%@方法艘蹋,位于:%@", gesture.actionName, gesture.clazzName);
}

在 hook initWithTarget:action: 的時(shí)候需要注意一些問(wèn)題锄贼,因?yàn)?initWithTarget:action: 會(huì)被很多系統(tǒng)類調(diào)用,而且還有很多 target 為 null的情況女阀。如果這里不做過(guò)濾會(huì)嚴(yán)重影響性能宅荤。

因?yàn)槭謩?shì)事件往往會(huì)添加給我們自定義的控件屑迂,所以我這里直接通過(guò)target過(guò)濾了所有系統(tǒng)類。
接下去的操作步驟基本和 hook didSelectRowAtIndexPath 方法的思路一樣冯键。
同樣的惹盼,你可以在分類中給手勢(shì)添加兩個(gè)屬性,用來(lái)作為唯一標(biāo)志惫确。

上面幾種情況的事件監(jiān)控只是給大家提供思路逻锐,具體的應(yīng)用還需要做大量的測(cè)試工作。

你們可以通過(guò) BuryDemo 做一些改進(jìn)雕薪。

最后:

本篇開頭有提到過(guò)主要的代碼埋點(diǎn)有三種方式:代碼埋點(diǎn)昧诱、可視化埋點(diǎn)
、無(wú)埋點(diǎn)所袁。其中可視化埋點(diǎn)和無(wú)埋點(diǎn)都屬于無(wú)侵入埋點(diǎn)的方案盏档。埋點(diǎn)的技術(shù)目前還處于初級(jí)階段,怎么安全又全面的統(tǒng)計(jì)用戶的行為也是一個(gè)很大的課題燥爷。
對(duì)此蜈亩,原文中提到使用 Clang AST的接口,在構(gòu)建時(shí)遍歷 AST 前翎,通過(guò)定義的規(guī)則將所需要的埋點(diǎn)代碼直接加進(jìn)去或許也是一種可行的方式稚配。

最后的最后,希望大家一同進(jìn)步港华。加油道川!~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市立宜,隨后出現(xiàn)的幾起案子冒萄,更是在濱河造成了極大的恐慌,老刑警劉巖橙数,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尊流,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡灯帮,警方通過(guò)查閱死者的電腦和手機(jī)崖技,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)钟哥,“玉大人迎献,你說(shuō)我怎么就攤上這事〉纱祝” “怎么了忿晕?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)银受。 經(jīng)常有香客問(wèn)我践盼,道長(zhǎng)鸦采,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任咕幻,我火速辦了婚禮渔伯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘肄程。我一直安慰自己锣吼,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布蓝厌。 她就那樣靜靜地躺著玄叠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拓提。 梳的紋絲不亂的頭發(fā)上读恃,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音代态,去河邊找鬼寺惫。 笑死,一個(gè)胖子當(dāng)著我的面吹牛蹦疑,可吹牛的內(nèi)容都是我干的西雀。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼歉摧,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼艇肴!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起判莉,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤豆挽,失蹤者是張志新(化名)和其女友劉穎育谬,沒(méi)想到半個(gè)月后券盅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡膛檀,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年锰镀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片咖刃。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡泳炉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出嚎杨,到底是詐尸還是另有隱情花鹅,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布枫浙,位于F島的核電站刨肃,受9級(jí)特大地震影響古拴,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜真友,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一黄痪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧盔然,春花似錦桅打、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至站绪,卻和暖如春潦嘶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背崇众。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工掂僵, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人顷歌。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓锰蓬,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親眯漩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子芹扭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

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

  • 概念 代碼埋點(diǎn): 通過(guò)手寫代碼的方式進(jìn)行埋點(diǎn)。代碼埋點(diǎn)存在高度耦合赦抖、依賴發(fā)版舱卡、無(wú)法動(dòng)態(tài)更新、容易誤刪队萤、重復(fù)埋點(diǎn)等問(wèn)...
    朽木自雕也閱讀 971評(píng)論 0 13
  • 簡(jiǎn)單介紹一下 AOP 無(wú)痕埋點(diǎn)最重要的技術(shù)是將埋點(diǎn)代碼從業(yè)務(wù)代碼中剝離轮锥,放到獨(dú)立的模塊中的技術(shù)。寫業(yè)務(wù)的同學(xué)只需按...
    Magic_Unique閱讀 7,826評(píng)論 16 53
  • 前言 隨著公司業(yè)務(wù)的發(fā)展要尔,數(shù)據(jù)的重要性日益體現(xiàn)出來(lái)舍杜。 數(shù)據(jù)埋點(diǎn)的準(zhǔn)確和全面性顯得尤為重要。通過(guò)精準(zhǔn)和詳細(xì)的數(shù)據(jù)赵辕,后...
    MMR無(wú)與倫比閱讀 5,921評(píng)論 2 13
  • 前言 在初期既绩,沒(méi)有做好埋點(diǎn)工作,或者著急趕時(shí)間还惠,未能合理的做好埋點(diǎn)的工作饲握,隨著用戶的增多,就會(huì)有分析用戶的行為需求...
    41c48b8df394閱讀 893評(píng)論 1 4
  • 早上好!靜暖人生:每日一句正能量[玫瑰][玫瑰][玫瑰] (2018年11月16日 農(nóng)歷十月初九 星期五) 牽手救欧,...
    俠姐27687閱讀 162評(píng)論 0 1