前言
最近在整理博客,发现自己之前写的关于Runtime拦截替换方法的一篇文章《12- Runtime基础使用场景-拦截替换方法(class_addMethod ,class_replaceMethod和method_exchangeImplementations)》,大家还是很关注的,文章大家看完依然疑问,但是由于当时生产力不足和后续的种种原因,并没有补发文章。最近工作不忙,正好结合自己工作中遇到的Runtime使用场景,补上自己这个坑。
之前留给大家的疑惑主要有两点,如下:
-
第一,下边这三个方法的具体作用:
class_addMethod
class_replaceMethod
method_exchangeImplementations
第二,为什么方法交换需要用到
class_addMethod
和class_replaceMethod
这两个方法?
要回答这两个问题,我们有必要了解一下OC中的类对象结构和消息机制,往下看。
类结构
OC中的类对象结构和消息机制包含的内容其实很多,避免篇幅过长,这里我只简单的说一下和本文相关的部分。
首先,在OC中有实例对象、类对象、元类对象,如下:
[Student class]; // 是类对象
Student *stu = [Student new]; // p是实例对象
object_getClass([Student class]) // 元类
类对象
(即我们日常叫称为的类
),它是基于实例对象的一种抽象定义,比如说喵咪小花都属于猫,那么猫就是一种抽象的概念,定义了猫的外形、活动特定等等属性。我们用代码的方法可以这么定义:
猫 *小花 = [猫 new];
所以OC中,类对象也是一个定义了一个实例对象包含了哪些方法、属性、父类是谁等信息的抽象对象。OC的底层是c/c++实现,所以OC中的类结构是采用c++中的结构体来表示的。下边是我写的一个伪结构体,从伪结构体中我们可以知道,类对象中有方法列表
、父类
这个信息。
struct objc_class {
Class super_class // 当前类的父类
struct objc_method_list * * methodLists // 方法列表
//...... 其它信息这里忽略
}
(类对象的真实结构并不是这样,这里我们也可以忽略它的真实结构。即便你日后了解了类的真实结构,也不会影响到下边的结论。)
super_class 就是当前类的父类。
methodLists实际上是一个数组,保存着类有哪些方法。这里可以提到元类了,实例对象是一种结构,而类对象和元类对象是另外一种结构。关于类对象和元类对象的区别,对于本文只要记住一个:对象方法是保存在类对象的methodLists中,而类方法保存在元类的methodLists中
。
methodLists数组中保存着结构体method_t,这个结构体包含了我们平时写的方法的信息。伪结构体如下:
struct method_t {
SEL method_name
IMP method_imp
char * types
}
SEL method_name
sel就是方法的名字
IMP method_imp
imp保存着一个指针,这个指针指向函数的具体实现地址。 所以,方法真正的实现是单独保存在一个地方的,它的实现地址交给imp保存。当我们执行方法的时候,实际上是从method_t结构体中找到imp,然后调用。
这里有一点很重要,Runtime替换方法实际上就是替换imp。所以产生了替换了方法之后,明明你调用的是methodA,但是执行的是methodB的效果。因为methodA对应的method_t结构体中的imp实际保存的是methodB方法的实现地址了。
types
可以简单的认为到能代表这个方法的特定字符,用法我们暂时忽略。 有一点需要注意,如果你修改了imp为新的imp外,同时修改types改成新方法的types,这样才是真正把method_t结构体改成了新的方法。
消息机制
一般在OC中调用方法,底层会转成一个objc_msgSend的c++函数。比如说:[stu instanceMthod]
,底层实际上是
objc_msgSend([Student class], @Seletor(instanceMthod))
表示我们从Person这个类对象结构体中查找instanceMthod这个方法,找到它并且调用。查找调用这个方法的过程,我们可以简单认为就是消息机制。 下边我们简单说一下消息机制的流程,还是用[stu instanceMthod]
来作为例子:
假如这个方法在Student的父类Person中
- 首先,实例对象p在对应的类对象Student的方法列表中methodLists查找instanceMthod方法,没有找到。(如果能找到,那么就直接调用对应method_t结构中的imp执行方法,结束查找)
- 通过类对象Student的superClass找到父类对象Person,在父类的的方法列表中methodLists查找instanceMthod方法,找到了,调用方法,结束查找。(如果在父类对象Person、以及Person的父类对象NSObject没有找到该方法,那么会进入消息转发的另外两个阶段,如果这两个阶段还是没有找到要调用的方法,那么就会报经典错误
unrecognized selector sent to instance
)
当然这个消息机制的过程是非常简陋的,实际上在进入methodLists查找之前,会先进入方法缓存cache中查找,有兴趣你可以自己多了解一下。
三个方法的作用
class_addMethod
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
作用:就是动态在类对象cls中添加一个方法。参数SEL
、IMP
、types
我们上边都提到过。
注意:如果cls中已经有了要添加的方法声明和方法实现,那么添加失败,返回NO。如果没有声明和实现方法,或者只声明没有方法实现,都可以添加成功,返回YES。
class_replaceMethod
IMP class_replaceMethod(Class cls, SEL name, IMP newImp, const char *newTypes)
作用:把类对象中的cls的方法的imp替换成newImp,同时还需要替换newTypes。
method_exchangeImplementations
void method_exchangeImplementations(Method m1, Method m2)
作用:Method这里可以认为就是上边说到的method_t结构体,交换m1和m2,实际上就是交换两个method_t结构体中的IMP和types。
注意:如果imp为nil,交换操作将失败。
为什么会用到dispatch_once
dispatch_once我们日常最长用的就是单例。保证在程序运行过程中,其代码块内的代码只执行一次。Runtime交换方法之所以会用到dispatch_once,是为了防止load被手动调用。 load方法的调用时机是在main函数被调用之前,且只被系统调用一次。正常情况下,我们无需再手动调用load方法,但是为了防止意外,所以加了dispatch_once,保证替换方
法的Runtime代码只能执行一次,从而避免方法有替换回去。
method_exchangeImplementations
一般我们交换方法实现的场景比较明确,比如替换苹果API中的类的某个方法或者第三方框架中的类的某个方法。
例子如下:
@interface LLPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation LLPerson
- (void)personInstanceMethod{
NSLog(@"person对象方法, 调用者:%@ 方法:%s", self,__FUNCTION__);
}
@end
@interface LLPerson (huan)
@end
@implementation LLPerson (huan)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"------// 替换开始 //------");
NSLog(@"%@", self);
Method oriMethod = class_getInstanceMethod(self, @selector(personInstanceMethod));
Method swiMethod = class_getInstanceMethod(self, @selector(new_personInstanceMethod));
method_exchangeImplementations ( oriMethod, swiMethod) ;
NSLog(@"------// 替换完毕 //------");
});
}
- (void)new_personInstanceMethod {
NSLog(@"Person中的新方法, 调用者:%@, 方法:%s",self,__FUNCTION__);
[self new_personInstanceMethod];
}
@end
调用代码如下:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"------------");
LLStudent *stu = [LLStudent new];
[stu personInstanceMethod];
}
@end
结果如下:
2020-01-07 22:19:27.096634+0800 RunTime1[1972:190371] ------// 替换开始 //------
2020-01-07 22:19:27.097367+0800 RunTime1[1972:190371] LLPerson
2020-01-07 22:19:27.097739+0800 RunTime1[1972:190371] ------// 替换完毕 //------
2020-01-07 22:19:27.203340+0800 RunTime1[1972:190371] ------------
2020-01-07 22:19:27.203516+0800 RunTime1[1972:190371] Person中的新方法, 调用者:<LLStudent: 0x600003520330>, 方法:-[LLPerson(huan) new_personInstanceMethod]
2020-01-07 22:19:27.203679+0800 RunTime1[1972:190371] person对象方法, 调用者:<LLStudent: 0x600003520330> 方法:-[LLPerson personInstanceMethod]
这种情况非常简单,我们明确的知道了LLPerson中的方法声明和方法实现,只需要在分类中直接交换就可以了。不需要其它的额外代码。 下边介绍一种特殊的情况,请往下看。
为什么会用到class_addMethod、class_replaceMethod
下边要讲的这种情况是在我开发过程中遇到的,如果只是用method_exchangeImplementations进行方法交换之后,运行会出现crash。
场景:Person声明了某个方法并且实现了方法,然后Student继承Person,没有重写父类的这个方法,依然不影响直接调用和使用Person中的这个方法。 如果此时我们在Student的分类中交换父类的这个方法,会发生了什么?
代码如下:
@interface LLPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation LLPerson
- (void)personInstanceMethod{
NSLog(@"person对象方法, 调用者:%@ 方法:%s", self,__FUNCTION__);
}
@end
@interface LLStudent : LLPerson
@end
@implementation LLStudent
@end
@interface LLStudent (huan)
@end
@implementation LLStudent (huan)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"------// 替换开始 //------");
NSLog(@"%@", self);
Method oriMethod = class_getInstanceMethod(self, @selector(personInstanceMethod));
Method swiMethod = class_getInstanceMethod(self, @selector(studentInstanceMethod));
method_exchangeImplementations ( oriMethod, swiMethod) ;
NSLog(@"------// 替换完毕 //------");
});
}
- (void)studentInstanceMethod {
NSLog(@"Student中的新方法, 调用者:%@, 方法:%s",self,__FUNCTION__);
[self studentInstanceMethod];
}
@end
调用代码:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"------------");
LLStudent *stu = [LLStudent new];
[stu personInstanceMethod];
NSLog(@"------------");
LLPerson *person = [LLPerson new];
[person personInstanceMethod];
}
@end
运行结果:
2020-01-07 22:41:10.744351+0800 RunTime1[2421:244815] ------// 替换开始 //------
2020-01-07 22:41:10.745134+0800 RunTime1[2421:244815] LLStudent
2020-01-07 22:41:10.745427+0800 RunTime1[2421:244815] ------// 替换完毕 //------
2020-01-07 22:41:10.852287+0800 RunTime1[2421:244815] ------------
2020-01-07 22:41:10.852488+0800 RunTime1[2421:244815] Student中的新方法, 调用者:<LLStudent: 0x600003964160>, 方法:-[LLStudent(huan) studentInstanceMethod]
2020-01-07 22:41:10.852652+0800 RunTime1[2421:244815] person对象方法, 调用者:<LLStudent: 0x600003964160> 方法:-[LLPerson personInstanceMethod]
2020-01-07 22:41:10.852797+0800 RunTime1[2421:244815] ------------
2020-01-07 22:41:10.852948+0800 RunTime1[2421:244815] Student中的新方法, 调用者:<LLPerson: 0x600003958050>, 方法:-[LLStudent(huan) studentInstanceMethod]
2020-01-07 22:41:10.853127+0800 RunTime1[2421:244815] -[LLPerson studentInstanceMethod]: unrecognized selector sent to instance 0x600003958050
// 崩溃
具体的崩溃位置是LLStudent+huan分类中的[self studentInstanceMethod];
这一行。看到这里,可能大家会有点蒙,会有下边这几个问题:
- 不是已经交换了父类的方法吗,为什么执行[person personInstanceMethod]会crash呢?
- 为什么
[stu personInstanceMethod]
执行之后没有crash? - 为什么在父类的分类中交换方法就没有问题呢?
- 怎么处理这个问题呢?
分析如下:
问题一:为什么执行[person personInstanceMethod]会crash呢?
注意,上边的代码是在子类Student的分类中把父类的方法和子类的方法进行了交换。交换之后如下:
当执行[person personInstanceMethod]时,实际上是执行子类中的studentInstanceMethod方法,首先调用NSLog,输出结果。然后调用studentInstanceMethod方法中的 [self studentInstanceMethod]。这里要特别注意,NSLog打印出的当前self是<LLPerson: 0x600003958050>,所以 [self studentInstanceMethod]实际上就是[person studentInstanceMethod],底层实现代码为
objc_msgSend([LLPerson class], @Seletor(studentInstanceMethod))
用我们上边提到的消息机制来还原查找方法studentInstanceMethod的过程,类对象LLPerson中的方法列表methodLists中只有一个方法personInstanceMethod,且这个方法的IMP指向了studentInstanceMethod的实现地址。但是methodLists中根本没有studentInstanceMethod这个方法,所以经过消息机制的三个阶段也找不到该方法,最终报错-[LLPerson studentInstanceMethod]: unrecognized selector sent to instance 0x600003958050
。
问题二:为什么[stu personInstanceMethod]
执行之后没有crash?
[stu personInstanceMethod]底层实现代码为
底层代码即
objc_msgSend([Student class], @Seletor(personInstanceMethod))
用消息机制来还原查找方法personInstanceMethod的过程,首先在
- 首先,在实例对象stu对应的类对象Student的方法列表methodLists中查找personInstanceMethod方法,没有找到。
- 然后通过类对象Student的superClass找到父类对象Person,在父类的的方法列表methodLists中查找personInstanceMethod方法,找到了,调用方法的IMP,此时是studentInstanceMethod。首先执行NSLog,打印结果。注意打印结果中的self是<LLStudent: 0x600003964160>,所以接下来调用
[self studentInstanceMethod]
实际上就是[stu studentInstanceMethod]
,所以底层实现代码是
objc_msgSend([Student class], @Seletor(studentInstanceMethod))
按照消息机制的查找过程,我们在类对象Student的方法列表methodLists中找到studentInstanceMethod,然后调用该方法的IMP,此时是personInstanceMethod。
所以我们可以看到,[stu personInstanceMethod]
这行代码的运行结果是:
2020-01-07 22:41:10.852488+0800 RunTime1[2421:244815] Student中的新方法, 调用者:<LLStudent: 0x600003964160>, 方法:-[LLStudent(huan) studentInstanceMethod]
2020-01-07 22:41:10.852652+0800 RunTime1[2421:244815] person对象方法, 调用者:<LLStudent: 0x600003964160> 方法:-[LLPerson personInstanceMethod]
问题三:为什么在父类的分类中交换方法就没有问题呢?
简而言之,就是类对象Person在进行方法交换之前,它的方法列表methodLists中已经包含了交换前的方法和交换后的方法,不会存在交换之后,方法找不到的问题。
问题四:怎么处理这个问题呢?
首先再次明确我们的目的是为了在Student中将使用的父类方法进行方法交换。成功的标志和直接在父类中的分类中进行方法交换的结果一样,如果stu执行studentInstanceMethod和personInstanceMethod能够调用到对方的实现,就达到了目的。 一定要明确这一点。
通过上边的分析,我们知道直接使用method_exchangeImplementations
的方法实现不了我们想要的目的。解决的方式,如问题三中提到的那样,先让stu拥有交换前和交换后的方法,然后再进行交换。
好了,先看下代码和运行结果,我们再做具体的分析。
@implementation LLStudent (huan)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"------// 替换开始 //------");
NSLog(@"%@", self);
Method oriMethod = class_getInstanceMethod(self, @selector(personInstanceMethod));
Method swiMethod = class_getInstanceMethod(self, @selector(studentInstanceMethod));
BOOL didAddMethod = class_addMethod(self,
@selector(personInstanceMethod),
method_getImplementation(swiMethod),
method_getTypeEncoding(swiMethod)
);
if (didAddMethod) {
class_replaceMethod(self,
@selector(studentInstanceMethod),
method_getImplementation(oriMethod),
method_getTypeEncoding(oriMethod));
}else{
method_exchangeImplementations (oriMethod, swiMethod);
}
NSLog(@"------// 替换完毕 //------");
});
}
- (void)studentInstanceMethod {
NSLog(@"Student中的新方法, 调用者:%@, 方法:%s",self,__FUNCTION__);
[self studentInstanceMethod];
}
@end
2020-01-08 00:14:35.333082+0800 RunTime1[3674:509474] ------// 替换开始 //------
2020-01-08 00:14:35.333878+0800 RunTime1[3674:509474] LLStudent
2020-01-08 00:14:35.334058+0800 RunTime1[3674:509474] ------// 替换完毕 //------
2020-01-08 00:14:35.441442+0800 RunTime1[3674:509474] ------------
2020-01-08 00:14:35.441627+0800 RunTime1[3674:509474] Student中的新方法, 调用者:<LLStudent: 0x6000036185e0>, 方法:-[LLStudent(huan) studentInstanceMethod]
2020-01-08 00:14:35.441779+0800 RunTime1[3674:509474] person对象方法, 调用者:<LLStudent: 0x6000036185e0> 方法:-[LLPerson personInstanceMethod]
2020-01-08 00:14:35.441893+0800 RunTime1[3674:509474] ------------
2020-01-08 00:14:35.442032+0800 RunTime1[3674:509474] person对象方法, 调用者:<LLPerson: 0x600003614160> 方法:-[LLPerson personInstanceMethod]
通过打印结果,可以看到已经实现了我们的目的,并且父类依然可以调用到,没有崩溃。我们具体分析下:
首先,执行class_addMethod
方法
BOOL didAddMethod = class_addMethod(self,
@selector(personInstanceMethod),
method_getImplementation(swiMethod),
method_getTypeEncoding(swiMethod)
);
结果didAddMethod = YES, 我们动态给类对象Student新添加了personInstanceMethod方法,并且这个方法的IMP是studentInstanceMethod。 此时类对象Student方法列表中就包含了交换前和交换后的方法,而类对象Person的方法列表我们并没有进行操作,所以不变,看图二。
然后进入if判断中,执行下边代码:
class_replaceMethod(self,
@selector(studentInstanceMethod),
method_getImplementation(oriMethod),
method_getTypeEncoding(oriMethod));
类对象Student中的方法studentInstanceMethod的imp和types替换为oriMethod(即personInstanceMethod)的。此时,类对象Student中的两个方法及它们的imp实际如下:
是不是很熟悉?没错,和图一中交换后的类对象Person的方法列表中一样。这个时候执行
[stu personInstanceMethod]
就不会crash且实现方法交换的效果了。小结一下:如果想要实现方法交换,那么交换前后的方法必须都在当前类对象中有实现才可以。
所以,AFNetworking和其它一些第三方框架要用到class_addMethod、class_replaceMethod两个方法,是为了兼顾上边这种特殊的情况,造成crash。
结尾
终于把之前的坑补上了。其实在Runtime交换方法的使用过程中还有其它的情况存在,比如说组内多个人都对同一个方法进行了交换操作等等,所以我们最好是把这种操作交给一个人或者一个组来统一维护,避免这种情况。
交流
希望能和大家交流技术
我的博客地址: http://www.lilongcnc.cc/