SwiftiOS开发笔记iOS Developer

【iOS】category 重写方法的调用

2017-07-20  本文已影响1507人  Yaso

前两天工程中,出现了一个类的两个分类(自己实现了一个,第三方SDK里有一个),同时实现了一个方法名相同的方法,当时就产生了一个疑问,当实际调用时调的是哪个分类的方法呢?

一、category重写主类方法

首先先来看看这一点,相信大部分人已经知道,如果一个类的分类重写了这个类的方法后,那么这个类的这个方法将失效,起作用的将会是分类的那个重写方法,在分类重写的时候Xcode也会给出相应警告:分类实现将会替代主类实现

分类重写主类已有方法警告 ️分类重写主类已有方法警告 ️

那么原方法失效,分类方法生效的原理是?

想弄清这点先来看一下类的初始化,首先oc是动态语言,建立在runtime 的基础上,同样类的初始化也是动态的,根类NSObject 的+load+initilize两个方法,用于类的初始化,我们这里要着重看的是+load方法:

+load 方法是当类或分类被添加到 Objective-C runtime 时被调用的,实现这个方法可以让我们在类加载的时候执行一些类相关的行为。子类的 +load 方法会在它的所有父类的 +load 方法执行之后执行,而分类的 +load 方法会在它的主类的 +load 方法执行之后执行。但是不同的类之间的 +load 方法的调用顺序是不确定的。

原因就在这里,因为加载顺序是父类先+load,然后子类+load,然后分类+load,那么如果分类重写子类方法:首先子类+load,将方法添加到类的方法列表methodLists,然后分类+load,将重写的方法添加到方法列表中,但是这里存在几点疑问:

  1. 方法列表methodLists里是否会有两个SEL相同的方法?
  2. 如果会有,这两个方法在方法列表中的顺序是怎样的?(顺序决定哪个被调用)

下面来求证一下:

#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] 分类一

可见:

  1. 方法列表里会存在两个SEL相同的方法。
  2. 实际调用时,调用的是后添加的方法,即后添加的方法在方法列表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的确如此:

Compile Sources .m文件排序Compile Sources .m文件排序
总结一句话:类的加载顺序,决定方法的添加顺序,调用的时候,后添加的方法会先被找到,所以调用的始终是后加载的类的方法实现。
上一篇下一篇

猜你喜欢

热点阅读