无侵入埋点方案

目录

小菜

埋点解决两大问题

埋点方式

主菜-无侵入埋点实现方案

运行时方法替换(即HOOK方法)

话不多说直接上代码

总结

饭后甜点

Clang AST 实现无侵入埋点

Swift怎么实现运行时方法置换

实现方式

思考    


小菜

埋点解决两大问题

  1. 了解用户行为,为改进产品方案提供数据支持
  2. 根据用户行为降低分析线上问题的难度

埋点方式

  • 代码埋点主要就是通过手写代码的方式来埋点,能很精确的在需要埋点的代码处加上埋点的代码,可以很方便地记录当前环境的变量值,方便调试,并跟踪埋点内容,但存在开发工作量大,并且埋点代码到处都是,后期难以维护等问题。(手动埋点,想埋哪就埋哪;但是消耗了开发成本,同时到处散落着埋点代码让代码不好看,也难以维护)
  • 可视化埋点,就是将埋点增加和修改的工作可视化了,提升了增加和维护埋点的体验。(什么技术实现可视化埋点?)
  • 无埋点,并不是不需要埋点,而更确切地说是“全埋点”,而且埋点代码不会出现在业务代码中,容易管理和维护。它的缺点在于,埋点成本高,后期的解析也比较复杂,再加上 view_path 的不确定性。所以,这种方案并不能解决所有的埋点需求,但对于大量通用的埋点需求来说,能够节省大量的开发和维护成本。

主菜-无侵入埋点实现方案

运行时方法替换(即HOOK方法)

话不多说直接上代码

        交换方法类SMHook


#import "SMHook.h"
#import <objc/runtime.h>

@implementation SMHook

+ (void)hookClass:(Class)classObject fromSelector:(SEL)fromSelector toSelector:(SEL)toSelector {
    Class class = classObject;
    // 得到被替换类的实例方法
    Method fromMethod = class_getInstanceMethod(class, fromSelector);
    // 得到替换类的实例方法
    Method toMethod = class_getInstanceMethod(class, toSelector);
    
    // class_addMethod 返回成功表示被替换的方法没实现,然后会通过 class_addMethod 方法先实现;返回失败则表示被替换方法已存在,可以直接进行 IMP 指针交换 
    if(class_addMethod(class, fromSelector, method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
      // 进行方法的替换
        class_replaceMethod(class, toSelector, method_getImplementation(fromMethod), method_getTypeEncoding(fromMethod));
      // 这里是否也可以直接用交换指针的方法替换???
    } else {
      // 交换 IMP 指针
        method_exchangeImplementations(fromMethod, toMethod);
    }

}

@end

        页面进入次数、页面停留时间都需要对 UIViewController 生命周期进行埋点,通过类名来区别不同的 UIViewController。

        代码如下:


@implementation UIViewController (logger)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 通过 @selector 获得被替换和替换方法的 SEL,作为 SMHook:hookClass:fromeSelector:toSelector 的参数传入 
        SEL fromSelectorAppear = @selector(viewWillAppear:);
        SEL toSelectorAppear = @selector(hook_viewWillAppear:);
        [SMHook hookClass:self fromSelector:fromSelectorAppear toSelector:toSelectorAppear];
        
        SEL fromSelectorDisappear = @selector(viewWillDisappear:);
        SEL toSelectorDisappear = @selector(hook_viewWillDisappear:);
        
        [SMHook hookClass:self fromSelector:fromSelectorDisappear toSelector:toSelectorDisappear];
    });
}

- (void)hook_viewWillAppear:(BOOL)animated {
    // 先执行插入代码,再执行原 viewWillAppear 方法
    [self insertToViewWillAppear];
    // 注意这里因为方法指针已经被交换了,所以调用hook_viewWillAppear
    [self hook_viewWillAppear:animated];
}

- (void)hook_viewWillDisappear:(BOOL)animated {
    // 执行插入代码,再执行原 viewWillDisappear 方法
    [self insertToViewWillDisappear];
    // 注意这里因为方法指针已经被交换了,所以调用hook_viewWillDisappear
    [self hook_viewWillDisappear:animated];
}

- (void)insertToViewWillAppear {
    // 在 ViewWillAppear 时进行日志的埋点
    [[[[SMLogger create]
       message:[NSString stringWithFormat:@"%@ Appear",NSStringFromClass([self class])]]
      classify:ProjectClassifyOperation]
     save];
}

