登录 立即注册
金钱:

Code4App-iOS开发-iOS 开源代码库-iOS代码实例搜索-iOS特效示例-iOS代码例子下载-Code4App.com

Category的本质 load,initialize方法 [复制链接]

2018-7-31 10:57
kengsirLi 阅读:230 评论:0 赞:1
Tag:  

面试题1:Category中有load方法吗?load方法是什么时候调用?

面试题2:load,initialize的区别是什么?它们在Category中的调用顺序以及出现继承时它们之间的调用过程是怎么样的?

那么这篇文章主要就是回答这两个问题。

load方法

load方法什么时候调用?

load方法是在runtime加载类和分类的时候调用。

我们创建了一个Person类和它的两个分类,然后重写了各自的load方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Person
+ (void)load{
     
    NSLog(@"Person + load");
}
 
//Person+Test1
+ (void)load{
     
    NSLog(@"Person (Test1) + load");
}
 
//Person+Test2
+ (void)load{
     
    NSLog(@"Person (Test2) + load");
}

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

1
2
3
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方法并实现它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Person
+ (void)test{
     
    NSLog(@"Person + test");
}
 
//Person+Test1
+ (void)test{
     
    NSLog(@"Person (Test1) + test");
}
 
//Person+Test2
+ (void)test{
     
    NSLog(@"Person (Test2) + test");
}

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

1
[Person test];

得到打印结果:

1
2
3
4
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的源码入手:

  • 1.找到objc-os.mm这个文件,然后找到这个文件的void _objc_init(void)这个方法,runtime的初始化都是在这个方法里面完成。

  • 2.这个方法的最后一行调用了函数_dyld_objc_notify_register(&map_images, load_images, unmap_image);,我们点进load_images,这是加载模块的意思。

  • 3.

  • 4

  • 5我们点进call_class_loads();这个方法查看对类的load方法的调用过程:

6.然后我们再点进call_category_loads()查看对分类的load方法的调用过程:

那么这样我们就搞清楚了为什么load方法不是像test方法一样,执行分类的实现

因为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方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Student
+ (void)load{
     
    NSLog(@"Student + load");
}
 
//Student (Test1)
+ (void)load{
     
    NSLog(@"Student (Test1) + load");
}
 
//Student (Test2)
+ (void)load{
     
    NSLog(@"Student (Test2) + load");
}

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

1
2
3
4
5
6
7
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中文件的放置顺序就是文件的编译顺序。

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

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

1
2
3
4
5
6
7
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的源码入手。

  • 1.objc-os.mm中void _objc_init(void)这个入口方法,点进load_images.

  • 2.在void load_images(const char *path __unused, const struct mach_header *mh)这个方法中,最后有个call_load_methods();方法,点击进去。

  • 3.在void call_load_methods(void)这个方法中,找到call_class_loads();这个方法,上面已经讲到,这是调用类的load方法。点进去。

  • 4

  • 5.为了搞清楚这里的classes数组的来历,我们回退到void load_images(const char *path __unused, const struct mach_header *mh)这个方法,这个方法中有一个prepare_load_methods((const headerType *)mh);这个方法,根据方法名可能和我们的问题有关。因此我们点进这个方法查看一下

  • 6.

  • 7.点进

1
schedule_class_load(remapClass(classlist[i]));

这个方法:

通过这个方法我们就可以很清晰的看到,当要把一个类加入最终的这个classes数组的时候,会先去上溯这个类的父类,先把父类加入这个数组。

由于在classes数组中父类永远在子类的前面,所以在加载类的load方法时一定是先加载父类的load方法,再加载子类的load方法。

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

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

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

下面我们通过打印结果验证一下,这是编译顺序:

按照我们前面的分析,load方法的调用顺序应该是:

Person -> Student -> Person + Test1 -> Student + Test2 -> Student + Test1 -> Person + Test2。

我们看一下打印结果:

1
2
3
4
5
6
7
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方法

  • 按照编译先后顺序调用(先编译,先调用)

  • 调用子类的load方法之前会先调用父类的load方法

2.再调用分类的load方法

  • 按照编译先后顺序,先编译,先调用

initialize方法

initialize方法的调用时机

  • initialize在类第一次接收到消息时调用,也就是objc_msgSend()。

  • 先调用父类的+initialize,再调用父类的initialize。

  • 我们首先给Student类和Person类覆写+initialize方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//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类发送消息,也就是调用函数:

1
[Person alloc];

打印结果:

1
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方法:

1
2
3
[Person alloc];
[Person alloc];
[Person alloc];

打印结果:

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

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

1
[Student alloc];

打印结果:

1
2
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方法,判断是第几次接收到消息。

  • 1.我们去runtime源码中搜索class_getClassmethod方法,会在objc-class.mm这个文件中找到这个方法的实现:

  • 2.我们点进class_getInstanceMethod(cls->getMeta(), sel);这个方法:

  • 3.点进这个方法:

     

4.继续寻找lookUpImpOrForward这个方法的实现,我截取其中有价值的代码块:

这个代码也说明了每个类的+initialize方法只会被调用一次。

  • 5.我们点进_class_initialize (_class_getNonMetaClass(cls, inst));寻找真正的实现:

  • 6.然后我们通过callInitialize(cls);查看具体的调

这样一来+initialize方法的调用过程就很清楚了。

+initialize的调用过程:

  • 1查看本类的initialize方法有没有实现过,如果已经实现过就返回,不再实现。

  • 2.如果本类没有实现过initialize方法,那么就去递归查看该类的父类有没有实现过initialize方法,如果没有实现就去实现,最后实现本类的initialize方法。并且initialize方法是通过objc_msgSend()实现的。

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

  • 如果子类没有实现+initialize方法,会调用父类的+initialize(所以父类的+initialize方法可能会被调用多次)

  • 如果分类实现了+initialize,会覆盖类本身的+initialize调用。

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

1
2
    [Student alloc];
    [Teacher alloc];

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

1
2
3
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方法竟然被调用了三次,这多少有些出乎意外吧。下面我们来分析一下。

1
2
3
4
5