iOS Developer有些文章不一定是为了上首页投稿iOS技术交流

Category的本质<二>load,initial

2018-07-25  本文已影响149人  雪山飞狐_91ae

Category的本质<一>
面试题1:Category中有load方法吗?load方法是什么时候调用?
面试题2:load,initialize的区别是什么?它们在Category中的调用顺序以及出现继承时它们之间的调用过程是怎么样的?
那么这篇文章主要就是回答这两个问题。

load方法

load方法什么时候调用?
load方法是在runtime加载类和分类的时候调用。
我们创建了一个Person类和它的两个分类,然后重写了各自的load方法:

//Person
+ (void)load{
    
    NSLog(@"Person + load");
}

//Person+Test1
+ (void)load{
    
    NSLog(@"Person (Test1) + load");
}

//Person+Test2
+ (void)load{
    
    NSLog(@"Person (Test2) + load");
}

然后我们什么也不做,运行代码,看到打印结果:

2018-07-24 20:45:08.369170+0800 interview - Category[14157:409819] Person + load
2018-07-24 20:45:08.371806+0800 interview - Category[14157:409819] Person (Test1) + load
2018-07-24 20:45:08.373190+0800 interview - Category[14157:409819] Person (Test2) + load

通过打印结果我们可以看到Person及其分类的load方法都被调用了,这就证实了load方法是由runtime加载类和分类的时候调用的。
然后我们再给Person类及其子类创建一个+ (void)test方法并实现它:

//Person
+ (void)test{
    
    NSLog(@"Person + test");
}

//Person+Test1
+ (void)test{
    
    NSLog(@"Person (Test1) + test");
}

//Person+Test2
+ (void)test{
    
    NSLog(@"Person (Test2) + test");
}

然后用Person类对象去调用test方法:

[Person test];

得到打印结果:

2018-07-24 21:07:32.886316+0800 interview - Category[14670:428685] Person + load
2018-07-24 21:07:32.887195+0800 interview - Category[14670:428685] Person (Test1) + load
2018-07-24 21:07:32.887461+0800 interview - Category[14670:428685] Person (Test2) + load
2018-07-24 21:07:33.050735+0800 interview - Category[14670:428685] Person (Test2) + test

通过打印结果我们可以看到,Person (Test2)的test方法被调用了,这个很好理解因为我们在Category的本质<一>中说的很清楚了,如果分类和类同时实现了一个方法,那么分类中的方法和类中的方法都会保存下来存入内存中,并且分类的方法在前,类的方法在后,这样在调用的时候就会首先找到分类的方法,给人的感觉就是好像类的方法被覆盖了。
那么问题来了,同样是类方法,同样是分类中实现了类的方法,为什么load方法不像test方法一样,调用分类的实现,而是类和每个分类中的load方法都被调用了呢?load方法到底有什么不同呢?
要想弄清楚其中的原理,我们还是要从runtime的源码入手:

因为load方法的调用并不是objc_msgSend机制,它是直接找到类的load方法的地址,然后调用类的load方法,然后再找到分类的load方法的地址,再去调用它。

而test方法是通过消息机制去调用的。首先找到类对象,由于test方法是类方法,存储在元类对象中,所以通过类对象的isa指针找到元类对象,然后在元类对象中寻找test方法,由于分类也实现了test方法,所以分类的test方法是在类的test方法的前面,首先找到了分类的test方法,然后去调用它。

有继承关系时load方法的调用顺序

通过上面的分析我们确定了load方法的一个调用规则:先调用所有类的load方法,然后再调用所有分类的load方法。

下面我们再创建一个Student类继承自Person类,并且为Student类创建两个子类Student (Test1), Student (Test2),并且覆写load方法:

//Student
+ (void)load{
    
    NSLog(@"Student + load");
}

//Student (Test1)
+ (void)load{
    
    NSLog(@"Student (Test1) + load");
}

//Student (Test2)
+ (void)load{
    
    NSLog(@"Student (Test2) + load");
}

然后我们运行一下程序,看打印结果:

