写在前面
+load 和 +initialize 方法大家肯定不陌生,我们的项目中会有很多重写这两个方法的地方,但是你有没有想过他们有什么区别?产生区别的原因是什么?今天我们就从源码的层面来解答一下这些问题。
阅读文章时请注意:留意源码中我增加注释的部分,往往是该函数的重点部分。
问题
针对 load 和 initialize 方法,我们先来思考几个问题:
- 分类中有 load 和 initialize 方法吗?
- 如果有,那么分类会覆盖原类的方法吗?
- 调用顺序是怎样的,子类和父类哪个先?
- 调用子类时会不会调用父类的方法?
由于源码较多,将全部源码贴出来不太现实,因此最好的方法是下载runtime源码,跟着文章一起阅读。
+load方法
调用时机
load方法的调用时机是runtime加载类和分类的时候调用。
我们跟上一篇文章一样,定义一个Person类和Person的两个分类
// Person
+ (void)load {
NSLog(@"person");
}
+ (void)test {
NSLog(@"person test");
}
// Person + Eat
+ (void)load {
NSLog(@"Person Eat");
}
+(void)test {
NSLog(@"Person(Eat) test");
}
2020-06-01 19:25:49.718843+0800 TestCategory[38073:13350285] person
2020-06-01 19:25:49.719425+0800 TestCategory[38073:13350285] Person Eat
2020-06-01 19:25:49.895610+0800 TestCategory[38073:13350285] Person(Eat) test
可以看到,load方法不论分类还是原类都会被调用,且每个类只会被调用一次,但是test方法却只调用了一次。
test方法只调用一次的原因在上一篇文章中已经解释了,分类的方法会放到list的前面,通过msgSend调用方法时会先查找到分类的方法。
load方法为什么会调用两次呢?我们还是要从runtime源码中寻找答案。
runtime 源码解析
- 我们同样从 objc-os.mm 中的入口函数 _objc_init 入手,这个函数中调用了一个函数 _dyld_objc_notify_register(&map_images, load_images, unmap_image) ,我们点进load_images函数。
void
load_images(const char *path __unused, const struct mach_header *mh)
{
// 判断是否Categories已经被attach,attach相关详见上期
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
loadAllCategories();
}
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
// 调用 call_load_methods
call_load_methods();
}
- 可以看到调用load方法的函数叫做 call_load_methods(),我们点进去看一看
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
// 1. 重复的调用 类 的+load方法直到所有类的load方法都被调用完
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
// 2. 对每个 分类 调用一次+load方法
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
- 这里就不难看出,原类的 +load 方法一定比分类的 +load 方法要早调用
- 调用 +load 方法的地方有两个,一个叫 call_class_loads(),一个叫 call_category_loads(),我们先点进 call_class_loads()看一看
/***********************************************************************
* call_class_loads
* Call all pending class +load methods.
* If new classes become loadable, +load is NOT called for them.
*
* Called only by call_load_methods().
**********************************************************************/
static void call_class_loads(void)
{
int i;
// Detach current loadable list.
// 留意这一行代码,后面会用到
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
// typedef void(*load_method_t)(id, SEL);
// load_method_t是一个指针类型,这里创建一个指针指向该类的load方法的地址
// 可以看到这里classes实际上是上面的loadable_classes
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
// 这里是调用load方法的地方,可以看到是通过地址找到相应的函数后调用
(*load_method)(cls, @selector(load));
}
// Destroy the detached list.
if (classes) free(classes);
}
这里就破案了
通过上面的代码可以看出,+load 方法的调用不是通过msgSend,而是直接找到该类 +load 方法的地址,通过地址来调用 +load 方法,因此不会出现只调分类的 +load 方法而不调原类的 +load 方法的情况。
有继承关系时的 +load 调用顺序
实验
我们一共创建4个类,分别是
- Person
- Person(Eat)
- Student
- Student(Eat)
经过多次实验得到如下结论:
- 原类的 +load 方法总是在分类之前被调用(修改Build Phases → Compile Sources 中的顺序后也是这样)
- 父类的 +load 方法总是在子类之前被调用(修改顺序后也是如此)
- 分类的父类 +load 方法可能在 分类的子类的 +load方法后才会被调用,和Compile Sources中的顺序有关
为了搞清楚这个结论的原因,我们还是要从runtime的源码入手。
源码
原类
在上面的 call_class_loads 函数中,是从 loadable_classes 中将函数地址一个一个取出来的,因此我们需要搞清 loadable_classes 是怎么来的,以及他的顺序是怎么样的。
由于是从 call_class_loads 函数一步一步往回找,因此这里的调用顺序是倒序的,大家要注意这点。
- 全局搜索一下关键字 loadable_classes = ,可以发现在 add_class_to_loadable_list(Class cls) 中有出现,我们来看下这个函数的实现:
/***********************************************************************
* add_class_to_loadable_list
* Class cls has just become connected. Schedule it for +load if
* it implements a +load method.
**********************************************************************/
void add_class_to_loadable_list(Class cls)
{
IMP method;
loadMethodLock.assertLocked();
// 如果没有实现 +load 方法,那么不需要添加到list中
method = cls->getLoadMethod();
if (!method) return; // Don't bother if cls has no +load method
if (PrintLoading) {
_objc_inform("LOAD: class '%s' scheduled for +load",
cls->nameForLogging());
}
// 可以看到当list容量不够时,扩充的逻辑是 *2 + 16
if (loadable_classes_used == loadable_classes_allocated) {
loadable_classes_allocated = loadable_classes_allocated*2 + 16;
loadable_classes = (struct loadable_class *)
realloc(loadable_classes,
loadable_classes_allocated *
sizeof(struct loadable_class));
}
// 将class存放到list中,并将method赋值给对应的cls
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
- 我们已经知道 add_class_to_loadable_list(cls) 是将该 class 放入list,现在看一下谁调用了 add_class_to_loadable_list,发现在 schedule_class_load(Class cls) 函数中有调用,我们来看一下这个函数的实现
/***********************************************************************
* prepare_load_methods
* Schedule +load for classes in this image, any un-+load-ed
* superclasses in other images, and any categories in this image.
**********************************************************************/
// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
static void schedule_class_load(Class cls)
{
if (!cls) return;
ASSERT(cls->isRealized()); // _read_images should realize
// #define RW_LOADED (1<<23)
// 这里目测是某个标志位,猜测代表已经加入到了list
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
// 这里可以看出,这个方法是递归调用的,每个类传进来会先去将他的父类加进来
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
// 设置标志位
cls->setInfo(RW_LOADED);
}
- 继续回溯,可以看到是一个叫做 prepare_load_methods(const headerType *mhdr) 调用了该方法。来看一下这个函数的实现
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
// 取到classList
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
// 执行schedule_class_load,将有 +load 方法的class加入到 loadable_classes 中
schedule_class_load(remapClass(classlist[i]));
}
// 分类相关操作,这里暂时不看
category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
realizeClassWithoutSwift(cls, nil);
ASSERT(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
- 再往上回溯,看看谁调用了 prepare_load_methods
void
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
loadAllCategories();
}
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
// 调用 prepare_load_methods
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
至此,我们已经把完整的链路打通了,prepare_load_methods 的调用方是 load_images,而 load_images 的调用方是是 objc_init ,我们来理一下方法的调用顺序与作用
那么原类的 +load 方法调用过程的源码我们已经分析完了,接下来让我们来看看分类的 +load 方法的调用过程是怎么样的
分类
还是先看一下 prepare_load_methods(const headerType *mhdr) 函数:
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
// 原类
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
// 分类
// _getObjc2NonlazyCategoryList 函数取出了所有分类,这里没有做任何顺序上的调整,因此是按照编译顺序直接拿到数组
category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
// 通过for循环,直接将 categorylist 中的分类加入到loadable_categories中
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
realizeClassWithoutSwift(cls, nil);
ASSERT(cls->ISA()->isRealized());
// 添加到 loadable_categories 列表中
add_category_to_loadable_list(cat);
}
}
因此可以知道,分类不会受子父类的关系影响,单纯的就是谁先编译谁先调用 +load 方法,我们可以对 Person(Eat) 和 Student(Eat) 调整编译顺序,来验证我们的结论。
如下面两幅图:
+initialize方法
调用时机
先说结论:
- +initialize 在方法第一次给类对象发消息的时候会调用。
- +initialize 仅会调用一次。
- 子类的 +initialilze 方法调用时会先调用父类的 +initialize 方法。
- 如果有分类,那么调用顺序和上一章一张,会调用到后编译的分类的 +initialize 方法
用上文中的Person, Person(Eat), Student, Student(Eat) 就可以进行验证,这里就不将验证过程放出来了。
从打印的结果来看是这样的,但是我们需要通过源码进行探究。
源码
- 我们在 objc-class.mm 这个文件中找到 class_getClassmethod 函数
Method class_getClassMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
// 可以看到这里是核心
return class_getInstanceMethod(cls->getMeta(), sel);
}
- class_getInstanceMethod 才是重点,这个函数传了两个参数,一个是 cls->getMeta(), 一个是 sel,我们点进这个函数看一下
/***********************************************************************
* class_getInstanceMethod. Return the instance method for the
* specified class and selector.
**********************************************************************/
Method class_getInstanceMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
// This deliberately avoids +initialize because it historically did so.
// This implementation is a bit weird because it's the only place that
// wants a Method instead of an IMP.
Method meth;
// 此时还没有给这个cls发过消息,因此不会在cache中找到
meth = _cache_getMethod(cls, sel, _objc_msgForward_impcache);
if (meth == (Method)1) {
// Cache contains forward:: . Stop searching.
return nil;
} else if (meth) {
return meth;
}
// Search method lists, try method resolver, etc.
// 这里是核心逻辑,注意这里传递的第四个参数是 LOOKUP_INITIALIZE | LOOKUP_RESOLVER
lookUpImpOrForward(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);
meth = _cache_getMethod(cls, sel, _objc_msgForward_impcache);
if (meth == (Method)1) {
// Cache contains forward:: . Stop searching.
return nil;
} else if (meth) {
return meth;
}
return _class_getMethod(cls, sel);
}
- 这里的核心函数是 lookUpImpOrForward(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER),注意最后一个参数,我们点进这个函数中看一下,这里我只放出了关键代码
// 可以看出 behavior & LOOKUP_INITIALIZE 是真, !cls->isInitialized() 也是真,因为没有调用过 initialized 方法
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
// 所以可以走到if判断里,这里是核心逻辑
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
// runtimeLock may have been dropped but is now locked again
// If sel == initialize, class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
- 核心函数是 initializeAndLeaveLocked(cls, inst, runtimeLock),我们一路点进去,最后会进入 initializeAndMaybeRelock,这里只放出核心逻辑
initializeNonMetaClass(nonmeta);
- 点进 initializeNonMetaClass 函数看一看,这里只放出核心代码
/***********************************************************************
* class_initialize. Send the '+initialize' message on demand to any
* uninitialized class. Force initialization of superclasses first.
**********************************************************************/
void initializeNonMetaClass(Class cls)
{
ASSERT(!cls->isMetaClass());
Class supercls;
bool reallyInitialize = NO;
// Make sure super is done initializing BEFORE beginning to initialize cls.
// See note about deadlock above.
supercls = cls->superclass;
// 这里也是递归调用
// 这里在找是否有super class,并且super class的initialize方法是否被执行过,如果没执行过,递归调用传递super class
if (supercls && !supercls->isInitialized()) {
initializeNonMetaClass(supercls);
}
// 这里是真正调用initialize方法的地方
callInitialize(cls);
}
- 至此,我们终于追踪到了真正调用 +initialize 方法的地方,让我们点进 callInitialize(cls) 函数一探究竟
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
asm("");
}
终于,真相大白了,我们看到真正调用 +initialize 方法的地方,是通过 objc_msgSend 函数,这也就解释了为什么会调用到分类的 +initialize 方法,详情见上一篇文章。
我们也终于验证了,父类的 +initialize 方法会先调用,且只会被调用到一次。
写在后面
希望大家多多上手尝试,不要觉得看过了就是会了,对于知识需要深层次的挖掘,浅尝辄止是没有用的。
文中如有错误,欢迎指出。