iOS UI控件埋点技术方案之基于runtime hook

       关于大数据,记得马爸爸说过一句话,具体哪几个字忘了,但大概的意思是:未来数据就是最大的财富。互联网发展到今天,特别是移动互联网,数据就是财富已经开始在被验证,各个互联网公司都在从各个方面收集提炼数据,分析数据,然后变成财富。^_^

        说了这么多,还没切入正题,好吧,对于我们iOS客户端,也有大量的数据需要收集,通过统计客户对于我们app的使用行为,不断的改进我们的app,通过分析趋势,拓展公司的盈利模式,改变公司经营战略都有可能。所有说这么多,就是想说明收集用户行为数据对于我们app来说也是很重要的。

        那么,怎样才能进行数据收集?比如,用户在界面点击了某个按钮,很容易想到的是打印日志文件,一个按钮可以,一个app上存在成千上百个按钮,手势,界面,都打印日志,这样导致日志文件有很多,考虑后续我们从日志文件提炼用户行为的数据比较庞大复杂,既无形的增加了开发的工作量,也增加了数据分析的工作量。所以说最好的办法还是“专人专事”,即:app中有个模块,再不影响其他业务逻辑的情况下,单独负责数据统计收集,这就是埋点技术。

一、埋点控件

        哪些控件需要埋点呢,根据用户与app的交互方式包括点击按钮(UIControl)、手势(UIGestureRecognizer)、列表某一行的点击(UITableView)、查看了某个界面(UIViewController)等,以及公司的业务需求(可以用配置文件的方式)。本文具体讲交互方式,即UI控件交互方式的捕捉。

二、埋点的技术架构


埋点架构

三、交互UI的事件捕捉

1、原理

利用runtime运行时机制,将类原生方法替换成用户自定义的方法,相当于强行在原本调用栈中插入一个方法,我们在其中插入一段统计代码即可,需要注意的是不要多次替换,谨防其他代码重复替换。

1.1、黑魔法原理

Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。

交换原理

1.2、黑魔法用法

先给要替换的方法的类添加一个Category,然后在Category中的+(void)load方法中添加Method Swizzling方法,我们用来替换的方法也写在这个Category中。

由于load类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。

2、UI埋点实现技术方案

2.1、类图

实现类图

2.2、UI埋点具体实现

2.2.1、SwizzManager实现(hook工具类)

/** *方法交换 *@param clazz 交换的类 *@param originSel 需要交换的原始方法sel *@param newSel 动态方法sel */

+ (void)swizzMethodForClass:(Class)clazz originSel:(SEL)originSel newSel:(SEL)newSel{

    swizzleMethod(clazz, originSel, newSel);

}

/** *动态添加方法并交换 *@param clazz 交换的类 *@param impClass 动态方法的imp所在类 *@param impSel 动态方法的imp对应的sel *@param originSel 需要交换的原始方法sel *@param newSel 动态方法sel */

+ (void)swizzMethodForClass:(Class)clazz newSelImpClass:(Class)impClass impSel:(SEL)impSel originSel:(SEL)originSel newSel:(SEL)newSel{

    IMP newImp = method_getImplementation(class_getInstanceMethod(impClass, impSel));

    BOOLresult =class_addMethod(clazz, newSel, newImp,nil);

    result = result && [selfcontainsSel:originSelinClass:clazz];

    if(result) {

        swizzleMethod(clazz, originSel, newSel);

    }

}

///类是否包含方法

+ (BOOL)containsSel:(SEL)sel inClass:(Class)class{

    unsignedintcount;

    Method*methodList =class_copyMethodList(class,&count);

    for(inti =0; i < count; i++) {

        Methodmethod = methodList[i];

        NSString *tempMethodString = [NSString stringWithUTF8String:sel_getName(method_getName(method))];

        if([tempMethodStringisEqualToString:NSStringFromSelector(sel)]) {

            returnYES;

        }

    }

   return NO;

}

///交换方法

voidswizzleMethod(Classclass,SELoriginalSelector,SELswizzledSelector)

{

    // the method might not exist in the class, but in its superclass

    MethodoriginalMethod =class_getInstanceMethod(class, originalSelector);

    MethodswizzledMethod =class_getInstanceMethod(class, swizzledSelector);

 // class_addMethod will fail if original method already exists

    BOOLdidAddMethod =class_addMethod(class, originalSelector,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));

// the method doesn’t exist and we just added one

    if(didAddMethod) {

        class_replaceMethod(class, swizzledSelector,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));

    }

    else{

        method_exchangeImplementations(originalMethod, swizzledMethod);

    }

}