2018-07-25 15:45:58.605156+0800 interview - Category[13869:359239] Person + load
2018-07-25 15:45:58.605684+0800 interview - Category[13869:359239] Student + load
2018-07-25 15:45:58.606420+0800 interview - Category[13869:359239] Student (Test2) + load
2018-07-25 15:45:58.606870+0800 interview - Category[13869:359239] Person (Test1) + load
2018-07-25 15:45:58.607293+0800 interview - Category[13869:359239] Student (Test1) + load
2018-07-25 15:45:58.607514+0800 interview - Category[13869:359239] Person (Test2) + load
2018-07-25 15:45:58.812025+0800 interview - Category[13869:359239] Person (Test2) + test

通过打印结果我们可以很清楚的看见,Person类和Student类的load方法先被调用,然后调用分类的load方法。再运行多次,都是Person类和Student类的load方法先被调用,然后分类的方法才被调用。并且总是Person类的load在Student类的load方法前面被调用,这会不会和编译顺序有关呢?我们改变一下编译顺序看看:

TARGETS -> Build Phases -> Complle Sources中文件的放置顺序就是文件的编译顺序。

381E32DF-9B1D-4752-AFFB-D3925E70579D.png

目前是Person类在Student类的前面编译,现在我们把Student类放到Person类的前面编译:


4A6F780D-FC13-48B4-9C7C-88ADB832D617.png

然后我们再运行一下程序,查看打印结果:

2018-07-25 15:56:07.270034+0800 interview - Category[14070:367686] Person + load
2018-07-25 15:56:07.270619+0800 interview - Category[14070:367686] Student + load
2018-07-25 15:56:07.271107+0800 interview - Category[14070:367686] Student (Test2) + load
2018-07-25 15:56:07.271494+0800 interview - Category[14070:367686] Person (Test1) + load
2018-07-25 15:56:07.271762+0800 interview - Category[14070:367686] Student (Test1) + load
2018-07-25 15:56:07.272118+0800 interview - Category[14070:367686] Person (Test2) + load
2018-07-25 15:56:07.433068+0800 interview - Category[14070:367686] Person (Test2) + test

我们发现还是Person类的load方法在Student类前面被调用,所以好像和编译顺序无关呀。那么我们就需要思考一下是不是由于Student和Person之间的继承关系导致的呢?
为了搞清楚这个问题,我们只能从runtime的源码入手。

schedule_class_load(remapClass(classlist[i]));
这个方法: 571870F3-8F9C-4C24-82A6-3305ECE9B3ED.png

通过这个方法我们就可以很清晰的看到,当要把一个类加入最终的这个classes数组的时候,会先去上溯这个类的父类,先把父类加入这个数组。
由于在classes数组中父类永远在子类的前面,所以在加载类的load方法时一定是先加载父类的load方法,再加载子类的load方法。

类的load方法调用顺序搞清楚了我们再来看一下分类的load方法调用顺序

我们还是看一下void prepare_load_methods(const headerType *mhdr)这个函数

66AC18CB-9401-4FC4-8F5E-3FAD776954ED.png

通过这个分析我们就能知道,分类的load方法加载顺序很简单,就是谁先编译的,谁的load方法就被先加载。

下面我们通过打印结果验证一下,这是编译顺序: C0E77171-09EC-415E-85B0-6FB874EEB8B4.png

按照我们前面的分析,load方法的调用顺序应该是:
Person -> Student -> Person + Test1 -> Student + Test2 -> Student + Test1 -> Person + Test2。
我们看一下打印结果:

2018-07-25 16:48:10.271679+0800 interview - Category[15094:408222] Person + load
2018-07-25 16:48:10.272357+0800 interview - Category[15094:408222] Student + load
2018-07-25 16:48:10.272661+0800 interview - Category[15094:408222] Person (Test1) + load
2018-07-25 16:48:10.272872+0800 interview - Category[15094:408222] Student (Test2) + load
2018-07-25 16:48:10.273103+0800 interview - Category[15094:408222] Student (Test1) + load
2018-07-25 16:48:10.273434+0800 interview - Category[15094:408222] Person (Test2) + load
2018-07-25 16:48:10.441457+0800 interview - Category[15094:408222] Person (Test2) + test

打印结果完美的验证了我们的结论。

总结 load方法调用顺序

1.先调用类的load方法
2.再调用分类的load方法

initialize方法

initialize方法的调用时机
//Person
+ (void)initialize{
    
    NSLog(@"Person + initialize");
}

//Person+Test1
+ (void)initialize{
    
    NSLog(@"Person (Test1) + initialize");
}

//Person+Test2
+ (void)initialize{
    
    NSLog(@"Person (Test2) + initialize");
}

