iOS 中对实例对象进行hook的方式

1,440 阅读6分钟

iOS 中setDelegate: 进行hook的方式

最近在做埋点服务, 需要对UITableViewUITableViewDelegate方法tableView:didSelectRowAtIndexPath:进行hook, 这里对常见的几种HOOK方式进行总结.

首先需要常见的 Method-Swizzling 分类方法:

@interface NSObject (WBAutoTrack)
+ (BOOL)wb_autotrack_swizzleMethod:(SEL)origSel_ withMethod:(SEL)altSel_ error:(NSError **)error_;
@end

@implementation NSObject (WBAutoTrack)
+ (BOOL)wb_autotrack_swizzleMethod:(SEL)origSel_ withMethod:(SEL)altSel_ error:(NSError *__autoreleasing _Nullable *)error_{
    // 可能获取到的 Method是从父类中出来的
    Method origMethod = class_getInstanceMethod(self, origSel_);
    if (!origMethod) {
        NSString *errstring = [NSString stringWithFormat:@"original method %@ not found for class %@", NSStringFromSelector(origSel_), [self class]];
        NSAssert(NO, errstring);
        return NO;
    }
    
    Method altMethod = class_getInstanceMethod(self, altSel_);
    if (!altMethod) {
        NSString *errstring = [NSString stringWithFormat:@"alternate method %@ not found for class %@", NSStringFromSelector(altSel_), [self class]];
        NSAssert(NO, errstring);
        return NO;
    }
    
    class_addMethod(self,
                    origSel_,
                    class_getMethodImplementation(self, origSel_),
                    method_getTypeEncoding(origMethod));
    class_addMethod(self,
                    altSel_,
                    class_getMethodImplementation(self, altSel_),
                    method_getTypeEncoding(altMethod));
    
    Method m1 = class_getInstanceMethod(self, origSel_);
    Method m2 = class_getInstanceMethod(self, altSel_);
    method_exchangeImplementations(m1, m2);
    return YES;
}
@end

这里的实现基本能满足需求, 如果寻求一些更加安全的写法可以参考第三方的RSSwizzle 或者Aspects, 但是这里用这种方式已经够用.

首先,我们需要在合适的时机HOOK UITableView的setDelegate:, 然后设置时, HOOK delegate对象:

@implementation UITableView(Category)
// 实现UITableView+Category 分类
+(void)load{
	[UITableView wb_autotrack_swizzleMethod:@selector(setDelegate:)
                                withMethod:@selector(wbautotrack_setDelegate:)
                                     error:nil];
}

-(void)wbautotrack_setDelegate:(id<UITableViewDelegate>)delegate{
    // 调用原来设置Delegate方法
    [self wbautotrack_setDelegate:delegate];
		
    // TODO 需要 想办法 HOOK Delegate 的 tableView:didSelectRowAtIndexPath: 方法
}
@end

一般而言, 有以下三种方法:

  1. method-swizzling方法交换
  2. isa swizzling 动态子类
  3. 使用NSProxy代理转发

1. method-swizzling方法交换

方法交换的方式很简单:

-(void)wbautotrack_setDelegate:(id<UITableViewDelegate>)delegate{
    // 调用原来设置Delegate方法
    [self wbautotrack_setDelegate:delegate];

    // 方案一:
    // 在设置delegate时, 先交换delegate对象中的 tableView:didSelecRowAtIndexPath: 方法与我们自己的实现的方法
    [self wbautotrack_swizzleDidSelectRowAtIndexPathMethodWithDelegate:delegate];
}

