【iOS】category 重写方法的调用
前两天工程中,出现了一个类的两个分类(自己实现了一个,第三方SDK里有一个),同时实现了一个方法名相同的方法,当时就产生了一个疑问,当实际调用时调的是哪个分类的方法呢?
一、category重写主类方法
首先先来看看这一点,相信大部分人已经知道,如果一个类的分类重写了这个类的方法后,那么这个类的这个方法将失效,起作用的将会是分类的那个重写方法,在分类重写的时候Xcode也会给出相应警告:分类实现将会替代主类实现
那么原方法失效,分类方法生效的原理是?
想弄清这点先来看一下类的初始化,首先oc是动态语言,建立在runtime 的基础上,同样类的初始化也是动态的,根类NSObject 的+load
和+initilize
两个方法,用于类的初始化,我们这里要着重看的是+load
方法:
+load
方法是当类或分类被添加到 Objective-C runtime 时被调用的,实现这个方法可以让我们在类加载的时候执行一些类相关的行为。子类的 +load
方法会在它的所有父类的 +load
方法执行之后执行,而分类的 +load
方法会在它的主类的 +load
方法执行之后执行。但是不同的类之间的 +load
方法的调用顺序是不确定的。
原因就在这里,因为加载顺序是父类先+load,然后子类+load,然后分类+load,那么如果分类重写子类方法:首先子类+load,将方法添加到类的方法列表methodLists,然后分类+load,将重写的方法添加到方法列表中,但是这里存在几点疑问:
- 方法列表methodLists里是否会有两个SEL相同的方法?
- 如果会有,这两个方法在方法列表中的顺序是怎样的?(顺序决定哪个被调用)
下面来求证一下:
#import "TestCategory.h"
/*主类实现*/
@implementation TestCategory
- (void)newMethod {
NSLog(@"主类");
}
@end
#import "TestCategory+add.h"
/*分类一实现*/
@implementation TestCategory (add)
- (void)newMethod {
NSLog(@"分类一");
}
@end
#import <objc/runtime.h>
#import "TestCategory.h"
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id LenderClass = objc_getClass("TestCategory");
unsigned int outCount, i;
//获取实例方法列表
Method *methodList = class_copyMethodList(LenderClass, &outCount);
for (i=0; i<outCount; i++) {
Method method = methodList[i];
NSLog(@"instanceMethod:%@", NSStringFromSelector(method_getName(method)));
}
//调用一下
TestCategory *tCategory = [[TestCategory alloc]init];
[tCategory newMethod];
}
看输出:
2017-07-19 21:38:13.593 TestRuntimeProperty[27827:1306334] instanceMethod:newMethod
2017-07-19 21:38:13.593 TestRuntimeProperty[27827:1306334] instanceMethod:newMethod
2017-07-19 21:38:13.594 TestRuntimeProperty[27827:1306334] 分类一
可见:
- 方法列表里会存在两个SEL相同的方法。
- 实际调用时,调用的是后添加的方法,即后添加的方法在方法列表methodLists的这个数组的顶部
答案已经很明确了:后+load的类的方法,后添加到方法列表,而这时的添加方式又是插入顶部添加,即[methodLists insertObject:category_method atIndex:0];
所以objc_msgSend遍历方法列表查找SEL 对应的IMP时,会先找到分类重写的那个,调用执行。然后添加到缓存列表中,这样主类方法实现永远也不会调到。
(注:methodLists和method的结构定义可以看下我上篇文章-【iOS】Runtime解读)
二、多个category实现同一个方法
但是如果多个分类同时重写同一个方法,执行顺序又是怎样的呢?
答案是:对于多个分类同时重写同一个方法,Xcode在运行时是根据buildPhases->Compile Sources里面的顺序从上至下编译的,那么很明显就像子类和分类一样,后编译的后load,即后添加到方法列表,所以后编译的分类,方法会放到方法列表顶部,调用的时候先执行。
添加代码验证一下:
#import "TestCategory+addAgain.h"
/*分类二实现*/
@implementation TestCategory (addAgain)
- (void)newMethod {
NSLog(@"分类二");
}
@end
看输出:
2017-07-19 22:18:13.593 TestRuntimeProperty[28385:1331972] instanceMethod:newMethod
2017-07-19 22:18:13.593 TestRuntimeProperty[28385:1331972] instanceMethod:newMethod
2017-07-19 21:18:13.593 TestRuntimeProperty[28385:1331972] instanceMethod:newMethod
2017-07-19 22:18:13.594 TestRuntimeProperty[28385:1331972] 分类一
结果输出仍然是分类一
,那就说明"TestCategory+add.h"
在buildPhases->Compile Sources里面的顺序是靠下的,看下buildPhases的确如此:
总结一句话:类的加载顺序,决定方法的添加顺序,调用的时候,后添加的方法会先被找到,所以调用的始终是后加载的类的方法实现。