iOS底层学习——load和initialize分析

1,294 阅读6分钟

上一篇文章应用程序加载了解了应用程序的加载流程,dyldobjc_init的启动关系,也了解了map_imagesload_images的调用时机。本片文章我们需要解决遗留的一些问题,同时在上一篇文章的基础上做一些扩展。

1.+load()方法

在学习应用程序加载dyld的时候,我们定位到了load_images方法的调用时机。load方法是我们平时开发中常用的,很多面试题也经常会出现,比如父类分类load方法的调用顺序等。这里我们首先通过案例明确一下load方法的调用顺序,然后结合源码去分析。

1.案例分析

引入一个案例,父类Fater、子类Son及其分类均实现了load方法,见下面代码:

// 父类
@implementation Father
+ (void)load{
    NSLog(@"%s", __func__);
}
@end

// 父类分类
@implementation Father (GF)
+ (void)load{
    NSLog(@"%s", __func__);
}
@end

// 子类
@implementation Son
+ (void)load{
    NSLog(@"%s", __func__);
}
@end

// 子类分类
@implementation Son (GF)
+ (void)load{
    NSLog(@"%s", __func__);
}
@end

运行程序,会发现,系统会自动调用各个类的load方法。调用顺序为:父类->子类->分类。见下图:

image.png

2.load方法的过程

那么上面案例中的load方法是什么时候调用的呢? 上一篇中我们知道,调用sNotifyObjCInit也就是调用了load_images,在libobjc.A.dylib源码中可以找到load_images实现。见下图:

image.png

load_images函数实现中,找到load方法的调用入口:call_load_methods。同时根据注释,我们也可以验证这一点。在call_load_methods 中会反复调用class+load方法,直到没有更多。

image.png

  1. 类load方法

call_class_loads中实现了函数级别的load方法调用,见下图:

image.png

源码中是从loadable_classes获取获取的方法,并调用。

  1. 分类load方法

call_category_loads中实现了函数级别的load方法调用,见下图:

image.png

源码中是从loadable_categories获取获取的方法,并调用。

这里需要思考个问题,我们找到了load方法的调用过程,但是这个顺序在哪里确定下来的?load_images中有个关键地方还没有研究!

3.load方法调用顺序的确定

Discover load methods发现加载方法,这里会对load方法进行处理,并放入对应的loadable_classesloadable_categories表中。

load_images

prepare_load_methods方法到底在做什么准备工作呢?进去看看。见下面源码:

image.png

  1. 非懒加载的类处理

_getObjc2NonlazyClassList方法做什么呢?获取所有实现了+load方法的类(也就是非懒加载的类),加入到一个静态数组loadable_classes。看schedule_class_load的源码实现:

static void schedule_class_load(Class cls)
{
    if (!cls) return;
    ASSERT(cls->isRealized());  // _read_images should realize

    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); 
}

首先判断判断类是否为空,如果为空,直接返回;如果不为空,判断是否已经设置为RW_LOADED,如果已经被设置为RW_LOADED ,直接返回。然后进行了递归操作,获取当前类的父类,以确保父类优先进行了处理,然后调用add_class_to_loadable_list,将load方法添加到loadable_classes数组中。

  1. 非懒加载的分类处理

_getObjc2NonlazyCategoryList方法做什么呢?获取所有实现了+load的分类(非懒加载的分类),然后判断分类所对应的类是否为nil,如果分类所对应的类为nil则跳过,反之初始化分类所对应的类,然后将分类加入一个静态数组loadable_categories

  • 总结: 走完prepare_load_methods,也就是准备工作做好后,程序走到call_load_methods,调用load方法call_load_methods方法的实现,从代码中我们可以看出来,在这个方法中系统会把类本身的+load方法分类的+load方法都调用了,并且类的+load要比分类中先调用。 那么如果有多个分类都实现了+load,先调用哪个分类呢?这个和编译有关,编译时谁在前面谁先调用。

至此我们明白了load方法的调用顺序,但是依然有个问题,分类的方法是如何加载到类中的呢?后面的文章再解密!

2.+initialize()方法

initialize方法也是面试过程中经常问到的问题,那么它与load方法又有什么区别呢?它又是在哪里调用的呢?

1.initialize源码分析

其实在进行消息慢速查找流程中,我们已经找到了入口。见下图:

image.png

在此过程中,首先要确保类已经实现,也就是针对懒加载的类(未实现load方法)进行实现。类实现后,再进行initialize方法的调用。见下图:

image.png

这里只分析initialize方法的调用的调用过程,类的实现会在之后的章节中分析。

继续跟踪源码,最终找到了方法调用的位置: initializeAndLeaveLocked->initializeAndMaybeRelock->initializeNonMetaClass->callInitialize->objc_msgSend(cls, @selector(initialize))

void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
    asm("");
}
  • 通过该源码可以发现: initialize是通过obj_msgSend消息发送的方式进行调用的,与load方法调用有本质区别;load方法是在应用程序加载阶段,通过函数级别调用,而非消息发送。

那么initialize调用逻辑是怎样的呢?关键在initializeNonMetaClass实现中,见下图:

iShot2021-07-14 13.47.10.png

  • 所以可以得出以下结论:当一个类在发送消息时,采用递归的方式,优先判断父类是否isInitialized(),如果没有会调用对应的initialize方法,因为是消息发送逻辑,所以如果子类没有实现initialize方法,就会调用父类的initialize方法

2.initialize案例验证

依然引用分析load的案例,父类Fater、子类Son及其分类同时实现了load方法initialize方法,见下面代码:

// 父类
@implementation Father
+ (void)load{
    NSLog(@"%s", __func__);
}
+ (void)initialize
{
    NSLog(@"%s", __func__);
}
@end

// 父类分类
@implementation Father (GF)
+ (void)load{
    NSLog(@"%s", __func__);
}
+ (void)initialize
{
    NSLog(@"%s", __func__);
}
@end

// 子类
@implementation Son
+ (void)load{
    NSLog(@"%s", __func__);
}
+ (void)initialize
{
    NSLog(@"%s", __func__);
}
@end

// 子类分类
@implementation Son (GF)
+ (void)load{
    NSLog(@"%s", __func__);
}
+ (void)initialize
{
    NSLog(@"%s", __func__);
}
@end

直接启动程序,会发现只打印了load方法initialize并没有触发,这个结果也验证了前面的分析,load方法是由应用程序加载阶段,dyld流程中自动调动。而initialize是在向类发送消息的时候才会被触发。

  1. 创建Son对象,运行程序,结果如下:

    image.png

    • 首先调用了父类的initialize,再调用子类的initialize
    • 因为分类中也实现了initialize方法,所以会调用分类的initialize,本类不会被调用
    • load方法,先于initialize方法调用
  2. 创建Father对象,运行程序,结果如下:

    image.png

    • 不会触发子类的initialize方法
  3. 注意: 如果子类和子类的分类都没有实现initialize方法,在对子类进行发送消息时,父类的initialize方法会被调用两次!见下图:

    image.png

    • 在子类进行第一次消息发送时(父类没有调用过的情况下),进行递归处理,确保父类先调用了initialize方法,而子类本身没有实现,所以消息发送流程会找到父类,进行调用一次!所以会调用两次!