类的加载原理——(上)

884 阅读5分钟

前言

我们上一篇章应用程序的加载原理中还遗留了一些问题,这些问题跟我们后面讲的类的加载原理还是有些关系的,所以这里首先我们把这个问题分析下。

map_images & load_images的调用

我们在应用程序的加载原理中讲到,_dyld_objc_notify_register(&map_images, load_images, unmap_image) ——> registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped),我们看下图:

截屏2022-05-04 下午7.04.57.png

我们看map_imagesload_images的调用,其实就是这里sNotifyObjcMappedsNotifyObjcInit的调用。我们在dyld源码中全局搜索下sNotifyObjcMapped,找到它被调用的地方,最终找到如下图:

Xnip2022-05-04_19-38-39.jpg

我们看到sNotifyObjcMapped是在notifyBatchPartial()方法中被调用的,我们接着找notifyBatchPartial被调用地方:

截屏2022-05-04 下午7.04.57.png

我们看到它在registerObjcNotifiers被调用,也就是sNotifyObjcMapped被调用的流程是这样的:_objc_init ——> _dyld_objc_notify_register ——> registerObjCNotifiers ——> notifyBatchPartial ——> sNotifyObjcMapped(map_images)这样的调用顺序。我们接着全局搜索下sNotifyObjcInit,看它是在哪里被调用的:

截屏2022-05-04 下午8.31.14.png

截屏2022-05-04 下午8.33.10.png

我们从上图可以很清晰看到,sNotifyObjcInit(load_images)notifySingle中调用,而notifySinglerecursiveInitialization中被调用,所以sNotifyObjcInit调用流程是这样:recursiveInitialization ——> notifySingle ——> sNotifyObjcInit;而如果你看过我之前的应用程序加载原理可以知道,_dyld_objc_notify_register是沟通dyldobjc的一个重要函数。因为在objc源码中可以看到该函数被_objc_init调用:

截屏2022-05-04 下午8.18.15.png

_objc_init的调用流程是这样的:recursiveInitialization(dyld) ——> doInitialization(dyld) ——> doModInitFunctions(dyld) ——>libSystem_initializer(libSystem.dylib) ——> libdispatch_init ——> _os_object_init ——> _objc_init ——> _dyld_objc_notify_register;我们在recursiveInitialization方法中可以看到,notifySingle的调用是在doInitialization(初始化)之前的,两者都是在recursiveInitialization中被调用的。这里就有点奇怪了,我们的sNotifyObjcMapped(map_images)为啥会在doModInitFunctions(初始化)之前调用呢,其实并不冲突,因为recursiveInitialization这里是递归进行初始化的,第一次进来的时候可能什么文件也没有,所以并一定会调用sNotifyObjcMapped。我们看到recursiveInitialization方法中,有两次调用notifySingle的地方,若初始进来,第一个notifySingle没有调用,那么相当于首先调用doModInitFunctions初始化map_images,接着执行map_images(),之后调用context.notifySingle()进行load_images的调用。

load_images方法解析

load_images方法里具体做了什么呢,我们看下它的相关实现:

截屏2022-05-05 下午6.52.25.png

我们可以看到两个关键的方法prepare_load_methods((const headerType *)mh)call_load_methods(),我们先看第一个方法prepare_load_methods

截屏2022-05-05 下午6.57.59.png

我们先进入schedule_class_load()方法看下它的实现:

截屏2022-05-05 下午7.03.37.png

可以看到它是一个递归调用吗,找完当前类,会接着添加它的父类,直到clsnil。我们接着进入add_class_to_loadable_list(cls)

截屏2022-05-05 下午7.13.03.png 我们可以非常清楚的看到,这里是获取当前的类的load方法,然后存入loadable_classes中。我们可看下getLoadMethod()方法:

截屏2022-05-05 下午7.15.33.png

分类跟主类是同样的存储方法,我们看下add_category_to_loadable_list

截屏2022-05-05 下午7.18.52.png

我们可以看到同样是_category_getLoadMethod(cat)获取分类load方法,然后存入loadable_categories中,只不过跟类存的不在一个地方。我们现在看下call_load_methods()方法:

截屏2022-05-05 下午7.25.11.png

