目录
一、Method-Swizzling是什么
Method swizzling
指的是改变一个已存在的选择器对应的实现的过程,它依赖于Objectvie-C
中方法的调用能够在运行时进行改变——通过改变类的调度表(dispatch table)
中的选择器到最终函数间的映射关系。
交换前后的SEL
和IMP
的对应关系如下:
二、Method-Swizzling实现
#import "ViewController+NACategory.h"
#import <objc/runtime.h>
@implementation ViewController (NACategory)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(category_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)category_viewWillAppear:(BOOL)animated {
[self category_viewWillAppear:animated];
// TODO: - To do something...
}
@end
1、Swizzling应该在+load
方法中实现?
+load
和 +initialize
是 Objective-C Runtime
会自动调用的两个类方法。但是它们被调用的时机却是有差别的,+load
方法是在类被加载的时候调用的,而+initialize
方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说 +initialize
方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的 +initialize
方法是永远不会被调用的。此外 +load
方法还有一个非常重要的特性,那就是子类、父类和分类中的 +load
方法的实现是被区别对待的。换句话说在 Objective-C Runtime
自动调用 +load
方法时,分类中的 +load
方法并不会对主类中的 +load
方法造成覆盖。综上所述,+load
方法是实现 Method Swizzling
逻辑的最佳“场所”。
2、Swizzling应该在dispatch_once
中实现
还是因为Swizzling
会改变全局,我们需要在运行时采取所有可用的防范措施。保障原子性就是一个措施,它确保代码即使在多线程环境下也只会被执行一次。GCD
中的diapatch_once
就提供这些保障,它应该被当做Swizzling
的标准实践。
3、为什么需要调用 class_addMethod 方法
通过class_addMethod
尝试添加你要交换的方法
如果添加成功
,即本类中没有这个方法,则通过class_replaceMethod
进行替换,其内部会调用class_addMethod
进行添加
如果添加不成功
,即类中有这个方法,则通过method_exchangeImplementations
进行交换
4、Swizzling在+load
方法中实现存在的问题
在iOS-底层探索14:分类的加载(类的加载下)文章中我们知道当主类为懒加载类
、分类为非懒加载分类
(+load
)时分类会迫使主类
变为非懒加载类样式
来提前加载数据。因此大量在+load
中实现 Method Swizzling
逻辑也会让主类和分类提前加载影响启动速度。
三、Method-Swizzling常见问题
1、父类实现了方法A,子类没有实现方法A。父类调用方法A会Crash,子类调用方法A正常。
//*********NAPerson类*********
@interface NAPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation NAPerson
- (void)personInstanceMethod{
NSLog(@"person对象方法:%s",__func__);
}
@end
//*********NAStudent类*********
@interface NAStudent : NAPerson
@end
@implementation NAStudent
@end
//*********调用*********
- (void)viewDidLoad {
[super viewDidLoad];
// 黑魔法坑点一: 子类没有实现 - 父类实现
NAStudent *s = [[NAStudent alloc] init];
[s personInstanceMethod];
NAPerson *p = [[NAPerson alloc] init];
[p personInstanceMethod];
}
方法交换代码如下,是通过NAStudent
的分类NACate
实现的
@implementation NAStudent (NACate)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL oriSEL = @selector(personInstanceMethod);
SEL swizzledSEL = @selector(na_studentInstanceMethod);
Method oriMethod = class_getInstanceMethod(self, oriSEL);
Method swiMethod = class_getInstanceMethod(self, swizzledSEL);
method_exchangeImplementations(oriMethod, swiMethod);
});
}
- (void)na_studentInstanceMethod {
[self na_studentInstanceMethod];
NSLog(@"NAStudent分类添加的na对象方法:%s",__func__);
}
@end
运行代码会出现如下Crash
:
原因分析:
[s personInstanceMethod];
中不报错是因为通过Method-Swizzling
,NAStudent
中personInstanceMethod
方法的imp
交换成了lg_studentInstanceMethod
,而NAStudent
中有这个方法(在NACate
分类中),所以不会报错。[p personInstanceMethod];
造成Crash
是因为通过方法交换,相当于在调用na_studentInstanceMethod
方法。但是NAPerson
中没有na_studentInstanceMethod
方法,因此就会Crash
。
修改方法交换代码避免父类imp找不到
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL oriSEL = @selector(personInstanceMethod);
SEL swizzledSEL = @selector(na_studentInstanceMethod);
Method oriMethod = class_getInstanceMethod(self, oriSEL);
Method swiMethod = class_getInstanceMethod(self, swizzledSEL);
BOOL success = class_addMethod(self, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
//success = YES:说明NAStudent中没有oriSEL(不在父类中找),并添加了oriSEL->swiMethod(IMP)
if (success) {
//替换swizzledSEL->oriMethod(IMP)
class_replaceMethod(self, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else {
method_exchangeImplementations(oriMethod, swiMethod);
}
});
}
运行后打印如下:
使用class_addMethod
的原因在上面已作了说明。
-
class_addMethod
添加方法成功
说明NAStudent本类
中没有personInstanceMethod
方法,然后会添加personInstanceMethod
并指向na_studentInstanceMethod实现IMP
。 - 再调用
class_replaceMethod
方法会替换na_studentInstanceMethod
方法的实现为na_studentInstanceMethod(imp)
。 - 因此这里只会对
NAStudent本类
中的方法进行交换,并不会对父类中的方法产生影响。
class_replaceMethod
、class_addMethod
和method_exchangeImplementations
的源码实现如下:
其中class_replaceMethod
和class_addMethod
中都调用了addMethod
方法,区别在于replace
赋值的不同,下面是addMethod
的源码实现:
2、子类没有实现,父类也没有实现,下面的调用有什么问题?
//*********NAPerson类*********
@interface NAPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation NAPerson
@end
//*********NAStudent类*********
@interface NAStudent : NAPerson
@end
@implementation NAStudent
@end
//*********调用*********
- (void)viewDidLoad {
[super viewDidLoad];
// 黑魔法坑点二: 子类父类都没有实现
NAStudent *s = [[NAStudent alloc] init];
[s personInstanceMethod];
//NAPerson *p = [[NAPerson alloc] init];
//[p personInstanceMethod];
}
NAStudent (NACate)
中load
方法不变
运行结果如下:
可见产生了循环调用
通过断点调试可以发现load
中class_addMethod
方法返回YES
,因此oriSEL->swiMethod(IMP)
成功,但是personInstanceMethod
没有实现,class_replaceMethod
方法中swizzledSEL->oriMethod(IMP)
失败。因此执行代码[s personInstanceMethod];
会调用na_studentInstanceMethod
方法实现(IMP
)。但na_studentInstanceMethod
方法中执行[self na_studentInstanceMethod];
并不会调用personInstanceMethod
方法实现(IMP
)。因此自己调自己
,即递归死循环。
优化:避免递归死循环
- 如果
oriMethod
为空,通过class_addMethod
给oriSEL
添加swiMethod
方法的IMP
- 通过
method_setImplementation
将swiMethod
的IMP
指向不做任何事的空实现
虽然这样能解决子类的问题,但父类调用personInstanceMethod
方法还是会出问题。
3、Method-Swizzling - 类方法
//*********NAStudent类*********
@interface NAStudent : NAPerson
+ (void)sayHello;
@end
@implementation NAStudent
@end
//*********NAStudent分类*********
@implementation NAStudent (NACate)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self ClassMethodSwizzling];
});
}
+ (void)ClassMethodSwizzling {
SEL oriSEL = @selector(sayHello);
SEL swizzledSEL = @selector(na_studentClassMethod);
Method oriMethod = class_getClassMethod([self class], oriSEL);
Method swiMethod = class_getClassMethod([self class], swizzledSEL);
if (!oriMethod) { // 避免动作没有意义
// 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
class_addMethod(object_getClass(self), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
CJLog(@"空的IMP")
}));
}
BOOL success = class_addMethod(object_getClass(self), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (success) {
class_replaceMethod(object_getClass(self), swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else {
method_exchangeImplementations(oriMethod, swiMethod);
}
}
+ (void)na_studentClassMethod {
[self na_studentClassMethod];
CJLog(@"NAStudent分类添加的na类方法:%s",__func__);
}
//*********调用*********
- (void)viewDidLoad {
[super viewDidLoad];
[NAStudent sayHello];
}
调用结果:
空的IMP
NAStudent分类添加的na类方法:+[NAStudent(NACate) na_studentClassMethod]
四、Method-Swizzling的应用
1、处理Button重复点击
@interface UIButton (QuickClick)
@property (nonatomic,assign) NSTimeInterval delayTime;
@end
@implementation UIButton (QuickClick)
static const char* delayTime_str = "delayTime_str";
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originMethod = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
Method replacedMethod = class_getInstanceMethod(self, @selector(miSendAction:to:forEvent:));
method_exchangeImplementations(originMethod, replacedMethod);
});
}
- (void)miSendAction:(nonnull SEL)action to:(id)target forEvent:(UIEvent *)event {
if (self.delayTime > 0) {
if (self.userInteractionEnabled) {
[self miSendAction:action to:target forEvent:event];
}
self.userInteractionEnabled = NO;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(self.delayTime * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
self.userInteractionEnabled = YES;
});
} else {
[self miSendAction:action to:target forEvent:event];
}
}
- (NSTimeInterval)delayTime {
return [objc_getAssociatedObject(self, delayTime_str) doubleValue];
}
- (void)setDelayTime:(NSTimeInterval)delayTime {
objc_setAssociatedObject(self, delayTime_str, @(delayTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
2、解决往Array或Dictionary中插入nil导致的crash
在iOS
中NSNumber、NSArray、NSDictionary
等这些类都是类簇
,一个NSArray
的实现可能由多个类组成。所以如果想对NSArray
进行Swizzling
,必须获取到其“真身”
进行Swizzling
,直接对NSArray
进行操作是无效的
。
下面列举了NSArray
和NSDictionary
本类的类名,可以通过Runtime
函数取出本类。
类名 | 本类 |
---|---|
NSArray | __NSArrayI |
NSMutableArray | __NSArrayM |
NSDictionary | __NSDictionaryI |
NSMutableDictionary | __NSDictionaryM |
我们可以用method swizzlin
g修改-[__NSDictionaryM setObject:forKey:]
方法,让它在设值时,先判断是否value
为空,为空则不设置。代码如下:
@implementation NSMutableDictionary (Safe)
+ (void)load {
Class dictCls = NSClassFromString(@"__NSDictionaryM");
Method originalMethod = class_getInstanceMethod(dictCls, @selector(setObject:forKey:));
Method swizzledMethod = class_getInstanceMethod(dictCls, @selector(na_setObject:forKey:));
method_exchangeImplementations(originalMethod, swizzledMethod);
}
- (void)na_setObject:(id)anObject forKey:(id<NSCopying>)aKey {
if (!anObject)
return;
[self na_setObject:anObject forKey:aKey];
}
@end
@implementation NSArray (Safe)
+ (void)load {
Method originalMethod = class_getClassMethod(self, @selector(arrayWithObjects:count:));
Method swizzledMethod = class_getClassMethod(self, @selector(na_arrayWithObjects:count:));
method_exchangeImplementations(originalMethod, swizzledMethod);
}
+ (instancetype)na_arrayWithObjects:(const id [])objects count:(NSUInteger)cnt {
id nObjects[cnt];
int i=0, j=0;
for (; i<cnt && j<cnt; i++) {
if (objects[i]) {
nObjects[j] = objects[i];
j++;
}
}
return [self na_arrayWithObjects:nObjects count:j];
}
@end
@implementation NSMutableArray (Safe)
+ (void)load {
Class arrayCls = NSClassFromString(@"__NSArrayM");
Method originalMethod1 = class_getInstanceMethod(arrayCls, @selector(insertObject:atIndex:));
Method swizzledMethod1 = class_getInstanceMethod(arrayCls, @selector(na_insertObject:atIndex:));
method_exchangeImplementations(originalMethod1, swizzledMethod1);
Method originalMethod2 = class_getInstanceMethod(arrayCls, @selector(setObject:atIndex:));
Method swizzledMethod2 = class_getInstanceMethod(arrayCls, @selector(na_setObject:atIndex:));
method_exchangeImplementations(originalMethod2, swizzledMethod2);
}
- (void)na_insertObject:(id)anObject atIndex:(NSUInteger)index {
if (!anObject)
return;
[self na_insertObject:anObject atIndex:index];
}
- (void)na_setObject:(id)anObject atIndex:(NSUInteger)index {
if (!anObject)
return;
[self na_setObject:anObject atIndex:index];
}
@end
防止数组越界代码:
@implementation NSArray (CJLArray)
//如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
+ (void)load {
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(cjl_objectAtIndex:));
method_exchangeImplementations(fromMethod, toMethod);
}
- (id)cjl_objectAtIndex:(NSUInteger)index {
//判断下标是否越界,如果越界就进入异常拦截
if (self.count-1 < index) {
// 这里做一下异常处理,不然都不知道出错了。
#ifdef DEBUG // 调试阶段
return [self cjl_objectAtIndex:index];
#else // 发布阶段
@try {
return [self cjl_objectAtIndex:index];
} @catch (NSException *exception) {
// 在崩溃后会打印崩溃信息,方便我们调试。
NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__);
NSLog(@"%@", [exception callStackSymbols]);
return nil;
} @finally {
}
#endif
} else { // 如果没有问题,则正常进行方法调用
return [self cjl_objectAtIndex:index];
}
}
@end