2.2.2、UIControl (Analysis)

当操作事件发生时,底层主动调用该方法sendAction:to:forEvent:来触发action,因此通过hook该方法,就可以拿到用户的UI事件,例如UIButton的点击事件,具体实现如下:

+(void)load{

    staticdispatch_once_tonceToken;

    dispatch_once(&onceToken, ^{


        Classclazz = [selfclass];

        SELoriginalSelector =@selector(sendAction:to:forEvent:);

        SELnewSelector =@selector(hxw_sendAction:to:forEvent:);

     [SwizzManagerswizzMethodForClass:clazzoriginSel:originalSelectornewSel:newSelector];

    });

}

///自定义发送点击响应方法

-(void)hxw_sendAction:(SEL)action to:(id)target forEvent:(UIEvent*)event{

    [selfhxw_sendAction:actionto:targetforEvent:event];

    [__analysisDelegateUIControl hxw_UIControlSendAction:action target:target forEvent:event];

}

2.2.3、UIGestureRecognizer (Analysis)

我们在初始化手势的时候,会给手势添加响应事件,但是手势不像UIControl那样,暴漏了相应事件主动调用的action的方法,但是没关系,我们知道绑定action的方法,就是通过hook绑定事件的方法,拿到相应的action和target,然后hook住target的action方法(先给target添加一个方法,然后与action交换),在hook的方法中就可以得到事件的响应时机,具体实现如下:

+(void)load{

    staticdispatch_once_tonceToken;

    dispatch_once(&onceToken, ^{


        SELoriginSel =@selector(initWithTarget:action:);

        SELnewSel =@selector(hxw_initWithTarget:action:);

        [SwizzManagerswizzMethodForClass:[selfclass]originSel:originSelnewSel:newSel];


        SELoriginSel1 =@selector(addTarget:action:);

        SELnewSel1 =@selector(hxw_addTarget:action:);

        [SwizzManagerswizzMethodForClass:[selfclass]originSel:originSel1newSel:newSel1];

    });


}

-(instancetype)hxw_initWithTarget:(id)target action:(SEL)action{

    UIGestureRecognizer* gestureRecognize = [selfhxw_initWithTarget:targetaction:action];

    if(!target && !action) {

        returngestureRecognize;

    }

    if([targetisKindOfClass:[UIScrollViewclass]]) {

        returngestureRecognize;

    }

    [selfhandleTarget:targetaction:action];


    returngestureRecognize;

}

-(void)hxw_addTarget:(id)target action:(SEL)action{

    [selfhxw_addTarget:targetaction:action];

    [selfhandleTarget:targetaction:action];

}

- (void)handleTarget:(id)target action:(SEL)action{


    Classclazz = [targetclass];

    NSString* newMethodName = [NSString stringWithFormat:@"hxw_%@_%@",NSStringFromClass(clazz),NSStringFromSelector(action)];

    SELnewSel =NSSelectorFromString(newMethodName);

    SELimpSel =@selector(respondActionForGestureRecognize:);

    // 向类身上添加方法并交换

    [SwizzManager swizzMethodForClass:clazz newSelImpClass:[self class] impSel:impSel originSel:action newSel:newSel];


    self.name= newMethodName;

}

- (void)respondActionForGestureRecognize:(UIGestureRecognizer*)gestureRecognize{

    ///调用原始action,self为target

    NSString* identifier = gestureRecognize.name;

    SELsel =NSSelectorFromString(identifier);

    if ([self respondsToSelector:sel]) {

        IMPimp = [selfmethodForSelector:sel];

        void(*func)(id,SEL,id) = (void*)imp;

        func(self,sel,gestureRecognize);

    }


    [__analysisDelegateUIGesture hxw_UIGestureCognizedRespondAction:gestureRecognize];

}

2.2.4、UIViewController (Analysis)

UIViewController这个简单,只需要hook住UIViewController的时机viewDidLoad、viewWillAppear、viewDidDisappear方法就可以完成页面的统计

+ (void)load{

    staticdispatch_once_tonceToken;

    dispatch_once(&onceToken, ^{

        SELoriginSel =@selector(viewDidLoad);

        SELnewSel =@selector(hxw_viewDidLoad);

        [SwizzManagerswizzMethodForClass:[selfclass]originSel:originSelnewSel:newSel];


        SELoriginSel1 =@selector(viewWillAppear:);

        SELnewSel1 =@selector(hxw_viewWillAppear:);

        [SwizzManagerswizzMethodForClass:[selfclass]originSel:originSel1newSel:newSel1];


        SELoriginSel2 =@selector(viewDidDisappear:);

        SELnewSel2 =@selector(hxw_viewDidDisappear:);

        [SwizzManagerswizzMethodForClass:[selfclass]originSel:originSel2newSel:newSel2];

    });

}

