美文网首页SwiftiOS开发笔记iOS Developer
【iOS】category 重写方法的调用

【iOS】category 重写方法的调用

作者: Yaso | 来源:发表于2017-07-20 08:19 被阅读1507次

    前两天工程中,出现了一个类的两个分类(自己实现了一个,第三方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文件排序
    总结一句话:类的加载顺序,决定方法的添加顺序,调用的时候,后添加的方法会先被找到,所以调用的始终是后加载的类的方法实现。

    相关文章

      网友评论

      • Corbin___:如果分类重写了主类方法,然后不import分类,那么这个分类重新系统方法还会起作用吗
        Corbin___:@Yaso 那这有点可怕,影响范围太大了
        Yaso:@陈泽槟_Corbin 会

      本文标题:【iOS】category 重写方法的调用

      本文链接:https://www.haomeiwen.com/subject/zjmrkxtx.html