通过Method Swizzling实现Hook中的深坑

Method Swizzling技术相信做过一段ios开发的人,多少都会了解一些,被业内称为黑魔法、黑科技,网上也有非常多相关的资料及例子。本篇文章对该技术的原理及使用方式不做赘述,这里主要讨论一下网上使用Method Swizzling技术实现Hook的主流方案。

目前互联网大环境大家都了解,比较热门的问题网上搜到的10篇帖子,可能都是同一篇的转载甚至是copy的,大多数人的拿来主义也是这么被养成的。很多潜在的问题也就逐渐被埋没,这里就来讨论一下网上常见方法的缺陷,话不多说,直接上代码:

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(swizzled_viewWillAppear:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        //@1
        BOOL didAddMethod =
        class_addMethod(class,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));
        //@2
        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        //@3
        } else {
        //@4
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

这段代码是再常见不过的了,几乎占据了搜索结果的80%,是通过创建一个UIViewController类别来描述问题的。接下来我会抛出两个疑问,也是今天讨论的重点:

疑问一:

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
    });
}

+load方法,是在应用启动runtime时,将会执行每个类的加载方法,而应用只会启动一次runtime,换言之+load也只会执行一次,所以这里为什么要再写一次dispatch_once?我不是很了解,有了解的大大求指点。
+load方法中使用dispatch_once是为了防止外界主动调用而造成多次执行
+load相关请看http://www.jianshu.com/p/872447c6dc3f 分析的蛮不错

疑问二:
上述代码大致思路如下
@1,向当前类尝试增加originalSelector方法,将引用指向swizzledMethod,并返回增加结果。
@2,判断是否添加成功,如果成功,说明原类中没有实现此方法,然后使用class_replaceMethod将交换方法的引用指向原有方法的引用,从而达到方法交换的功能。
@3,方法交换完成。(@3位置非常重要,稍后会解释)
@4,如果方法未添加成功,则代表原类中已经实现该方法,直接通过执行method_exchangeImplementations交换两个方法的引用。
那么问题来了
当前的demo测试是没有任何bug的。而我们只需要在中途增加一些打印,就会引出很多问题,例如在@1的位置增加一个

IMP originalIMP = class_getMethodImplementation(class, originalSelector);

打印这个IMP,这时我们会发现,无论你在类中,是否实现了viewWillAppear,这个IMP都是有值的。这也是代码不出问题的重要原因,因为UIViewController已经实现了viewWillAppear。所以理论上,永远不会进入if内部。那么其实一直都在走else,两个方法直接交换而已。
而我们的实际使用场景却远比这样复杂的多,如果换一个系统未实现的方法,会出现什么问题呢?我们照着葫芦画个瓢,同样原理的代码我们换成AppDelegate的类别,交换一下

- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings

在同样的代码@1处,打印一下IMP会出现两种场景:
场景一:原类中已实现该方法,输出

(HookDemo`-[AppDelegate application:didRegisterUserNotificationSettings:] + 1 at AppDelegate.m:54)

场景二:原类中未实现该方法,输出

(libobjc.A.dylib`_objc_msgForward + 1)

通过场景二可以看出如果原类中没有实现该方法,则该方法的IMP将指向消息转发。那么这样会导致的问题:@2处,两个方法的IMP将都指向swizzledSelector的IMP,也就是新建的swizzled_xxx方法。由于原类没有实现,所以didAddMethod为YES,就会走入@3,在@3的位置,我们会惊奇的发现,由于originalSelector的IMP为消息转发,所以导致class_replaceMethod方法执行失败,造成的后果就是original与swizzled两个方法的IMP均指向新建的方法。而此时我们在这个方法中执行

- (void)swizzled_application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings
{
    NSLog(@"%s", __FUNCTION__);
    [self swizzled_application:application didRegisterUserNotificationSettings:notificationSettings];
}

会造成无终结条件的递归。那么这是我们想要的效果吗?


接下来抛出一下我的解决方案,抛砖引玉,期待各位大大更优雅的Hook实现。

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

对同类/类别方法交换进行了简单的抽象。

代码中didAddMethod进行了一次针对原类中增加原有方法并返回结果。

如果增加成功,则代表原类中并无实现originalSelector。那么为了避免消息转发造成的崩溃,我们将改造一下原有方法,将他的IMP设置为我们的健壮性方法msgForwardHandle,并将originalSelector的IMP指向handleIMP。
然后进行原有方法与交换方法的exchange。

如果增加不成功,说明原类中已经实现了原有方法,则直接exchange即可。

msgForwardHandle方法仅仅是用来做健壮性的,避免原有方法未实现,而造成在新方法中调用自身系统找不到相关接收响应的方法,而执行消息转发,毕竟不是每个项目中,都有消息转发crash安全处理的,所以此处增加一个方法避免使程序崩溃。

以上为我的处理方案,欢迎各方大神各显神通,前来指点,指教!

勤于行,惰于思,仅仅只能提高你对操作的熟练度而已。

你可能感兴趣的:(通过Method Swizzling实现Hook中的深坑)