- (void)hxw_viewDidLoad{

    [self hxw_viewDidLoad];

    [__analysisDelegateUIViewController hxw_viewDidLoad:self];

}

- (void)hxw_viewWillAppear:(BOOL)animated{

    [self hxw_viewWillAppear:animated];

    [__analysisDelegateUIViewController hxw_viewWillAppear:animated viewController:self];

}

- (void)hxw_viewDidDisappear:(BOOL)animated{

    [self hxw_viewDidDisappear:animated];

    [__analysisDelegateUIViewController hxw_viewWillDisappear:animated viewController:self];

}

2.2.5、UITableView (Analysis)

UITableView的相应事件,就是点击cell,而点击cell则是代理方法- (void)hxw_tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath,因此首先我们先要拿到实现代理的类,这就需要hook住setDelegate:这个方法拿到代理delegate,然后hook住代理的- (void)hxw_tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath,与手势的思路实现有点类似。具体实现如下:

+(void)load{

    staticdispatch_once_tonceToken;

    dispatch_once(&onceToken, ^{

        SELoriginSel =@selector(setDelegate:);

        SELnewSel =@selector(hxw_setDelegate:);

        [SwizzManagerswizzMethodForClass:[selfclass]originSel:originSelnewSel:newSel];

    });

}

- (void)hxw_setDelegate:(id)delegate{

    [self hxw_setDelegate:delegate];

    Class delegateClass = [delegate class];

    SEL originSel =@selector(tableView:didSelectRowAtIndexPath:);

    NSString* newSelName = [NSStringstringWithFormat:@"hxw_%ld_%@",self.tag,NSStringFromSelector(originSel)];

    SEL newSel =NSSelectorFromString(newSelName);

    SEL impSel =@selector(hxw_tableView:didSelectRowAtIndexPath:);

    ///动态添加方法并交换

    [SwizzManager swizzMethodForClass:delegateClass newSelImpClass:[self class] impSel:impSel originSel:originSel newSel:newSel];

}

- (void)hxw_tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath{


    ///执行原始方法,交换后为hxw_tag_tableView:didSelectRowAtIndexPath:,指向原始方法- (void)tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath这个的IMP

    ///这里的self实际上为delegate

    NSString* selName = [NSStringstringWithFormat:@"hxw_%ld_%@",tableView.tag,NSStringFromSelector(@selector(tableView:didSelectRowAtIndexPath:))];

    SEL swizzSel =NSSelectorFromString(selName);

    if([self respondsToSelector:swizzSel]) {

        IMP imp = [self methodForSelector:swizzSel];

        void(*func)(id,SEL,id,id) = (void*)imp;

        func(self, swizzSel, tableView, indexPath);

    }

    [__analysisDelegateUITableView hxw_tableView:tableView didSelectRowAtIndexPath:indexPath delagete:(id<UITableViewDelegate>)self];


}

3、集成

导入后只需要,在AppDelegate的didFinishlaunch方法中,调用[AnalysisManager shareInstance]初始化,并将代理设置给他,最后实现AnalysisDelegate的接口方法即可。

具体见demo

参考:

iOS无埋点数据统计实践

iOS 无痕埋点方案探究

iOS开发·runtime原理与实践: 方法交换篇(Method Swizzling)(iOS“黑魔法”,埋点统计,禁止UI控件连续点击,防奔溃处理)

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

推荐阅读更多精彩内容

  • 一些公用类: @interface CustomClass : NSObject - (void) fun1; @...
    xh_0129阅读 530评论 0 0
  • 一、简介 <<UITableView(或简单地说,表视图)的一个实例是用于显示和编辑分层列出的信息的一种手段 <<...
    无邪8阅读 10,471评论 3 3
  • 转自:https://www.jianshu.com/p/10b2323f502e 1、禁止手机睡眠 [UIApp...
    aggie1024阅读 2,576评论 0 6
  • 1、禁止手机睡眠 [UIApplication sharedApplication].idleTimerDisab...
    小小夕舞阅读 1,388评论 1 1
  • 本文转载自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex阅读 719评论 0 1