先来看一个升级版面试题:
1、load与initialize分别是何时调用的?以及load与initialize这两个方法的在父类,子类,分类之间的调用顺序是怎样的?
2、分类实现了类的initialize方法,那么类的方法initialize还会调用吗?为什么?
针对这个面试题,我们继续深入底层,本篇文章结构:
- load函数与initialize函数调用时机
- 类(Class)的方法和分类(Category)的方法之间的调用关系(为什么分类的方法会覆盖类的方法)
- 面试题答案(笔者总结,仅供参考)
一、load函数与initialize函数调用时机及顺序
新建工程,实现父类BMPerson、子类BMStudent和子类的分类BMStudent(Cover),分别重写这三个类的load以及initialize,在main函数里面也做个函数打印,运行后打印结果如下
从打印结果我们粗略的能看出:
不管是子类,父类还是分类,load方法的调用都在main函数之前就已经调用了
而initialize方法则是在main函数之后,也就是程序运行的时候才开始调用
先来看load,结合笔者上篇深入App启动之dyld、map_images、load_images,我们其实知道:
load方法调用时机其实就是在程序运行,Runtime进行
load_images
时调用的,在main函数之前,父类子类分类的调用顺序是:先调用类,后调用所有分类;调用类会先递归调用父类,后调用子类;分类和类的调用顺序没有关系,是根据Mach-O文件的顺序进行调用的。
接下里我们分析initialize的调用时机及调用关系。
由于我们同时打印父类,子类,分类发现子类的并不调用,接下来我们注释掉分类的initialize,查看打印结果:
然后在子类的initialize中打上断点,查看函数调用堆栈:
利用控制变量的思想,从以上的所有打印结果,我们能得出:
1、子类父类分类的调用顺序是:如果实现了分类:先父类后分类,并且不再调用原来子类中的initialize;如果没有实现分类:先父类后子类
2、initialize方法调用时机是在Class对象进行初始化时,通过Runtime的消息转发机制,查找方法的imp然后进行调用的,对比load方法,它是在main函数之后,对象创建初始化的时候调用的。
那么问题来了:为什么分类的initialize会覆盖类的initialize呢?接下来我们从源码进行分析
二、类(Class)的方法和分类(Category)的方法之间的调用关系(为什么分类的方法会覆盖类的方法)
先思考:为什么分类的方法会覆盖类的方法呢?我们知道方法调用底层就是通过Runtime进行消息转发,去对应类的methodList进行方法编号imp查找,然后调用 而且上一篇深入App启动之dyld、map_images、load_images对map_images
进行分析过,在类的结构中方法都存储在data的methods方法表里面,这个表的类型是method_list_t
,method_list_t
的父类list_array_tt
会提供attachLists
方法把分类的方法都添加到类里面,中间也没有进行任何去重这种敏感的操作,而且从Mach-O文件中我们也能看出:类的方法并没有被分类覆盖掉,这类的initialize方法以及分类的initialize方法的地址也不一样,这两个方法都还存在。
既然存的时候,都存进去了,那么只有一种可能:在方法调用的时候,肯定做了只会读分类的方法的逻辑操作!
从上面断点打印的调用堆栈信息,我们直接进入Objc
源码搜索lookUpImpOrForward
,代码如下
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
//1、先从缓存查找,如果有就取出来cache_getImp;缓存没有,先看类是否实现,如果没实现就去实现并初始化
// Optimistic cache lookup
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
......
runtimeLock.lock();
checkIsKnownClass(cls);
if (!cls->isRealized()) {
realizeClass(cls);
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
}
//2.开始retry查找
retry:
runtimeLock.assertLocked();
// Try this class's cache.
//从这个类的缓存中查找
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
//
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// Try superclass caches and method lists.
//从父类的缓存以及方法列表里面进行查找
{
......
}
// No implementation found. Try method resolver once.
//没有找到,尝试一次动态方法解析_class_resolveMethod,方法还是找不到imp,看看开发者是否实现预留的方法resolveInstanceMethod或者resolveClassMethod
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
......
triedResolver = YES;
goto retry;
}
//还是找不到,就进行消息转发,打印方法找不到
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
return imp;
}
整个方法lookUpImpOrForward的imp查找过程大致就是三步:
- 从Optimistic cache缓存中查找
- 找不到先判断类是否实现,如果未实现就进行实现
- 然后开始retry查找
retry中的imp查找过程就是
- 先查找类的缓存和方法列表
- 在查找父类的缓存和方法列表
- 以上都找不到就进行一次动态方法解析,查看开发者针对该类有没有实现了设计时预留的方法resolveInstanceMethod或者resolveClassMethod
- 如果动态方法解析还找不到就进行消息转发,然后打印方法找不到
我们的场景主要是查看initialize方法的调用顺序,所以查看第一步,从类里面找就行了。
找到类方法查找的关键函数getMethodNoSuper_nolock
并找到关键函数search_method_list
点击进入,下面贴上这两个函数的源码
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
assert(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists)
{
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
#if DEBUG
// sanity-check negative results
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
#endif
return nil;
}
在search_method_list
找到关键函数findMethodInSortedMethodList
,重点来了!!!!!!!!!!
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
assert(list);
const method_t * const first = &list->first;
const method_t *base = first;
const method_t *probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)probe->name;
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
return (method_t *)probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
注意其中for循环中的一段核心代码及注释!这段代码正是category覆盖类方法的关键点!这段代码的逻辑就是:**倒序查找方法的第一次实现 **
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
return (method_t *)probe;
}
结合之前我们分析map_images
加载顺序:先加载父类->再子类->所有类的分类。所以在消息转发查找imp的时候,一定会从表的后边往前边查,而分类中的方法正是最后添加的!所以如果这个类分类也实现了这个方法,一定会先找分类中的方法,这里的逻辑正是分类重写的精髓所在!
知道了为啥分类中的方法会覆盖类中的方法之后,笔者从源码中也看出了分类方法会覆盖类中的,但是分类之间是没有绝对的先后顺序的,所以我们在为类添加分类的时候需要注意这一点,不然可能会导致分类之间互相影响。
三、面试题答案(笔者总结,仅供参考)
1、load与initialize分别是何时初始化的?以及load与initialize这两个方法的在父类,子类,分类之间的调用顺序是怎样的?
load调用时机
main函数之前,Runtime进行
load_images
时调用
load调用顺序
父类子类分类的调用顺序是:先调用类,后调用所有分类;调用类会先递归调用父类,后调用子类;分类和类的调用顺序没有关系,是根据Mach-O文件的顺序进行调用的。
initialize调用时机
main函数之后,Runtime通过消息转发查找方法的imp,在
lookUpImpOrForward
时,在类的方法列表中找到并调用
initialize调用顺序
如果分类中重写了initialize方法,则调用顺序:先父类后分类
如果分类未重写initialize方法,则调用顺序:先父类后子类
2、分类实现了类的initialize方法,那么类的方法initialize还会调用吗?为什么?
分类中实现的类的initialize方法,那么类的方法就不会调用了。
之所以出现这种覆盖的假象,是因为
map_images
操作方法的时候,是先处理类后处理分类的,所以方法存进类的方法的顺序是:先添加类,后添加分类。但是在Runtime查找imp的时候,是倒序查找类的方法列表中第一个出现的方法,只要找到第一个就直接返回了,所以会出现分类方法覆盖类方法的假象。