刚开始学习IOS的时候,听说黑魔法很强大,正如它的名字一样,可以做很多不可思议的事情,一直到今天才彻底静下心去了解了Method Swizzling一下,所以写下这篇文章分享给大家。(由于最近希望写文章的时候,顺便可以学习下英文,所以准备在后续的每一篇文章中加入一句英文名言,希望可以给大家一些鼓励。
本文内容参考 [南峰子的技术博客]
(http://southpeak.github.io/2014/11/06/objective-c-runtime-4/)
参考文献
东了个尼github
Method Swizzling“Too many of us are not living our dreams because we are living our fears.”
— Les Brown, Motivational Speaker
Method Swizzling 原理
1.Method Swizzling 原理
在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。
每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点>>类似函数指针,指向具体的Method实现。
Method Swizzling 简介
在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用objective-c的动态特性,可以实现在运行时偷换selector对应的方法实现。
每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。
我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP,
我们可以利用 class_replaceMethod 来修改类,
我们可以利用 method_setImplementation 来直接设置某个方法的IMP,
通过上边的方法,可以把类的调度表(dispatch table)中选择器到最终函数间的映射关系 替换。就相当于把IMP 的只想替换了。
swizzling 需要在 + (void)load{}中使用
swizzling 需要保证只执行一次。 需要使用 dispatch_once;
load 和initialize区别:load是只要类所在文件被引用就会被调用,而initialize是在类或者其子类的第一个方法被调用前调用。所以如果类没有被引用进项目,就不会有load调用;但即使类文件被引用进来,但是没有使用,那么initialize也不会被调用。
Method Swizzling 调用
在Objective-C中,运行时会自动调用每个类的两个方法。+load会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。由于method swizzling会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。+load能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。相比之下,+initialize在其执行时不提供这种保证–事实上,如果在应用中没为给这个类发送消息,则它可能永远不会被调用。
Swizzling应该在dispatch_once中实现。
还是因为swizzling会改变全局,我们需要在运行时采取所有可用的防范措施。保障原子性就是一个措施,它确保代码即使在多线程环境下也只会被执行一次。GCD中的diapatch_once就提供这些保障,它应该被当做swizzling的标准实践。
Method Swizzling 实践(一)
举个例子,假设我们想跟踪在一个iOS应用中每个视图控制器展现给用户的次数:
我们可以给每个视图控制器对应的viewWillAppear:实现方法中增加相应的跟踪代码,但是这样做会产生大量重复的代码。子类化可能是另一个选择,但要求你将UIViewController、 UITableViewController、 UINavigationController 以及所有其他视图控制器类都子类化,这也会导致代码重复。
幸好,还有另一个方法,在分类中进行method swizzling,下面来看怎么做:
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// When swizzling a class method, use the following:
// Class class = object_getClass((id)self);
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
// 通过class_getInstanceMethod()函数从当前对象中的methodlist获取 method结 构体,如果是类方法就使用class_getClassMethod()函数获取
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
/**
* 我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。
* 而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。
* 所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。
*/
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
// 交换的方法。
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}
Method Swizzling 实践(二)
需要统计事件,或者需要输出Log的时候,比如在delloc中,输出log。告诉我们哪个类释放了以及一些类的信息。
//封装方法
+ (void)swizzWithClass:(Class)class originSel:(SEL)originSel newSel:(SEL)newSel{
Method originM = class_getInstanceMethod(class, originSel);
Method newM = class_getInstanceMethod(class, newSel);
IMP newImp = method_getImplementation(newM);
BOOL addMethodSucess = class_addMethod(class, newSel, newImp, method_getTypeEncoding(newM));
if (addMethodSucess) {
class_replaceMethod(class, originSel, newImp, method_getTypeEncoding(newM));
}else{
method_exchangeImplementations(originM, newM);
}
}
代码如下
//在类加载的时候调用 需要保证只执行一次,多次,容易出现不可预知的错误。
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzWithClass:[self class] originSel:NSSelectorFromString(@"dealloc") newSel:@selector(swizz_dealloc)];
});
}
//调用自己写的方法 里面可以自定义想要执行的内容
- (void)swizz_dealloc{
NSLog(@" ** %@ 释放了 %s",NSStringFromClass([self class]),__func__);
[self swizz_dealloc];
}
Method Swizzling 实践(三)
处理数组越界问题空值,导致程序崩溃的问题。
小伙伴们都有遇到过数组越界导致程序崩溃的经历,我们可以给NSArray创建一个类目去解决
代码如下:
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(fd_objectAtIndex:));
method_exchangeImplementations(fromMethod, toMethod);
});
}
//写在 NSArray的类目里边
- (id)fd_objectAtIndex:(NSUInteger)index {
if (self.count-1 < index) {
// 这里做一下异常处理,不然都不知道出错了。
@try {
return [self fd_objectAtIndex:index];
}
@catch (NSException *exception) {
// 在崩溃后会打印崩溃信息,方便我们调试。
NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__);
NSLog(@"%@", [exception callStackSymbols]);
return nil;
}
@finally {}
} else {
return [self fd_objectAtIndex:index];
}
}
实现效果如下:更多的报错信息
效果图在这里我推荐Github上星最多的一个第三方-jrswizzle,很好用!
最后提醒小伙伴们不要滥用Method Swizzling对系统方法进行替换,除非必要的情况下。
网友评论