通过Method Swizzling实现跨类Hook

上篇文章描述了对同类/类别中通过Method Swizzling实现Hook的方案。而在日常实际工作中,场景远比预想要复杂的多。MVC的设计模式意味着view中并不掺杂model的逻辑,并且view与model原则上是没有任何牵连的,如果想通过model的变化去控制view更新,苹果给出的解决方案有委托、通知、设置API等等,由Controller统一去统筹控制。

例如UITableView、UICollectionView等。
这类控件的展示与数据源的绑定关系体现在了委托代理、数据源代理上。而当我们想要Hook这些回调或代理方法时,同类/类别的Method Swizzling就只能望洋兴叹了,因为要拦截的方法的实现位置,往往并不是自身。

这时就体现出我们本章讨论内容的重要性了----跨类Method Swizzling。

话不多说,先上个图:


通过Method Swizzling实现跨类Hook_第1张图片
跨类Hook原理.png

相信图片已经将原理描述的非常清楚了,如果再有看不明白的。。。。那就再看一遍~


接下来是代码实现部分:
抛砖引玉,期待各位大神更优雅的实现。

//跨类交换方法
+ (void)exchangeImpWithOriginalClass:(Class)oriCls swizzledClass:(Class)swiCls originalSel:(SEL)oriSel swizzledSel:(SEL)swiSel tmpSel:(SEL)tmpSel msgForwardSel:(SEL)msgForwardSel
{
    //增加原始方法
    Method originalMethod = class_getInstanceMethod(oriCls, oriSel);
    BOOL didAddOriMethod = class_addMethod(oriCls, oriSel, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    if (didAddOriMethod) {
        originalMethod = class_getInstanceMethod(oriCls, oriSel);
        SEL handleSel = msgForwardSel;
        IMP handleIMP = class_getMethodImplementation(self, handleSel);
        method_setImplementation(originalMethod, handleIMP);
    }
    
    //增加临时中转方法
    class_addMethod(oriCls, tmpSel, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    
    
    Method swizzledMethod = class_getInstanceMethod(swiCls, swiSel);
    class_replaceMethod(oriCls, oriSel, method_getImplementation(swizzledMethod), method_getTypeEncoding(originalMethod));
    
}

代码对照着上图一起看,相信已经非常清晰了,不再赘述。
不明白msgForwardSel的作用的,可以看下我的上篇文章

接下来重点强调一下需要注意的点:
1.当我们在swiIMP中插入Hook后需要操作的代码时,一定要注意,此时self为oriCls实例而并非当前Hook类的实例。所以使用一些方法时一定要注意,可能造成欺骗过了编译器而形成了运行时crash。
2.在swiIMP中执行[self tmpSel]时,是安全的,这是由于我们创建tmpSel,是创建在了oriCls中,所以这里请放心使用。
3.tmpSel方法的本质只是提供一个交换时的临时宿主,所以该方法实现中的代码永远不会执行,可以不写或随意写一个return nil即可。


到了举例的阶段,一切的理论都要配合在实际场景中才显得更好理解:

例如:我们需要在获取UITableView有多少组,并且打印出来。
创建一个UITableView的类别,将下列代码粘贴进去

+ (void)load
{
    //第一步:同类/类别交换setDataSource:方法。主要是用来获取tableView的DataSource实现实例
    [self exchangeImpWithClass:self originalSel:@selector(setDataSource:) swizzledSel:@selector(hook_setDataSource:)];
}

- (void)hook_setDataSource:(id)dataSource
{
    //第二步:通过跨类交换numberOfSectionsInTableView:方法,来监听方法触发动作
    [UITableView exchangeImpWithOriginalClass:[dataSource class] swizzledClass:[self class] originalSel:@selector(numberOfSectionsInTableView:) swizzledSel:@selector(hook_numberOfSectionsInTableView:) tmpSel:@selector(tmp_numberOfSectionsInTableView:) msgForwardSel:@selector(msgForward_numberOfSectionsInTableView:)];
    
    [self hook_setDataSource:dataSource];
}

- (NSInteger)hook_numberOfSectionsInTableView:(UITableView *)tableView
{
    //插入逻辑代码
    NSInteger sec = [self tmp_numberOfSectionsInTableView:tableView];
    NSLog(@"拦截到的组数为:%ld", (long)sec);
    return sec;
}

- (NSInteger)tmp_numberOfSectionsInTableView:(UITableView *)tableView
{
    return 0;
}

- (void)msgForward_numberOfSectionsInTableView:(UITableView *)tableView
{
    //如果原类中,未实现numberOfSectionsInTableView: 则此方法会执行
    NSLog(@"%s", __FUNCTION__);
}


#pragma mark - Swizzled
+ (void)exchangeImpWithClass:(Class)cls originalSel:(SEL)originalSel swizzledSel:(SEL)swizzledSel
{
    Method originalMethod = class_getInstanceMethod(cls, originalSel);
    Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel);
    
    BOOL didAddMethod = class_addMethod(cls,
                                        originalSel,
                                        method_getImplementation(originalMethod),
                                        method_getTypeEncoding(originalMethod));
    
    if (didAddMethod) {
        //如果add成功,说明原始类并没有实现此方法的imp,为避免调用时执行消息转发,此处做统一处理
        originalMethod = class_getInstanceMethod(cls, originalSel);
        SEL handleSel = @selector(msgForwardHandle);
        IMP handleIMP = class_getMethodImplementation(self, handleSel);
        method_setImplementation(originalMethod, handleIMP);
    }
    
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

//跨类交换方法
+ (void)exchangeImpWithOriginalClass:(Class)oriCls swizzledClass:(Class)swiCls originalSel:(SEL)oriSel swizzledSel:(SEL)swiSel tmpSel:(SEL)tmpSel msgForwardSel:(SEL)msgForwardSel
{
    //增加原始方法
    Method originalMethod = class_getInstanceMethod(oriCls, oriSel);
    BOOL didAddOriMethod = class_addMethod(oriCls, oriSel, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    if (didAddOriMethod) {
        originalMethod = class_getInstanceMethod(oriCls, oriSel);
        SEL handleSel = msgForwardSel;
        IMP handleIMP = class_getMethodImplementation(self, handleSel);
        method_setImplementation(originalMethod, handleIMP);
    }
    
    //增加临时中转方法
    class_addMethod(oriCls, tmpSel, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    
    
    Method swizzledMethod = class_getInstanceMethod(swiCls, swiSel);
    class_replaceMethod(oriCls, oriSel, method_getImplementation(swizzledMethod), method_getTypeEncoding(originalMethod));
    
}

- (void)msgForwardHandle
{
    /**
     备用方法,防止原类中没有实现需要交换的方法,导致交换后执行消息转发最终没有处理导致crash。
     后续可以在做底层安全时,做到相应处理类中。
     */
    NSLog(@"%s  ", __FUNCTION__);
}

执行结果:

2017-06-23 16:38:33.579 HookDemo[22159:4362685] 拦截到的组数为:5


通过上述方案,可以实现非常强大的功能,例如通过类别方式实现UITableView/UICollectionView的刷新加载功能及自动显示隐藏空视图功能。
如有时间,我会再后续文章中会写下该模块及开源相关组件代码。

你可能感兴趣的:(通过Method Swizzling实现跨类Hook)