iOS 开发每天分享优质文章iOS

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

2019-03-31  本文已影响109人  41c48b8df394

前言

在初期,没有做好埋点工作,或者着急赶时间,未能合理的做好埋点的工作,随着用户的增多,就会有分析用户的行为需求,统计某个页面用户的留存时间,虽然市面上有很多统计的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端的设计与实现

上一篇下一篇

猜你喜欢

热点阅读