- (void)insertToViewWillDisappear {
    // 在 ViewWillDisappear 时进行日志的埋点
    [[[[SMLogger create]
       message:[NSString stringWithFormat:@"%@ Disappear",NSStringFromClass([self class])]]
      classify:ProjectClassifyOperation]
     save];
}
@end

        对于点击事件来说,我们也可以通过运行时方法替换的方式进行无侵入埋点。代码如下:


+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 通过 @selector 获得被替换和替换方法的 SEL,作为 SMHook:hookClass:fromeSelector:toSelector 的参数传入
        SEL fromSelector = @selector(sendAction:to:forEvent:);
        SEL toSelector = @selector(hook_sendAction:to:forEvent:);
        [SMHook hookClass:self fromSelector:fromSelector toSelector:toSelector];
    });
}

- (void)hook_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    [self insertToSendAction:action to:target forEvent:event];
    [self hook_sendAction:action to:target forEvent:event];
}

- (void)insertToSendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    // 日志记录
    if ([[[event allTouches] anyObject] phase] == UITouchPhaseEnded) {
        NSString *actionString = NSStringFromSelector(action);
        NSString *targetName = NSStringFromClass([target class]);
        [[[SMLogger create] message:[NSString stringWithFormat:@"%@ %@",targetName,actionString]] save];
    }
}

        除了 UIViewController、UIButton 控件以外,Cocoa 框架的其他控件都可以使用这种方法来进行无侵入埋点。以 Cocoa 框架中最复杂的 UITableView 控件为例,你可以使用 hook setDelegate 方法来实现无侵入埋点。另外,对于 Cocoa 框架中的手势事件(Gesture Event),我们也可以通过 hook initWithTarget:action: 方法来实现无侵入埋点。

总结

        这套方案由于唯一标识难以维护和准确性难以保障的原因,很难被全面采用,一般都只是用于一些功能和视图稳定的地方,手动侵入式埋点方式依然占据大部分场景。

饭后甜点

Clang AST 实现无侵入埋点

        使用 Clang AST 的接口,在构建时遍历 AST,通过定义的规则将所需要的埋点代码直接加进去,可能会更加合适。这时,我们可以使用前一篇文章“如何利用 Clang 为 App 提质?”中提到的 LibTooling 来开发一个独立的工具,专门以静态方式插入埋点代码。这样做,既可以享受到手动埋点的精确性,还能够享受到无侵入埋点方式的统一维护、开发解耦、易维护的优势。

Swift怎么实现运行时方法置换

