由于最近开发新版本,就避免不了在开发和调试过程中引起崩溃,以及诱发一些之前的bug导致的崩溃。而且项目比较大也很不好排查,正好想起之前研究过的`Method Swizzling`,考虑是否能用这个苹果的“黑魔法”解决问题,当然用好这个黑魔法并不局限于解决这些问题....
小编自己有一个学习交流群,里面都是有多年开发经验的iOS大牛在里面交流,有需要的伙伴可以进来一起交流群号(681503716)(验证编码:大鲨),不定时也会分享ARKit技术,移动架构,支付宝,底层,高级进阶学习不等的视频教程资料
开发需求
如果产品经理突然说:”在所有页面添加统计功能,也就是用户进入这个页面就统计一次”。我们会想到下面的一些方法:
- 手动添加
直接简单粗暴的在每个控制器中加入统计,复制、粘贴、复制、粘贴…
上面这种方法太Low了,消耗时间而且以后非常难以维护,会让后面的开发人员骂死的。
- 继承
我们可以使用继承的方式来解决这个问题。创建一个基类,在这个基类中添加统计方法,其他类都继承自这个基类。
然而,这种方式修改还是很大,而且定制性很差。以后有新人加入之后,都要嘱咐其继承自这个基类,所以这种方式并不可取。
- Category
我们可以为
UIViewController
建一个
Category
,然后在所有控制器中引入这个
Category
。当然我们也可以添加一个
PCH
文件,然后将这个
Category
添加到
PCH
文件中。
- Method Swizzling
我们可以使用苹果的“黑色魔法”
Method Swizzling
,
Method Swizzling
本质上就是对
IMP
和
SEL
进行交换。
Method Swizzling
原理
Method Swizzing
是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将
Method Swizzling
代码写到任何地方,但是只有在这段
Method Swilzzling
代码执行完毕之后互换才起作用。
使用注意
类簇设计模式
在iOS中
NSNumber
、
NSArray
NSDictionary
等这些类都是类簇(
Class Clusters
),一个
NSArray
的实现可能由多个类组成。
所以如果想对NSArray进行Swizzling,必须获取到其“真身”进行Swizzling,直接对NSArray进行操作是无效的。
下面列举了
NSArray
NSDictionary
本类的类名,可以通过
Runtime
函数取出本类。
注意要点
-
应该总在Swizzling
中执行+load
-
应该总是在Swizzling
dispatch_once
-
在Swizzling
中执行时,不要调用+load
。如果多次调用了[super load]
,可能会出现“[super load]
无效”的假象,原理见下图:Swizzle
封装
在项目中我们肯定会在很多地方用到
Method Swizzling
,而且在使用这个特性时有很多需要注意的地方。我们可以将
Method Swizzling
封装起来,也可以使用一些比较成熟的第三方。
里面核心就两个类,代码看起来非常清爽。
#import <Foundation/Foundation.h>
@interface NSObject (JRSwizzle)
+ (BOOL)jr_swizzleMethod:(SEL)origSel_ withMethod:(SEL)altSel_ error:(NSError**)error_;
+ (BOOL)jr_swizzleClassMethod:(SEL)origSel_ withClassMethod:(SEL)altSel_ error:(NSError**)error_;
@end
// MethodSwizzle类
#import <objc/objc.h>
BOOL ClassMethodSwizzle(Class klass, SEL origSel, SEL altSel);
BOOL MethodSwizzle(Class klass, SEL origSel, SEL altSel);
错误剖析
在上面的例子中,如果只是单独对
NSArray
或
NSMutableArray
中的单个类进行
Method Swizzling
,是可以正常使用并且不会发生异常的。如果进行
Method Swizzling
的类中,有两个类有继承关系的,并且
Swizzling
了同一个方法。例如同时对
NSArray
NSMutableArray
中的o
bjectAtIndex
:方法都进行了
Swizzling
,这样可能会导致父类
Swizzling
失效的问题。
对于这种问题主要是两个原因导致的,首先是不要在
+ (void)load
方法中调用
[super load]
方法,这会导致父类的Swizzling被重复执行两次,这样父类的Swizzling就会失效。例如下面的两张图片,你会发现由于
NSMutableArray
调用了
[super load]
导致父类
NSArray
的
Swizzling
代码被执行了两次。
#import "NSMutableArray+LXZArrayM.h"
@implementation NSMutableArray (LXZArrayM)
+ (void)load {
// 这里不应该调用super,会导致父类被重复Swizzling
[super load];
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(lxz_objectAtIndexM:));
method_exchangeImplementations(fromMethod, toMethod);
}
这里由于在子类中调用了
super
,导致
NSMutableArray
执行时,父类
NSArray
也被执行了一次。
父类
NSArray
执行了第二次
Swizzling
,这时候就会出现问题,后面会讲具体原因。
这样就会导致程序运行过程中,子类调用
Swizzling
的方法是没有问题的,父类调用同一个方法就会发现
Swizzling
失效了…..具体原因我们后面讲!
还有一个原因就是因为代码逻辑导致
Swizzling
代码被执行了多次,这也会导致
Swizzling
失效,其实原理和上面的问题是一样的,我们下面讲讲为什么会出现这个问题。
问题原因
我们上面提到过
Method Swizzling
的实现原理就是对类的
Dispatch Table
进行操作,每进行一次
Swizzling
就交换一次
SEL
IMP
(可以理解为函数指针),如果
Swizzling
被执行了多次,就相当于
SEL
IMP
被交换了多次。这就会导致第一次执行成功交换了、第二次执行又换回去了、第三次执行…..这样换来换去的结果,能不能成功就看运气了,这也是好多人说
Method Swizzling
不好用的原因之一。
从这张图中我们也可以看出问题产生的原因了,就是
Swizzling
的代码被重复执行,为了避免这样的原因出现,我们可以通过GCD的
dispatch_once
函数来解决,利用
dispatch_once
函数内代码只会执行一次的特性。
在每个
Method Swizzling
的地方,加上
dispatch_once
函数保证代码只被执行一次。当然在实际使用中也可以对下面代码进行封装,这里只是给一个示例代码。
#import "NSMutableArray+LXZArrayM.h"
@implementation NSMutableArray (LXZArrayM)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(lxz_objectAtIndexM:));
method_exchangeImplementations(fromMethod, toMethod);
});
}
这里还要告诉大家一个调试小技巧,已经知道的可以略过。我们之前说过
IMP
本质上就是函数指针,所以我们可以通过打印函数指针的方式,查看
SEL
IMP
的交换流程。
先来一段测试代码:
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lxz_objectAtIndex:));
NSLog(@"%p", method_getImplementation(fromMethod));
NSLog(@"%p", method_getImplementation(toMethod));
method_exchangeImplementations(fromMethod, toMethod);
NSLog(@"%p", method_getImplementation(fromMethod));
NSLog(@"%p", method_getImplementation(toMethod));
method_exchangeImplementations(fromMethod, toMethod);
NSLog(@"%p", method_getImplementation(fromMethod));
NSLog(@"%p", method_getImplementation(toMethod));
method_exchangeImplementations(fromMethod, toMethod);
NSLog(@"%p", method_getImplementation(fromMethod));
NSLog(@"%p", method_getImplementation(toMethod));
看到这个打印结果,大家应该明白什么问题了吧:
2016-04-13 14:16:33.477 [16314:4979302] 0x1851b7020
2016-04-13 14:16:33.479 [16314:4979302] 0x1000fb3c8
2016-04-13 14:16:33.479 [16314:4979302] 0x1000fb3c8
2016-04-13 14:16:33.480 [16314:4979302] 0x1851b7020
2016-04-13 14:16:33.480 [16314:4979302] 0x1851b7020
2016-04-13 14:16:33.480 [16314:4979302] 0x1000fb3c8
2016-04-13 14:16:33.481 [16314:4979302] 0x1000fb3c8
2016-04-13 14:16:33.481 [16314:4979302] 0x1851b7020
Method Swizzling危险吗?
既然
Method Swizzling
可以对这个类的
Dispatch Table
进行操作,操作后的结果对所有当前类及子类都会产生影响,所以有人认为
Method Swizzling
是一种危险的技术,用不好很容易导致一些不可预见的bug,这些bug一般都是非常难发现和调试的。
这个问题可以引用念茜大神的一句话:
使用 Method Swizzling 编程就好比切菜时使用锋利的刀,一些人因为担心切到自己所以害怕锋利的刀具,可是事实上,使用钝刀往往更容易出事,而利刀更为安全。