1 引入
新建工程,在viewContoll的+load函数中处设置断点,运行程序,可以看到程序的调用堆栈为下图所示
可知道_dyld_start的源码位于
dylib
断点调试可知程序的执行过程是:load-->c++函数-->main
c++函数:
__attribute__((constructor)) void kcFunc(){
printf("来了 : %s \n",__func__);
}
2 程序编译过程
静态库
在编译阶段,会将汇编生成的目标与引用的库一起链接打包到可执行文件当中,静态库直接拷贝一份复制到目标程序里
- 静态库的链接是在编译期完成的,编译完成后,目标程序
没有外部依赖
,直接就可以运行运行速度快
。 - 使
可执行文件变大
,每个引用位置都有一份静态库的拷贝。 - 一个
静态库更新
了,所有使用它的应用程序都需要重新编译、发布
。
动态库
链接时,目标程序只会存储指向动态库的引用
,程序运行时,用一份共享库的实例
加载进来。eg:UIKit,Foundation,CoreFoundation,libdispatch,libSystem,libobjc
- 相对于静态库
减少APP的大小
- 所有引用动态库的文件
共享一份动态库
,节约资源和空间 - 可以通过
更新动态库,达到更新程序
的目的 - 但动态载入会带来一部分
性能损失
,使用动态库也会使得程序依赖于外部环境
,如果环境缺少了动态库,或者库的版本不正确,就会导致程序无法运行
程序编译过程
源文件
:.h、.m、.mm、.cpp等文件
预处理
:宏替换,删除注释,展开头文件,生成.i文件
编译
:将.i文件转换为汇编语言,生成.s文件
汇编
:将汇编文件转换为目标文件,生成.o文件
链接
:对.o文件中引用其他库的地方进行链接,生成可执行文件
链接部分在ios中是由dyld
负责
3 动态链接器:dyld(the dynamic link editor)
APP的加载过程中,在app被编译打包成可执行文件格式的Mach-O文件后,交由dyld负责连接,加载程序:
3.1 dyld源码找到入口
全局搜索_dyld_start
:
-
在
dyldStartup.s
文件中找到了入口 -
看汇编注释,可知调用dyldbootstrap 命名空间下的start 方法
call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
3.2 dyldbootstrap::start
全局搜索dyldbootstrap
找到命名空间,再查找start方法
,关注重点返回值 调用了dyld的main函数
dyldbootstrap::start中,主要过程为:
- 使用全局变量之前,对dyld进行rebase操作,以修复为 real pointer 来运行;
- 设置参数和环境变量;
- 读取 app二进制文件 Mach-O 的header 得到偏移量 appSlide,然后调用dyld 命名空间下的_main 方法。
macho_header是Mach-O的头部
dyld加载的文件就是Mach-O类型文件
Mach-O类型是`可执行文件类型`,由四部分组成:`Mach-O头部`、`Load Command`、`section`、`Other Data`
3.3 分析 dyld::_main的思路
-
查看返回值result,在上面什么时候赋值的
-
找到
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN()
和result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD()
; -
搜索
sMainExecutable
,什么时候初始化的? -
找到
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath)
-
查看
instantiateFromLoadedImage
源码 ,通过调用ImageLoaderMachO::instantiateMainExecutable
方法创建一个ImageLoader实例对象 -
进入instantiateMainExecutable源码:为主程序创建映像,返回一个
ImageLoader类型的image对象
,其中sniffLoadCommands函数
获取Mach-O类型文件的Load Command的相关信息,并对其进行各种校验
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
// try mach-o loader
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
addImage(image);
return (ImageLoaderMachO*)image;
}
throw "main executable not a known format";
}
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
//dyld::log("ImageLoader=%ld, ImageLoaderMachO=%ld, ImageLoaderMachOClassic=%ld, ImageLoaderMachOCompressed=%ld\n",
// sizeof(ImageLoader), sizeof(ImageLoaderMachO), sizeof(ImageLoaderMachOClassic), sizeof(ImageLoaderMachOCompressed));
bool compressed;
unsigned int segCount;
unsigned int libCount;
const linkedit_data_command* codeSigCmd;
const encryption_info_command* encryptCmd;
sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
// instantiate concrete class based on content of load commands
if ( compressed )
return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
else
#if SUPPORT_CLASSIC_MACHO
return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
#else
throw "missing LC_DYLD_INFO load command";
#endif
}
3.3 dyld::_main主要功能
- 3.3.1 环境的配置:根据环境变量设置相应的值以及获取当前运行架构,为可执行文件的加载做准备工作; checkEnvironmentVariables(envp);
defaultUninitializedFalbackPaths(envp);
- 3.3.2 共享缓存:映射共享缓存到当前进程的逻辑内存空间;
checkSharedRegionDisable验证共享缓存的路径
mapSharedCache();
-
3.3.3 将dyld本身添加到UUID表 :addDyldImageToUUIDList();
-
3.3.4 初始化主程序:实例化主程序instantiateFromLoadedImage;
-
3.3.5 插入动态库: 遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载
-
3.3.6 链接主程序
-
3.3.7 链接插入的动态库
-
3.3.8 执行弱符号绑定(weakBind)
-
3.3.9 执行初始化方法;
-
3.3.10 查找程序入口 从
Load Command
读取LC_MAIN
入口,如果没有,就读取LC_UNIXTHREAD
。返回main( )--> notifyMonitoringDyldMain().
4 初始化主程序 initializeMainExecutable:为主程序实例化为ImageLoader对象
这一步主要是调用所有image的Initalizer
方法进行初始化。先为所有插入并链接完成的动态库执行初始化操作,再为主程序可执行文件执行初始化操作
具体流程为: runInitializers
--> processInitializers
--> recursiveInitialization
ImageLoader::recursiveInitialization
全局搜索notifySingle
:分析源码可知,通过 notifySingle 函数对sNotifyObjCInit
进行函数调用
获取镜像文件的真实地址 *sNotifyObjCInit)(image->getRealPath(), image->machHeader()
,而 sNotifyObjCInit
通过 registerObjCNotifiers
进行赋值
继而找到registerObjCNotifiers
的 调用函数 _dyld_objc_notify_register
_dyld_objc_notify_register
函数是供 objc runtime 使用的,objc4-781的源码libobjc.A.dylib 库里_objc_init
函数中找到其调用
搜索_dyld_objc_notify_register
,发现在_objc_init
源码中调用了该方法,并传入了参数,所以sNotifyObjCInit
的赋值
的就是objc
中的load_images
,而load_images
会调用所有的+load
方法,notifySingle是一个回调函数
load函数加载
下面我们进入load_images
的源码看看其实现,以此来证明load_images
中调用了所有的load
函数
- 通过objc源码中_objc_init源码实现,进入
load_images
的源码实现
- 进入
call_load_methods
源码实现,可以发现其核心是通过do-while
循环调用+load
方法
- 进入
call_class_loads
源码实现,了解到这里调用的load
方法证实我们前文提及的类的load
方法
所以,load_images
调用了所有的load
函数,以上的源码分析过程正好对应堆栈的打印信息
sMainExcuatable的initializer方法被调用前dyld会通知runtime进行类结构初始化,然后再通知调用load方法,这些目前还发生在main函数前,所有的依赖库的lnitializer都调用完后,dyld::main 函数会返回程序的main()函数地址,main函数被调用,从而代码来到了我们熟悉的程序入口
但由于OC的懒加载机制
,依赖库多数都是在使用时才进行bind,所以这些依赖库的类结构初始化都是发生在程序里第一次使用到该依赖库时才加载
。
那么 _objc_init 又是如何被调用的呢?-->doInitialization
回退到recursiveInitialization递归函数的源码实现,发现还有一个初始化镜像函数doInitialization
-
doInitialization源码
-
doInitialization源码有两个函数调用:
doImageInit
和doModInitFunctions
-
doImageInit源码:循环加载方法的调用
-
doModInitFunctions源码:加载了所有的Cxx文件
在 doModInitFunctions之后 会 先执行libSystem_initializer
,保证系统库优先初始化完毕,在这里初始化libdispatch_init
,进而在_os_object_init 中 调用 _objc_init
。
5 总结:_objc_init的调用链
底层库的调用流程为
_objc_init的调用链
为:_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> doInitialization -->libSystem_initializer(libSystem.B.dylib) --> _os_object_init(libdispatch.dylib) --> _objc_init(libobjc.A.dylib)
符号断点_objc_init
查看调用栈:
+load方法调用:
load_images
--> call_load_methods
-- call_class_loads[load_method]
Cxx函数调用:
doInitialization
--> doModInitFunctions
调用所有的CXX函数
在CXX函数中断点,bt打印堆栈
main 函数是固定的格式,被编译到内存里的,在dyld_start源码中可查看到走完dyldbootstrap::start后才调用main
所以,综上所述,最终dyld加载流程
,如下图所示,图中也诠释了前文中的问题:为什么是load-->Cxx-->main
的调用顺序