美文网首页埋点
iOS UI控件埋点技术方案之基于runtime hook

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

作者: huxinwen | 来源:发表于2019-04-29 15:12 被阅读0次

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

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

    相关文章

      网友评论

        本文标题:iOS UI控件埋点技术方案之基于runtime hook

        本文链接:https://www.haomeiwen.com/subject/ekoenqtx.html