美文网首页iOS 开发每天分享优质文章iOS
iOS 无侵入埋点的实践记录及思考

iOS 无侵入埋点的实践记录及思考

作者: 41c48b8df394 | 来源:发表于2019-03-31 12:50 被阅读109次

    前言

    在初期,没有做好埋点工作,或者着急赶时间,未能合理的做好埋点的工作,随着用户的增多,就会有分析用户的行为需求,统计某个页面用户的留存时间,虽然市面上有很多统计的SDK,他们大部分都是需要一个页面一个页面去添加,这对于程序猿来说是很不友好的,工作量又大,又不好管理,突然有一天需要修改某个地方,又要挨个去查找添加的埋点方法,去重新更改一遍。怎么样才能做好统一管理这些埋点的工作,让他们都统一到一块,又方便管理,是我们需要思考的,而且这样也节省了大家的时间。

    思考

    大家都知道objective-c是运行时的机制,所谓运行时就是将数据类型的确定由编译期延迟到了运行时,objective-c是通过runtime来实现的,它是一个非常强大的C语言库 ,这个代码很早以前就开源了,想要了解objective-c,可以看看Apple的Github
    Apple opensource开源代码。我们平时所编写的objective-c代码,会在运行时转换成runtimec语言代码,objective-c通过runtime创建类跟对象,并进行消息的发送与转发。

    在做无侵入埋点的同时,我们需要了解下我们做埋点统计时需要在什么地方进行埋点统计。
    以下是我的埋点思路


    image.png

    实践

    我们确定了需要在什么地方进行埋点,接下来就开始实践,Show me your code

    首先我们写个工具类用来统计页面

    ///后期用到交换方法比较多,统一一个函数进行方法交换
    - (void)ljl_exchangeMethodWithClass:(Class)cls
                            originalSEL:(SEL)originalSEL
                              changeSEL:(SEL)changeSEL{
        Method originalMethod = class_getInstanceMethod(cls, originalSEL);
        Method changeMethod = class_getInstanceMethod(cls, changeSEL);
        method_exchangeImplementations(originalMethod, changeMethod);
    }
    

    记录打印日志统一管理

    - (void)recordHookClass:(Class)cls identifier:(NSString *)identifier{
        NSLog(@"当前类名:%@",NSStringFromClass(cls));
        NSLog(@"标识符:%@",identifier);
        
    }
    

    UIViewController统计

    +(void)load{
            static dispatch_once_t onceToken;
            dispatch_once(&onceToken, ^{
                ///获取
                SEL willAppear = @selector(viewWillAppear:);
                SEL hook_willAppear = @selector(hook_viewWillAppear:);
                [[LJL_HookObjcLog logManage]ljl_exchangeMethodWithClass:self originalSEL:willAppear changeSEL:hook_willAppear];
                
              
                SEL disappear = @selector(viewDidDisappear:);
                SEL hook_disappear = @selector(hook_viewDidDisappear:);
                [[LJL_HookObjcLog logManage]ljl_exchangeMethodWithClass:self originalSEL:disappear changeSEL:hook_disappear];
                
            });
    }
    

    方法实现

    
    - (void)hook_viewWillAppear:(BOOL)animated{
        [[LJL_HookObjcLog logManage] recordHookClass:self.class identifier:@"进入"];
        [self hook_viewWillAppear:animated];
    }
    
    - (void)hook_viewDidDisappear:(BOOL)animated{
        [[LJL_HookObjcLog logManage] recordHookClass:self.class identifier:@"离开"];
        [self hook_viewDidDisappear:animated];
    }
    

    此方案只是针对用户的停留时间及用户的进入次数,日志打印可按需求来统计,不同的需求进行不同的方式。

    UITableView

    UITableViewUICollectionView统计用户点击cell的方法都是在代理中,我们需要进行替换设置delegate的方法,在、setDelegate:方法中插入统计的代码,这里有个小坑,有的页面是没有实现didSelectRowAtIndexPath,为了使得方法不交换可以判断下是否实现了didSelectRowAtIndexPath再进行统计操作。

    Code

    +(void)load{
        
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
    
            SEL originalSEL = @selector(setDelegate:);
            SEL changeSEL = @selector(hook_setDelegate:);
            [[LJL_HookObjcLog logManage]ljl_exchangeMethodWithClass:self originalSEL:originalSEL changeSEL:changeSEL];
            
        });
      
    }
    

    函数实现

    - (void)hook_setDelegate:(id<UITableViewDelegate>)delegate{
            [self hook_setDelegate:delegate];
            Method didSelectmethod = class_getInstanceMethod(delegate.class, @selector(tableView:didSelectRowAtIndexPath:));
            IMP hookIMP = class_getMethodImplementation(self.class, @selector(hook_tableView:didSelectRowAtIndexPath:));
            
            char const* type = method_getTypeEncoding(didSelectmethod);
            class_addMethod(delegate.class, @selector(hook_tableView:didSelectRowAtIndexPath:), hookIMP, type);
            Method hookMethod = class_getInstanceMethod(delegate.class, @selector(hook_tableView:didSelectRowAtIndexPath:));
            method_exchangeImplementations(didSelectmethod, hookMethod);
    
    }
    
    - (void)hook_tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
        [[LJL_HookObjcLog logManage] recordHookClass:self.class identifier:[NSString stringWithFormat:@"%ld,%ld",indexPath.row,indexPath.section]];
        [self hook_tableView:tableView didSelectRowAtIndexPath:indexPath];
    }
    

    UIButton的点击事件

    +(void)load{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            ///获取
            
            SEL originalSEL = @selector(sendAction:to:forEvent:);
            SEL changeSEL = @selector(hook_sendAction:to:forEvent:);
            [[LJL_HookObjcLog logManage]ljl_exchangeMethodWithClass:self originalSEL:originalSEL changeSEL:changeSEL];
        });
    }
    
    ///MAKR:
    - (void)hook_sendAction:(SEL)action
                         to:(nullable id)target
                   forEvent:(nullable UIEvent *)event{
        [self hook_sendAction:action to:target forEvent:event];
        ///点击事件结束记录
        if ([[event.allTouches anyObject]phase] == UITouchPhaseEnded) {
            [[LJL_HookObjcLog logManage] recordLogActionHookClass:[target class] action:action identifier:@"UIButton"];
        }
    }
    

    UIGestureRecognizer手势的Hook方法

    @implementation UIGestureRecognizer (Log_Category)
    
    +(void)load{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            ///获取
            
            SEL originalSEL = @selector(initWithTarget:action:);
            SEL changeSEL = @selector(hook_initWithTarget:action:);
            [[LJL_HookObjcLog logManage]ljl_exchangeMethodWithClass:self originalSEL:originalSEL changeSEL:changeSEL];
        });
    }
    
    - (instancetype)hook_initWithTarget:(nullable id)target action:(nullable SEL)action{
        UIGestureRecognizer *gestureRecognizer = [self hook_initWithTarget:target action:action];
        SEL changeSEL = @selector(hook_gestureAction:);
        IMP hookIMP = class_getMethodImplementation(self.class, changeSEL);
        const char *type = method_getTypeEncoding(class_getInstanceMethod([target class], action));
        class_addMethod([target class], changeSEL, hookIMP, type);
        
        [[LJL_HookObjcLog logManage]ljl_exchangeMethodWithClass:[target class] originalSEL:action changeSEL:changeSEL];
        
        
        return gestureRecognizer;
    }
    
    - (void)hook_gestureAction:(id)sender{
        [self hook_gestureAction:sender];
        [[LJL_HookObjcLog logManage] recordLogActionHookClass:[sender class] action:@selector(action) identifier:@"手势"];
    
    }
    
    @end
    
    

    思考总结

    本文简单讲述无侵入埋点的统计方案,思路大致上是通过Runtime的运行机制,在运行期可以向类中新增或替换选择子所对应的方法实现。使用另外一份实现原有的方法实现。
    在无侵入的基础上,即降低了代码的耦合,又方便了后期维护管理,相对于可视化埋点,方便简单,所有方式都会有优点与缺点。
    本文描述的优点就是无侵入,低耦合,好管理维护。
    缺点:有些页面是复用机制,比如cell的复用,一个控制器可能多次进入,需要我们做好统一管理的标识符,一个button的点击需要递归获取当前的控制器等操作。有些模块可能会出现统计不准确等因素,还有可能团队人员多了,定义的方法有时候都是一致的,这样对于这种无侵入的方式最终的效果是不太准确的。相比较可视化埋点,数据统计的更加合理,准确,维护成本略高
    每个项目所要统计的内容不一致,精确的程度也不一样,都是各自的观点,本文只是自己的理解与记忆,如果你又什么更好的方案可以留言分享,谢谢。

    可参考链接
    有货 iOS 数据非侵入式自动采集探索实践
    网易HubbleData无埋点SDK在iOS端的设计与实现

    相关文章

      网友评论

        本文标题:iOS 无侵入埋点的实践记录及思考

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