一、课程引进
创立一个工程,在ViewController中重写load办法,在main中加了一个C++办法 mideaCXXFun,同时在load办法、mideaCXXFun和main办法中添加log日志,下面咱们来 履行此demo,看一下log日志的打印结果:
从打印的日志能够看到履行的流程如下:
- 1.为什么是这么个调用次序呢?依照惯例思想了解,main不是入函数吗?为什么不是main最早履行?
- 2.mideaCXXFun函数没有调用,为什么会主动履行呢?
带着这些疑问,开端进入iOS底层源码发动流程的探索!!!
二、编绎进程和库
编译进程如下所示,首要分为以下几步:
静态库和动态库
静态库
在链接阶段,会将可汇编生成的方针程序与引证的库一同链接打包到可履行文件上中,静态库相当于被直接拷贝 了一份仿制到到方针程序里。
- 长处:方针程序没有外部依靠,直接能够运转;
- 缺陷:方针程序的体积增大,影响程序的内存、功能、速度。
动态库
程序在编译时不会将动态库拷贝到方针程序中,方针程序只会保存动态库的引证,在程序运转时才加载。
- 长处:减小包巨细、同享内存,节约资源、经过更新动态库,达到更新程序的目的;
- 缺陷:程序发动时刻需求加载动态库,会添加发动耗时;依靠外部资源。
三、DYLD加载流程剖析
什么是dyld?
dyld(the dynamic link editor)是苹果的动态链接器,苹果操作系统的重要 组成部分,在app被编绎打包成可履行文件格式的Mach-O文件后,交由dyld担任加载和链接。
dyld的源码是开源的,能够经过以下链接下载: opensource.apple.com/tarballs/dy…
1.APP发动的起始点
回到课程引进的demo,在load办法打下断点,运转,控制台输入“bt”打印调用仓库
(1)从仓库信息能够知道_dyld_start即为APP发动的起始点,翻开dyld“dyld-852.2”源码,大局查找“_dyld_start”
(2)_dyld_start是汇编完成的,内部调用了dyldbootrap:start函数,大局查找“dyldbootstrap”找到命名作用空间,再查找start函数
剖析start函数的完成能够发现,其回调调用了dyld::main函数,其间macho_header为Mach-O头部,而验证了dyld加载的文件便是Mach-O类型的可履行文件。
(3)进入dyld::_main完成,发现完成代码特别长,首要做了以下九件事
第一步:环境变量装备,根据环境变量设置相应的值,以及获取当时的运转架构
第二步:检查是否敞开同享缓存,并将同享缓存映射到同享区域,如UIKit、CoreFoundation等
第三步:主程序的初始化,加载可履行文件,为主程序实例化一个ImageLoader目标
第四步:刺进动态库,遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载一切的动态库
第五步:链接主程序
第六步:链接动态库
第七步:弱符号绑定
第八步:履行初始化办法
第九步:寻觅主程序进口—main函数
dyld::_main履行流程总结
(4)从dyld::_main的主流程咱们还未能清晰dyld是如何加载Mach-O文件的,以及未发现课堂引进比如中的问题的调用,下面咱们要点剖析一下第三步和第八步
第三步:sMainExecutable表示主程序变量,检查其赋值,是经过instantiateFromLoadedImage办法初始化主程序,检查其源码完成,创立了一个ImageLoader实例目标,经过instantiateMainExecutable创立
检查instantiateMainExecutable的完成,其作用是为主可履行文件创立镜像,回来一个ImageLoader类型的image目标,即主程序;其间sniffLoadCommands函数是获取Mach-O类型的Load Command并进行各种校验
第八步:履行初始化办法initializeMainExecutable函数,检查其完成,发现调用了runInitializers
大局查找“runInitializers(const”找到runInitializers源码完成,其进一步骤用了processInitializers函数
能够看到processInitializers调用了recursiveInitialization,检查注释的意思,镜像列表中的一切镜像调用递归实例化办法,以树立未初始化的向上依靠联系的列表
大局查找“recursiveInitialization(const”,检查其源码完成,经过注释咱们猜想并剖析notifySingle和doInitialization函数
来到notifySingle函数,其要害调用sNotifyObjCInit
大局查找sNotifyObjCInit,发现没有完成,只有在registerObjCNotifiers函数中有赋值,阐明它是一个回调函数
再来到registerObjCNotifiers的完成,发现在_dyld_objc_notify_register调用了它,大局查找_dyld_objc_notify_register,发现没有地方调用它
这儿的_dyld_objc_notify_register其实是在libobjc中调用的,下载一份libobjc的源码,下载地址为:opensource.apple.com/tarballs/ob… 。翻开libobjc“objc4-818.2”源码中查找“_dyld_objc_notify_register”
在libobjc中发现_objc_init中调用了_dyld_objc_notify_register,传入了map_images、load_images、unmap_image三个参数
结合registerObjCNotifiers的完成及调用链路能够知道
sNotifyObjcInit == load_images
在libobjc中大局查找load_images
来到call_load_methods函数的完成
进入call_class_loads函数完成,能够很明显看到,这儿循环调用了一切类的load办法
经过上面的剖析,咱们知道load_images调用了一切类的load,以上源码剖析进程正好对应课堂引进demo的仓库调用次序,至此解说了为什么load办法为什么会在main函数之前调用了
load办法调用链路总结
下面再来剖析一下doInitialization函数,回到dyld源码,来到doInitialization的完成,它首要调用了doImageInit和doModInitFunctions函数
来到doImageInit源码完成,其核心首要是for循环加载办法的调用,这儿需求注意的一点是,libSystem的初始化必须先运转.这儿会调用libSysteam中的相关办法,咱们能够下载libSysteam源码剖析(后边剖析),下载地址:opensource.apple.com/tarballs/Li…
检查doModInitFunctions完成,加载了一切的C++文件
回到课堂引进demo,在C++办法mideaCXXFun打上断点,bt打印仓库信息,由此咱们知道了C++的函数为什么会主动调用了,即doModInitFunctions。
经过前面的剖析,咱们知道了load办法和C++办法的调用逻辑。在剖析load办法的调用时,咱们发现是由_objc_init 调用_dyld_objc_notify_register给dyld注释了一个回调函数load_images,在dyld中调用load_images的后续调用链路中调用load办法的,那么_objc_init是在何时调用的呢?在dyld、libobjc、libSysteam源码中全查找都没有发现其调用,这让咱们怎么继续呢?
回到课堂引进demo,打上“_objc_init”的符号断点,运转卡住断点后bt打印仓库信息能够发现_objc_init的调用链路如下图红框所示:
来到libSysteam源码,大局查找libSystem_initializer函数。发现其的确调用了libdispatch_init函数。
下载libdispatch源码:opensource.apple.com/tarballs/li…
检查_os_object_init源码或查找_objc_init函数.总算咱们在libdispatch源码中找到了_objc_init的调用.
综合上述剖析,从_objc_init调用_dyld_objc_notify_register的load_images,到sNotifySingle的sNotifyObjCInit(==load_images)的调用形成一个闭环。总结一下_objc_init的调用链路:
来到第九步:寻觅main函数,最终dyld会寻觅进口函数main,假如项目中没有main函数的话会报错
dyld加载使用发动流程总结
此图源自月月大神的博客:www.jianshu.com/p/db765ff4e…
四、dyld与objc的相关
从_dyld_objc_notify_register函数到registerObjCNotifiers调用咱们能够发现如下的对应联系
前面咱们现已剖析过了sNotifyObjCInit(load_images)的调用逻辑,下面咱们看一下sNotifyObjCMapped(map_images)。dyld源码中查找sNotifyObjCMapped,发现在notifyBatchPartial函数中调用了它
查找notifyBatchPartial函数找到它的调用,发现在registerObjCNotifiers中调用了它,而且之后调用了sNotifyObjCInit(load_images)
结论:map_images是先于load_images调用,即先map_images ,再load_images。结合前面剖析的dyld加载流程,能够知道dyld的相关如下图所示
此图源自月月大神的博客:www.jianshu.com/p/db765ff4e…
回到_objc_init函数,咱们来看一下它做了哪些事情
- 1.初始化环境变量,并读取影响运转时的环境变量;
- 2.关于线程key的绑定,首要是本地线程池的初始化以及析构;
- 3.运转系统等级的C++静态结构函数,在dyld调用咱们自己完成的C++函数(静态结构函数)之前,libststem调用_objc_init,即”系统等级的C++结构函数先于自定义的C++结构函数运转”;
- 4.运转时的初始化,包含分类的初始化和类的初始化;
- 5.初始化libobjc的异常处理系统;
- 6.缓存的初始化;
- 7.发动回调机制;
- 8.dyld注册。
注册异常回调句柄
检查环境变量:environ_init(),咱们能够在终端输入“export OBJC_HELP=1”
Xcode一切环境变量对照表:
在Xcode项目中设置环境变量,以OBJC_PRINT_LOAD_METHODS为例,回到课堂引进demo,设置后运转起来即可在控制台打印完成了load办法的类和分类
五、由dyld加载使用发动流程引发的思考
- 1、发动时刻需求加载动态库,动态库过多会添加发动耗时,项目动态库的数量要控制一下,苹果建议是6个;假如动态库太多,能够考虑兼并动态库;
- 2、发动时刻会遍历一切完成了load办法的类并履行load办法,添加发动耗时;能够考虑将需求在load办法中完成的代码放到initialize办法中,这个办法会在第一次调用该类的办法时履行一次。
- 3、一切的C++函数都会在发动时刻主动履行,所以项目中应该尽量少写C++函数,这也会添加发动耗时;
- 4、苹果的Open Source为咱们开源了libobjc、libdispatch、libsysteam和dyld的源码等等,能够阅读他们了解iOS底层的完成逻辑。