参考swiftlint使用 & swift替换AppIcon & Swift替换方法 - 简书Swift之交换/替换方法 - Woodjobber

    Swift 是一种强类型语言。即默认类型是安全的静态类型。纯Swift类的函数调用已经不再是OC的运行时发送消息,而是类似于C++的vtable,在编译时就确定了调用哪个函数,所以没法通过runtime获取方法,属性。Swift中的动态性可以通过OC运行时来获得,动态性最常用的就是方法替换(Method Swizzling)。

        Swift中的动态修饰符

  • @objc: 将`Swift 函数 暴露给 OC运行时,但是它仍然不能保证完全动态,编译器会尝试去对它做静态优化
  • @dynamic: 动态功能修饰符,它能保证函数,属性可以获得动态性

实现方式

1. 用Swift中的unsafeBitCast 实现,代码如下:

extension SayHello {
    class func swizzleSayHello() {
        let originalSlector = #selector(SayHello.sayHello(_:))
        let originalMethod = class_getInstanceMethod(SayHello.self, originalSlector)!
        let originalImplementation = method_getImplementation(originalMethod)
        //id (*IMP)(id, SEL, ...)
        typealias originalClosureType = @convention(c) (AnyObject,Selector,String) -> Void
        let originalClosure:originalClosureType = unsafeBitCast(originalImplementation, to: originalClosureType.self)
        let newBlock:@convention(block)(AnyObject,String) -> Void = {
            obj, name in
            var name__ = name
            name__ = name + ",nice to meet you"
            return originalClosure(obj,originalSlector,name__)
        }
            //Its signature should be: method_return_type ^(id self, method_args...).
        let newImplementation = imp_implementationWithBlock(unsafeBitCast(newBlock, to: AnyObject.self))
        print(newImplementation)
        method_setImplementation(originalMethod, newImplementation)
    }
}

2.方法替换

extension SayHello {
    //Argument of '#selector' refers to instance method 'swizzleSayHello' that is not exposed to Objective-C
    //Add '@objc' to expose this instance method to Objective-C
    //@objc 表示 暴露 方法给 oc 调用
     @objc private func swizzledSayHello(_ name: String) -> Void {
        var name__ = name
        name__ = name + ",nice to meet you"
        return swizzledSayHello(name__)
    }
    class func swizzledSayHello() {
        
        let originalSelctor = #selector(SayHello.sayHello(_:))
        let swizzledSelector = #selector(SayHello.swizzledSayHello(_:))
        swizzleMethod(for: SayHello.self, originalSelector: originalSelctor, swizzledSelector: swizzledSelector)
    }
    private class func swizzleMethod(for aClass: AnyClass, originalSelector: Selector,swizzledSelector: Selector) {
        let originalMethod = class_getInstanceMethod(aClass, originalSelector)
        let swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector)
        let didAddMethod = class_addMethod(aClass, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!))
        if didAddMethod {
            class_replaceMethod(aClass, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!))
        } else {
            method_exchangeImplementations(originalMethod!, swizzledMethod!)
        }
    }
}

        在AppDelegate的启动方法里调用(显得有点low。。。)

3. 创建OC拓展文件, 将文件引入到混合开发桥接文件中。(多个拓展文件的+load()方法会被覆盖吗,+initialize()方法呢???)

 参考:App 启动速度怎么做优化与监控?_学海无涯乐作舟的博客-CSDN博客

+load()调用情况如下(类或者分类被加载时调用):

1.先调用类的+load()方法,先编译(build phases的添加顺序)哪个就先调用哪个;

2.调用该类的+load()方法之前,先调用父类的+load()方法;

3.分类的按照编译先后顺序调用+load方法;

        总结下来,就是独立制的。父类优先子类,类优先分类,类和分类之间按照谁先编译谁先调用。谁实现了就会按照顺序去调用谁,不会覆盖。

+initialize()调用情况如下(类被初始化时调用):

1.初始化时先调用父类,再调用子类;

2.如果子类未实现+initialize(),则调用父类;

3.分类的+initialize()会覆盖掉原来类的+initialize();

        总结下来,是属于责任制的。父类优先子类调用,子类不行父类上,分类实现了本类闲。这里注意,如果多个分类都实现了,按照编译先后顺序,最后一个分类的实现覆盖前面的。

思考    

        如本类和类别都做了方法hook,而load中hook则能满足这种场景。放在load中注意不要影响启动时间,所以根据业务场景选择是在load还是在InInitialize中hook。

        其实,也可以通过继承基类,在基类中埋点实现。不过这种方式对于代码是有侵入的,不够灵活。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java Agent是一种Java应用程序的扩展方式,通过在应用程序启动时加载Agent,可以在不修改原始代码的情况下实现对应用程序的监控和修改。Agent可以通过字节码注入的方式,在运行时动态修改应用程序的字节码,实现埋点操作。 埋点是指在应用程序中插入一些代码,用于记录关键的业务逻辑和性能指标。通过在Java Agent中实现埋点功能,可以方便地获取应用程序的执行过程和性能数据,以及关键业务逻辑的执行情况。 Java Agent埋点的实现方式通常涉及对类加载机制的hook和字节码编译技术。Agent可以通过在类加载之前修改字节码,将需要埋点的代码动态插入到应用程序中。埋点代码可以是用于记录日志、收集性能数据、统计方法执行时间等。 与传统的静态埋点相比,Java Agent埋点的优势在于不需要修改原始代码,且可以在运行时动态修改应用程序的行为。这种方式不会对原始代码产生任何影响,也不会增加部署的复杂性。同时,Java Agent还可以提供更加细粒度的监控和修改能力,可以对特定的方法、类和类加载器进行监控和修改。 总之,Java Agent埋点是一种非侵入式的监控和修改应用程序的方式。通过加载Agent并在运行时修改字节码,可以方便地实现埋点操作,用于记录关键的业务逻辑和性能指标,从而提供更好的应用程序监控和调优能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值