// 需要动态给 delegate对象添加一个方法, 直接交换delegate实例对象的类的方法!!!
-(void)wbautotrack_swizzleDidSelectRowAtIndexPathMethodWithDelegate:(id)delegate{
    // 需要交换的方法名
    SEL sourceSelector = @selector(tableView:didSelectRowAtIndexPath:);
    // delegate 没有实现该方法时, 直接返回
    if (![delegate respondsToSelector:sourceSelector]) {
        return;
    }
    
    SEL destinationSelector = NSSelectorFromString(@"wbautotrack_tableView:didSelectRowAtIndexPath:");
    // 当delegate 对象中已经存在这个 destinationSelector, 说明已经交换了, 可以直接返回
    if ([delegate respondsToSelector:destinationSelector]) {
        return;
    }
    
    // 获取 delegate instance对象的类
    Class delegateClass = [delegate class];
    Method sourceMethod = class_getInstanceMethod(delegateClass, sourceSelector);
    const char *encoding = method_getTypeEncoding(sourceMethod);
    
    // 该类中已经存在相同的方法, 则会添加方法失败!!!
    if (!class_addMethod([delegate class], destinationSelector, (IMP)wbautotrack_tableViewDidSelectRow, encoding)) {
        NSLog(@"Add %@ to %@ error", NSStringFromSelector(sourceSelector), [delegate class]);
        return;
    }
    
    //方法添加成功以后, 再进行方法交换
    [delegateClass wb_autotrack_swizzleMethod:sourceSelector withMethod:destinationSelector error:nil];
}


// 方案1的直接修改IMP的方式
// 这个IMP的 selector 使用的是 wbautotrack_tableView:didSelectRowAtIndexPath:
static void wbautotrack_tableViewDidSelectRow(id object, SEL selector, UITableView *tableView, NSIndexPath *indexPath) {
    SEL destinationSelector = NSSelectorFromString(@"wbautotrack_tableView:didSelectRowAtIndexPath:");
    // 通过消息发送, 调用原始的 tableview:didSelectRowAtIndexPath: 方法
    ((void(*)(id, SEL, id, id))objc_msgSend)(object, destinationSelector, tableView, indexPath);
    //TODO 触发 Click 事件
}

这个方案的风险很高!!! 因为单独一个 delegate instance 的某个方法需要HOOK, 将 delegate class 所有的指定SEL的实现全部替换了!!!

2. isa swizzling 动态子类

对于单独的某一个 instance 对象某个方法需要hook, 可以参考 Aspects 或者 KVO方式, 使用 isa-swizzling 方式:

-(void)wbautotrack_setDelegate:(id<UITableViewDelegate>)delegate{
    // 调用原来设置Delegate方法
    [self wbautotrack_setDelegate:delegate];

    // 方案二: 创建一个动态子类, 或者成为动态代理
    [WBAutoTrackTableViewDynamicClass proxyWithTableViewDelegate:delegate];
}

其中动态代理方式, 就是:

