埋点

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

2019-04-29  本文已影响0人  huxinwen

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

        说了这么多,还没切入正题,好吧,对于我们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控件连续点击,防奔溃处理)

上一篇 下一篇

猜你喜欢

热点阅读