从objc_init说起(一)

505 阅读5分钟

提出问题

说到应用初始化之前,先提出三个问题,先想想这三个问题你确实明白了吗?

  1. load方法何时调用
  2. 子类、父类、分类的load方法的调用顺序
  3. 子类、父类、分类的initialize方法的调用顺序 在回答这三个问题之前首先要明确的是整个app的加载过程是如何的

编译过程

  • 源文件:载入.h、.m、.cpp等文件
  • 预处理:替换宏,删除注释,展开头文件,产生.i文件
  • 编译:将.i文件转换为汇编语言,产生.s文件
  • 汇编:将汇编文件转换为机器码文件,产生.o文件
  • 链接:对.o文件中引用其他库的地方进行引用,生成最后的可执行文件 编译过程大致可分为以上五步,同时在这里还需要了解静态库动态库

静态库

静态库的特点就是在链接阶段,就会把目标程序和引用的库一起打包到可执行文件中,静态库此时就不可以更改了,这样做的优点是在编译完成后,如果目标程序不需要外部依赖则可以直接执行,缺点正是对应其优点来讲,因为是静态库是直接拷贝的,所以会造成目标程序体积增大,对性能是有损耗的。

动态库

与静态库相对比,动态库在编译的时期不会直接链接到目标程序中,目标程序只会存储指向动态库的引用,在程序运行时才被载入,这样它的优点就有三个:

  • 减小了目标程序的体积因为并不会像静态库一样打包进去
  • 可使用同一片共享内存同一个库可以被多个应用程序来使用
  • 灵活类似于热更新由于运行时才载入的特性使得动态库可以随时更新 缺点就是相比于静态库因为所谓的灵活,动态会损失一部分性能。

加载过程

image.png 整个加载过程用这张图展示的非常清晰,这里同时要注意一个非常重要的函数_dyld_objc_notify_register 这个函数就是把整个程序开启runtime回调的开始,系统库通过dyld管理,同时提供了运行时的特性。

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();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

image.png 那么通过以上的分析,发现这里是很有必要去从dyld开始分析,看看是如何从dyld切换到runtime这里的。

app启动的开始

image.png 接下来会从dyld的源码中开始分析

  • dyld整个工程中搜索__dyld_start可以看到这段是卸载汇编里的,同时这里会调起一个函数dyldbootstrap::start

image.png

  • 继续在工程里搜索dyldbootstrap::start,整个函数最重要的地方就是调起了dyldmain函数

image.png

由于dyld::main这里的代码非常长,此处仅展示部分重要代码,整个过程可以划分为9步,接下来会分步来剖析整个过程

1.配置环境变量

这一步主要是检查设置的环境变量 image.png

2.检查共享缓存

检查是否开启了共享缓存,并且是否映射到了共享区域,例如系统的UIKitimage.png

3.主程序的初始化

调用instantiateFromLoadedImage函数加载可执行文件并生成ImageLoader对象 image.png 进入instantiateFromLoadedImage函数

image.png

其内部调用instantiateMainExecutable,为主要可执行文件创建映像,返回主程序 image.png

4.插入动态库

通过遍历DYLD_INSERT_LIBRARIES环境变量来加载所有指定的动态库 image.png

5.链接主程序

image.png

6.链接动态库

image.png

7.弱符号绑定

image.png

8.执行初始化方法

image.png

initializeMainExecutable为所有插入的dylibs都执行初始化,也就是runInitializers方法 image.png

runInitializers方法中核心代码是processInitializers image.png

processInitializers在镜像列表中调用递归实例化,以建立未初始化的向上依赖关系的新列表 image.png recursiveInitialization中有两块地方要注意notifySingledoInitialization image.png

notifySingle这里的重点在于(*sNotifyObjCInit)(image->getRealPath(), image->machHeader())

image.png 找到sNotifyObjCInit的赋值 image.png 顺藤摸瓜找到调用的地方_dyld_objc_notify_register是不是非常眼熟这个函数,没错,这个就是我们一开始在libobjc中找到的函数

image.png

image.png 这时怎能看出notifySingle是一个回调函数,会调起在libobjc中传入的load_images方法 那么load_images方法究竟做了什么

load_images

image.png

prepare_load_methods image.png schedule_class_load 在类中会递归加载load方法到loadable_classes这个表中,此时注意加载顺序,由于是递归加载,那么顺序一定是找到祖先类、父类、子类,以这种顺序加载到表中 image.png add_category_to_loadable_list同样加载分类也是会将所有分类的load方法加载到loadable_categories这张表中,由于分类不存在继承关系,故不像类的加载要采用递归加载的方式。 image.png call_load_methods中先调用所有类里面的load方法后接着调用分类里load方法

image.png call_class_loads image.png 看到这里就可以得到load_image调用了所有的load函数,这里就可以回答了之前的回答,load方法的调用是先类,后分类,其中类的调用顺序与之前加载顺序是一致的,即祖先类、父类、子类的顺序。

doInitialization

刚才是把从dyld_start开始一路找到了libobjc中,那么objc_init究竟是如何调用的,也就是dyld如何到libojbc的呢?先回到recursiveInitialization image.png 这里同样有两个重要的函数doImageInitdoModInitFunctions

doImageInit

image.png doImageInit这里是循环加载方法的调用 image.png doModInitFunctions循环加载了所有Cxx文件 image.png 走到这里依旧是没发现objc_init是如何调起的,这时去看下调用栈的信息

image.png 可以看到doImageInit后调用了libSystem_initializer,那么去libSystem寻找

image.png

image.png

image.png

9.找到主程序入口即main函数

image.png

initialize

文章刚开始提到的问题中,针对load的问题已分析完成,接下来来看看initialize,首先来看下是如何调用的

image.png 很明显已经是在main之后才开始调用的,明显晚于load的调用 首先找到lookUpImpOrForward

image.png

image.png

image.png 此处又可以看到是递归调用,当全部准备做好后,开始调用 callInitialize image.png

image.png