//Student
+ (void)initialize{
    
    NSLog(@"Student + initialize");
}

//Student (Test1)
+ (void)initialize{
    
    NSLog(@"Student (Test1) + initialize");
}

//Student (Test2)
+ (void)initialize{
    
    NSLog(@"Student (Test2) + initialize");
}

我们运行程序,发现什么也没有打印,说明在运行期没有调用+initialize方法。
然后我们给Person类发送消息,也就是调用函数:

[Person alloc];

打印结果:

2018-07-25 17:26:22.462601+0800 interview - Category[15889:437305] Person (Test2) + initialize

可以看到调用了Person类的分类的initialize方法。通过这个打印结果我们能看出initialize方法和load方法的不同,load方法由于是直接获取方法的地址,然后调用方法,所以Person及其分类的load方法都会调用。而initialize方法则更像是通过消息机制,也即是objc_msgend(Person, @selector(initialize))这种来调用的。
然后我多次调用alloc方法:

[Person alloc];
[Person alloc];
[Person alloc];

打印结果:

018-07-25 17:26:22.462601+0800 interview - Category[15889:437305] Person (Test2) + initialize

可见initialize方法只在类第一次收到消息时调用。然后我们再给Student类发送消息:

[Student alloc];

打印结果:

2018-07-25 18:34:14.648279+0800 interview - Category[17187:473502] Person (Test2) + initialize
2018-07-25 18:34:14.648394+0800 interview - Category[17187:473502] Student (Test1) + initialize

我们看到不仅调用了Student类的initialize方法,而且还调用了Student类的父类,Person类的方法,因此我们猜测在调用类的initialize方法之前会先调用父类的initialize方法。
以上仅仅是我们根据打印结果的猜测,还需要通过源码来验证。
[Person alloc]就相当于objc_msgSend([Person class], @selector(alloc)),说明objc_msgSend()内部会去调用initialize方法,判断是第几次接收到消息。

+initialize的调用过程:
+initialize和+load的一个很大区别是,+initialize是通过objc_msgSend进行调用的,所以有以下特点:

下面我们把Student类及其分类中的+initialize这个方法的实现去掉,然后增加一个Teacher类继承自Person类。然后我们给Student类和Teacher类都发送alloc消息:

    [Student alloc];
    [Teacher alloc];

这个时候也就是只有Person类及其分类实现了+initialize方法。那么打印结果会是怎样呢?

2018-07-25 21:47:59.899995+0800 interview - Category[20981:582224] Person (Test2) + initialize
2018-07-25 21:47:59.900112+0800 interview - Category[20981:582224] Person (Test2) + initialize
2018-07-25 21:47:59.900240+0800 interview - Category[20981:582224] Person (Test2) + initialize

这里Person类的+initialize方法竟然被调用了三次,这多少有些出乎意外吧。下面我们来分析一下。

    BOOL studentInitialized = NO;
    BOOL personinitialized = NO;
    BOOL teacherInitialized = NO;
    
    [Student alloc];
    //判断Student类是否初始化了,这里Student类还没有被初始化,所以进入条件语句。
    if(!studentInitialized){
        //判断Student类的父类Person类是否初始化了
        if(!personinitialized){
            //这里Person类还没有初始化,就利用objc_msgSend调用initialize方法
            objc_msgSend([Person class], @selector(initialize));
            //变更Person类是否初始化的状态
            personinitialized = YES;
        }
        //利用objc_msgSend调用Student的initialize方法
        objc_msgSend([Student class], @selector(initialize));
        //变更Student是否初始化的状态
        studentInitialized = YES
    }
    
    [Teacher alloc];
    
    //判断Teacher类是否已经初始化了,这里Teacher类还没有初始化,进入条件语句
    if(!teacherInitialized){
        //判断其父类Person类是否初始化了,这里父类已经初始化了,所以不会进入这个条件语句
        if(!personinitialized){
            
            objc_msgSend([Person class], @selector(initialize));
            personinitialized = YES;
        }
        //利用objc_msgSend调用Teacher类的initialize方法
        objc_msgSend([Teacher class], @selector(initialize));
        //变更状态
        teacherInitialized = YES;
    }

上面列出来的是调用initialize的伪代码,下面再详细说明这个过程:

上一篇 下一篇

猜你喜欢

热点阅读