我们其实可以非常清楚地看到,这里就是调用类里和分类里找到的那些load方法,我们可以进去看下call_class_loads()

截屏2022-05-05 下午7.27.18.png

所以到这里我们基本可以明白,load_images里主要就是查找类及分离里的load方法,并调用它。

cxx(c++)与load方法的调用顺序解析

我们在main.m文件中声明一个c++方法,CTPerson实现下load方法,如下图:

截屏2022-05-04 下午11.12.33.png

截屏2022-05-04 下午11.13.56.png

我们运行下工程,打印输出看下结果:

截屏2022-05-04 下午11.17.10.png

这里我们看到cxx函数在doInitialization调用的时候就被调用了,但是load方法是在notifySingle里调用的,为啥load打印输出在cxx之前呢。这里我们在objc源码中再定义一个c++方法,运行调试下结果就清楚了:

截屏2022-05-04 下午11.27.35.png

调试打印结果:

截屏2022-05-04 下午11.31.16.png

第一个打印输出的cxx是存在于objc源码中,说明首先调用doInitialization初始化的镜像文件,属于镜像文件的内部初始化,并不属于当前工程的初始化,然后执行的load(context.notifySingle)方法,接着才执行的工程中的cxx方法。

objc_init分析

我们上面讲到_objc_init ——> _dyld_objc_notify_register,这两个方法是重点,我们先看下_objc_init方法:

void _objc_init(void)
{
    static bool initialized = false;

    if (initialized) return;
    initialized = true;

    // fixme defer initialization until an objc-using image is found?
    //环境变量的初始化
    environ_init();
    //关于线程key的绑定 - 比如每线程数据的析构函数
    tls_init();
    //运行C++静态构造函数,在dyld调用我们的静态函数之前,’libc‘会调用我们的objc_init()
    static_init();
    //runtime 运行时环境初始化
    runtime_init();
    //初始化libobjc的异常处理系统
    exception_init();

#if __OBJC2__
    //缓存条件初始化
    cache_t::init();
#endif
    //启动回调机制
    _imp_implementationWithBlock_init();
    //&map_images --- 指针传递,保持同步变化
    //load_images--- 主要是load方法的调用
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

map_images解析

我们看下map_images方法实现,我们这里主要看镜像文件的加载过程,所以就寻找镜像文件相关的代码:

截屏2022-05-05 下午4.12.20.png

Xnip2022-05-05_16-13-34.jpg

我们从map_images方法中找到map_images_nolock,然后在map_images_nolock看到一句重要的代码_read_images,我们重点看下这里。

_read_images解析

这里_read_images是如何读取镜像文件呢,我们看下该方法的实现,这里代码很长,我们先从整体看下它的大致流程:

Xnip2022-05-05_16-33-40.jpg

这里我们从它的相关log输出看到一些大概的流程,错误类处理、协议读取、分类处理等相关处理,我们重点关注类的相关读取和加载处理。我们可以看到一个重点方法readClass:

截屏2022-05-05 下午5.23.50.png

进入readClass方法后,这时候我们调试下,因为代码判断有很多,我们可以打印出我们自定义的类进行调试观察:

Xnip2022-05-05_17-54-56.jpg

经过工程调试验证,代码最终没有进入rw、ro读取那里,只是调用了addNamedClass(cls,mangledName,replacing)addClassTableEntry(cls)

截屏2022-05-05 下午6.04.31.png

我们可以看下addNamedClass,这里是把类名等相关信息存入一个全局表中:

截屏2022-05-05 下午6.17.05.png

这个NXMapTable是在read_images条件控制一次加载的过程创建的,我们可以看到相关代码:

截屏2022-05-05 下午6.21.04.png

这里创建了一个全局表,用于存储一些类的相关信息,而我们在objc_init那里看到runtime_init有创建了一张表,它用于存储开辟过内存的类的相关存储:

截屏2022-05-05 下午6.24.40.png 代码注释中也有明确指出:

截屏2022-05-05 下午6.25.16.png

这里readClass后面调用的addClassTableEntry就是插入的这张表allocatedClasses

截屏2022-05-05 下午6.28.05.png

分析到这里,其实我们还是有点奇怪的,因为到目前为止,类的rw、ro还没有进行处理过,它是在哪里处理的呢,我们后面进行讲解。