@interface WBAutoTrackTableViewDynamicClass : NSObject
+(void)proxyWithTableViewDelegate:(id<UITableViewDelegate>)delegate;
@end
// delegate 对象的子类前缀
static NSString *const kWBAutoTrackDelegatePrefix = @"com.pp.autotrack.";
@implementation WBAutoTrackTableViewDynamicClass
+(void)proxyWithTableViewDelegate:(id<UITableViewDelegate>)delegate{
    SEL originalSelector = NSSelectorFromString(@"tableView:didSelectRowAtIndexPath:");
    // 当delgate对象中没有实现 tableView:didSelectRowAtIndexPath: 方法时, 直接返回
    if (![delegate respondsToSelector:originalSelector]) {
        return;
    }
    
    // 获取 delegate instance 的原始子类
    Class originalClass = object_getClass(delegate);
    NSString *originalClassName = NSStringFromClass(originalClass);
    // 当delegate对象已经是一个动态子类代理, 无需重复创建, 直接返回
    if ([originalClassName hasPrefix:kWBAutoTrackDelegatePrefix]) {
        return;
    }
    
    NSString *subclassName = [kWBAutoTrackDelegatePrefix stringByAppendingString:originalClassName];
    Class subclass = NSClassFromString(subclassName);
    if (!subclass) {
        //1. 注册全新子类 -- 动态子类
        subclass = objc_allocateClassPair(originalClass, subclassName.UTF8String, 0);
        
        //2. 给子类添加 tableView:didSelectRowAtIndexPath: 方法 method
        Method method = class_getInstanceMethod(self, originalSelector);
        IMP methodIMP = method_getImplementation(method);
        const char * types = method_getTypeEncoding(method);
        
        if (!class_addMethod(subclass, originalSelector, methodIMP, types)) {
            NSLog(@"cannot copy method to destination selector %@ as it already exists.", NSStringFromSelector(originalSelector));
        }
        
        //3. 给子类对象添加 -(void)class 方法, 类似 kvo, 隐藏实现
        Method classMethod = class_getInstanceMethod(self, @selector(wbautotrack_class));
        IMP classIMP = method_getImplementation(classMethod);
        const char *classTypes = method_getTypeEncoding(classMethod);
        if (!class_addMethod(subclass, @selector(class), classIMP, classTypes)) {
            NSLog(@"Cannot copy method to destination selector -(void)class as it already exists");
        }
        
        // 子类和原始类的大小必须相同, 不能有更多的成员变量(ivars)或者属性
        // 如果不同, 将导致设置新的子类时, 重新分配内存, 重写对象的 isa 指针
        if (class_getInstanceSize(originalClass) != class_getInstanceSize(subclass)) {
            NSLog(@"Cannot create subclass of Delegate, because the created subclass is not the same size. %@", NSStringFromClass(originalClass));
            NSAssert(NO, @"Classes must be the same size to swizzle isa");
            return;
        }
        
        objc_registerClassPair(subclass);
    }

    // isa swizzling
    if (object_setClass(delegate, subclass)) {
        NSLog(@"Successfully created Delegate Proxy automatically");
    }
}

// 调用 delegate 的 tableView:didSelectRowAtIndexPath: 时, 实际会调用动态代理子类的这个方法
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(nonnull NSIndexPath *)indexPath{
    // 第一步先获取原始类, 也就是 superClass
    Class cla = object_getClass(self);
    NSString *className = [NSStringFromClass(cla) stringByReplacingOccurrencesOfString:kWBAutoTrackDelegatePrefix withString:@""];
    Class originalClass = objc_getClass([className UTF8String]);
    
    // 第二步 调用tableview.delegate 的方法!!! 也就是 superClass 的方法
    SEL originalSelector = NSSelectorFromString(@"tableView:didSelectRowAtIndexPath:");
    Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
    IMP originalImplementation = method_getImplementation(originalMethod);
    if (originalImplementation) {
	      // tableView:didSelectRowAtIndexPath: 方法指针类型
        ((void(*)(id, SEL, UITableView *, NSIndexPath *))originalImplementation)(tableView.delegate, originalSelector, tableView, indexPath);
    }
    
    // 第三步, 调用埋点
    // 触发 Click 埋点
}

// -class 的实现!!! 隐藏动态代理子类
-(Class)wbautotrack_class {
    // 类似 KVO 返回原始
    Class class = object_getClass(self);
    NSString *className = [NSStringFromClass(class) stringByReplacingOccurrencesOfString:kWBAutoTrackDelegatePrefix withString:@""];
    return objc_getClass([className UTF8String]);
}
@end

这种动态子类的方式比较像KVO的实现.

3. NSProxy消息转发方式

我们知道iOS中有两个根类, 一个是NSObject, 另外一个是NSProxy, NSProxy完全可以作为消息转发类型, 也可以帮助模拟多重继承的关系.

一般来说NSObject也能实现消息转发, 但是NSProxy更加适合:

  1. NSObject类实现的代理类型, 不会自动转发NSObject中实现了的方法!!!
  2. NSObject类实现的代理不会自动转发NSObject Category中的方法!!! 例如 KVC的valueForKey:等等!

