四、initialize方法分析
我们可以采用和分析load方法时一样的策略来对initialize方法的执行情况,进行测试,首先将测试工程中所有类中添加initialize方法的实现。此时如果直接运行工程,你会发现控制台没有任何输出,这是由于只有第一次调用类的方法时,才会执行initialize方法,在main函数中编写如下测试代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
[MySubObjectOne new];
[MyObjectOne new];
[MyObjectTwo new];
NSLog(@"------------");
[MySubObjectOne new];
[MyObjectOne new];
[MyObjectTwo new];
}
return 0;
}
运行代码控制台打印效果如下:
2021-02-18 21:29:55.761897+0800 KCObjc[43834:23521232] initialize-cateOne:MyObjectOne
2021-02-18 21:29:55.762526+0800 KCObjc[43834:23521232] initialize:MySubObjectOne
2021-02-18 21:29:55.762622+0800 KCObjc[43834:23521232] initialize-cate:MyObjectTwo
2021-02-18 21:29:55.762665+0800 KCObjc[43834:23521232] ------------
可以看到,打印数据都出现在分割线前,说明一旦一个类的initialize方法被调用后,后续再向这个类发送消息,也不会在调用initialize方法,还有一点需要注意,需要注意,如果对子类发送消息,父类的initialize会先调用,再调用子类的initialize,同时,分类中如果实现了initialize方法则会覆盖类本身的,并且分类的加载顺序靠后的会覆盖之前的。下面我们就通过源码来分析下initialize方法的这种调用特点。
首先,在调用类的类方法时,会执行runtime中的class_getClassMethod方法来寻找实现函数,这个方法在源码中的实现如下:
Method class_getClassMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
return class_getInstanceMethod(cls->getMeta(), sel);
}
通过源码可以看到,调用一个类的类方法,实际上是调用其元类的示例方法,getMeta函数用来获取类的元类,关于类和元类的相关组织原理,我们这里先不扩展。我们需要关注的是class_getInstanceMethod这个函数,这个函数的实现也非常简单,如下:
Method class_getInstanceMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
// 做查询方法列表,尝试方法解析相关工作
lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
// 从类对象中获取方法
return _class_getMethod(cls, sel);
}
在class_getInstanceMethod方法的实现中,_class_getMethod是最终获取要调用的方法的函数,在这之前,lookUpImpOrForward函数会做一些前置操作,其中就有initialize函数的调用逻辑,我们去掉无关的逻辑,lookUpImpOrForward中核心的实现如下:
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
IMP imp = nil;
// 核心在于!cls->isInitialized() 如果当前类未初始化过,会执行initializeAndLeaveLocked函数
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
return imp;
}
initializeAndLeaveLocked会直接调用initializeAndMaybeRelock函数,如下:
static Class initializeAndLeaveLocked(Class cls, id obj, mutex_t& lock)
{
return initializeAndMaybeRelock(cls, obj, lock, true);
}
initializeAndMaybeRelock函数中会做类的初始化逻辑,这个过程是线程安全的,其核心相关代码如下:
static Class initializeAndMaybeRelock(Class cls, id inst,
mutex_t& lock, bool leaveLocked)
{
// 如果已经初始化过,直接返回
if (cls->isInitialized()) {
return cls;
}
// 找到当前类的非元类
Class nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// 进行初始化操作
initializeNonMetaClass(nonmeta);
return cls;
}
initializeNonMetaClass函数会采用递归的方式沿着继承链向上查询,找到所有未初始化过的父类进行初始化,核心实现简化如下:
void initializeNonMetaClass(Class cls)
{
Class supercls;
// 标记是否需要初始化
bool reallyInitialize = NO;
// 父类如果存在,并且没有初始化过,则递归进行父类的初始化
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
initializeNonMetaClass(supercls);
}
SmallVector<_objc_willInitializeClassCallback, 1> localWillInitializeFuncs;
{
// 如果当前不是正在初始化,并且当前类没有初始化过
if (!cls->isInitialized() && !cls->isInitializing()) {
// 设置初始化标志,此类标记为初始化过
cls->setInitializing();
// 标记需要进行初始化
reallyInitialize = YES;
}
}
// 是否需要进行初始化
if (reallyInitialize) {
@try
{
// 调用初始化函数
callInitialize(cls);
}
@catch (...) {
@throw;
}
return;
}
}
callInitialize函数最终会调用objc_msgSend函数来向类发送initialize初始化消息,如下:
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
asm("");
}
需要注意,initialize方法与load方法最大的区别在于其最终是通过objc_msgSend来实现的,每个类如果未初始化过,都会通过objc_msgSend来向类发送一次initialize消息,因此,如果子类没有对initialize实现,按照objc_msgSend的消息机制,其是会沿着继承链一路向上找到父类的实现进行调用的,所有initialize方法并不是只会被调用一次,假如父类中实现了这个方法,并且它有多个未实现此方法的子类,则当每个子类第一次接受消息时,都会调用一遍父类的initialize方法,这点非常重要,在实际开发中一定要牢记。
五、结语
load和initialize方法是iOS开发中非常简单也也非常常用的两个方法,然而其与普通的方法比起来,还有有一些特殊,通过对源码的解读,我们可以更加深刻的理解这些特殊之处的原因及原理,编程的过程就像修行,知其然也知其所以然,与大家共勉。