Category的本质<一>
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的源码入手:
- 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方法:
//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中文件的放置顺序就是文件的编译顺序。
目前是Person类在Student类的前面编译,现在我们把Student类放到Person类的前面编译:
然后我们再运行一下程序,查看打印结果:
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.点进
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。
我们看一下打印结果:
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方法:
//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方法,判断是第几次接收到消息。
- 1.我们去runtime源码中搜索
class_getClassmethod
方法,会在objc-class.mm这个文件中找到这个方法的实现: - 2.我们点进
class_getInstanceMethod(cls->getMeta(), sel);
这个方法:
-
3.点进这个方法:
- 4.继续寻找
lookUpImpOrForward
这个方法的实现,我截取其中有价值的代码块: - 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消息:
[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的伪代码,下面再详细说明这个过程:
- 1.Student类收到alloc消息,开始着手准备调用initialize方法。首先判断自己有没有初始化过。
- 2.判断自己没有初始化过,所以就去找自己的父类Person类,看Person类有没有初始化过,发现Person类也没有初始化过,且Person类也没有父类,多以对Person类使用
objc_msgSend([Person class], @selector(initialize))
调用Person类的initialize方法。这是第一次调用Person类的initialize方法。 - 3.父类处理完后,再通过
objc_msgSend([Student class], @selector(initialize));
调用Student类的initialize方法,但是由于Student类没有实现initialize方法,所以通过其superclass指针找到父类Person类,然后调用了Person类的initialize实现。这是第二次调用Person类的initialize方法。 - 4.Teacher类收到alloc方法,开始准备调用initialize放啊发。首先判断自己有没有被初始化过。
- 5.判断自己没有被初始化过后,又开始判断其父类Person类有没有被初始化过,刚刚父类Person类已经被初始化过。
- 6.于是通过
objc_msgSend([Teacher class], @selector(initialize))
调用Teacher类的initialize方法。但是由于Teacher类没有实现initialize方法,所以只能通过superclass指针去查找父类有没有实现initialize方法,发现父类Person类实现了initialize方法,于是调用父类的initialize方法。这是第三次调用Person类的initialize方法。