使用NSProxy作为类实现代理只需要关注如下方法:

  1. - (void)forwardInvocation:(NSInvocation *)invocation;
  2. - (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel , 因为如果使用forwardInvocation

另外, 很多场景下, 如果使用NSObject的子类实现Proxy, 那么该类一定有一个- (id)forwardingTargetForSelector:(SEL)aSelector;进行快速转发!!!

另外, NSProxy的instance实例默认是没有 - (BOOL)respondsToSelector:(SEL)aSelector; 方法的!!!

因此我们可以构造一个 NSProxy对象, 帮助转发这个消息:

static void *const wbautotrack_delegateProxyKey = (void *)&wbautotrack_delegateProxyKey;
@implementation UITableView (WBAutoTrack)

+(void)load{
    [UITableView wb_autotrack_swizzleMethod:@selector(setDelegate:) withMethod:@selector(wbautotrack_setDelegate:) error:nil];
}

-(void)setWbautotrack_delegateProxy:(WBAutoTrackTableViewDelegateProxy *)wbautotrack_delegateProxy{
    objc_setAssociatedObject(self, &wbautotrack_delegateProxyKey, wbautotrack_delegateProxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(WBAutoTrackTableViewDelegateProxy *)wbautotrack_delegateProxy{
    return objc_getAssociatedObject(self, &wbautotrack_delegateProxyKey);
}

// 方案三 - 可能与第三方库冲突, 比如 ReactiveCocoa
-(void)wbautotrack_setDelegate:(id<UITableViewDelegate>)delegate{
    self.wbautotrack_delegateProxy = nil;
    if (delegate) {
        WBAutoTrackTableViewDelegateProxy *proxy = [WBAutoTrackTableViewDelegateProxy proxyWithTableViewDeleagte:delegate];
        self.wbautotrack_delegateProxy = proxy;
        // 调用原来设置Delegate方法
        [self wbautotrack_setDelegate:proxy];
    }else{
        // 调用原来设置Delegate方法
        [self wbautotrack_setDelegate:nil];
    }
}

@end

简单来说实际上 UITableView的delegate实际是我们自己创建的 NSProxy对象!!! 这个对象在会转发一些消息给真正的delegate:


@interface WBAutoTrackTableViewDelegateProxy : NSProxy
+(instancetype)proxyWithTableViewDeleagte:(id<UITableViewDelegate>)delegate;
@end

@interface WBAutoTrackTableViewDelegateProxy()
// 保存需要转发给的 delegate 对象
@property (nonatomic, weak) id delegate;
@end

@implementation WBAutoTrackTableViewDelegateProxy
+(instancetype)proxyWithTableViewDeleagte:(id<UITableViewDelegate>)delegate{
    WBAutoTrackTableViewDelegateProxy *proxy = [WBAutoTrackTableViewDelegateProxy alloc];
    proxy.delegate = delegate;
    return proxy;
}

-(NSMethodSignature *)methodSignatureForSelector:(SEL)selector{
    //返回代理对象的 方法签名信息
    return [(NSObject *)self.delegate methodSignatureForSelector:selector];
}

-(void)forwardInvocation:(NSInvocation *)invocation{
    //先执行对象的代理方法
    [invocation invokeWithTarget:self.delegate];
  
    // 判断代理方法是否是cell的点击事件的代理方法
    if (invocation.selector == @selector(tableView:didSelectRowAtIndexPath:)) {
        // 将方法修改为进行数据采集的方法, 即本类中的实例方法: wbautotrack_tableView:didSelectRowAtIndexPath:
        invocation.selector = NSSelectorFromString(@"wbautotrack_tableView:didSelectRowAtIndexPath:");
        // 执行采集数据相关的方法
        [invocation invokeWithTarget:self];
    }
}

-(void)wbautotrack_tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
		// 埋点!!!
}
@end

这种代理模式会破坏原有的delegate链结构!!!

小结

以上多种方式目前都没有考虑delegate instance可能被注册KVO的场景. 这种情况还需要单独兼容. 另外使用NSProxy代理的模式, 可能会破坏原有的代理链. 并且默认会使用慢消息转发, 可能有一定的性能影响.

参考:

<<iOS 全埋点解决方案>>