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安全处理的,所以此处增加一个方法避免使程序崩溃。
以上为我的处理方案,欢迎各方大神各显神通,前来指点,指教!
勤于行,惰于思,仅仅只能提高你对操作的熟练度而已。