main函数之前底层做了那些准备?
准备
创建一个工程,在ViewController
中重写了load方法
,在main
中加了一个C++方法
,即kcFUnc
如图
打印结果如下:
结果:在
main函数
中打印之前首先打印了viewController 中load方法
其次打印了c++函数
,那么为什么会出现这种情况?为了探索这个问题,在C++函数执行打印下断点
,结果如图结论:
在main函数执行之前需要dyld参与
那么什么是dyld
?
dyld(the dynamic link editor)
是苹果的动态链接器
,是苹果操作系统的重要组成部分,在app被编译打包
成可执行文件格式的Mach-O文件
后,交由dyld负责连接
,加载程序。
app编译成可执行文件Mach-O格式流程 编译器原理
静态库 和 动态库
-
静态库:
在链接阶段,会将可汇编生成的目标程序与引用的库一起链接打包到可执行文件当中。此时的静态库就不会在改变了,因为它是编译时被直接拷贝一份,复制到目标程序里的好处
:编译完成后,库文件实际上就没有作用了,目标程序没有外部依赖,直接就可以运行缺点:
由于静态库会有两份,所以会导致目标程序的体积增大,对内存、性能、速度消耗很大
-
动态库:
程序编译时并不会链接到目标程序中,目标程序只会存储指向动态库的引用,在程序运行时才被载入-
优势
:减少打包之后app的大小:因为不需要拷贝至目标程序中,所以不会影响目标程序的体积,与静态库相比,减少了app的体积大小
共享内存,节约资源
:同一份库可以被多个程序使用
通过更新动态库,达到更新程序的目的
:由于运行时才载入的特性,可以随时对库进行替换,而不需要重新编译代码 -
缺点
:动态载入会带来一部分性能损失,使用动态库也会使得程序依赖于外部环境,如果环境缺少了动态库,或者库的版本不正确,就会导致程序无法运行
-
1.从打印的堆栈信息来看程序是从dyld
中的_dyld_start
开始的所以下载dyld源码在dyld中全局搜索_dyld_start
2.搜索
dyldbootstrap
找到uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[], const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
函数,其核心是返回值的调用了dyld的main函数
,其中macho_header是Mach-O的头部
,而dyld加载的文件
就是Mach-O类型
的,即Mach-O类型是可执行文件类型
,由四部分组成:Mach-O头部、Load Command、section、Other Data
,可以通过MachOView查看可执行文件信息Mach-O类型是可执行文件类型
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{
....省略部分代码
// now that we are done bootstrapping dyld, call dyld's main
uintptr_t appsSlide = appsMachHeader->getSlide();
return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
3.第二步返回main方法
上方注释"现在我们完成了引导程序,请调用dyld的main
进入dyld::_main()"
函数,由于此函数信息较长,可调重要参数进行分析:
-
第一步:
环境变量配置
,准备可执行文件cdHash - 检查环境
#if __MAC_OS_X_VERSION_MIN_REQUIRED
if ( !gLinkContext.allowEnvVarsPrint && !gLinkContext.allowEnvVarsPath && !gLinkContext.allowEnvVarsSharedCache ) {
pruneEnvironmentVariables(envp, &apple);
// set again because envp and apple may have changed or moved
setContext(mainExecutableMH, argc, argv, envp, apple);
}
else
#endif
{
//检查环境
checkEnvironmentVariables(envp);
//将其设置默认值
defaultUninitializedFallbackPaths(envp);
}
#if __MAC_OS_X_VERSION_MIN_REQUIRED
- 获取当前运行环境的架构信息
getHostInfo(mainExecutableMH, mainExecutableSlide);
-
第二步:
共享缓存
,检查是否开启了共享缓存,以及共享缓存是否映射到共享区域,例如UIKit、CoreFoundation
等
-
第三步:
主程序的初始化
,调用instantiateFromLoadedImage函数
实例化了一个ImageLoader对象
-
第四步:
插入动态库
,遍历调用loadInsertedDylib函数
-
第五步:
链接主程序
-
第六步:
链接动态库
-
第七步:
弱符号绑定
image->recursiveBind(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
-
第八步:
初始化程序
initializeMainExecutable();
-
第九步:
寻找主程序入口即main函数
,从Load Command
读取LC_MAIN入口
,如果没有,就读取LC_UNIXTHREAD
,这样就来到了日常开发中熟悉的main函数
了
主要分析第三步和第八步
第三步:主程序初始化
-sMainExecutable
表示主程序变量
,查看其赋值,是通过instantiateFromLoadedImage静态函数
初始化
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
- 进入
instantiateFromLoadedImage
源码,其中创建一个ImageLoader实例对象
,通过instantiateMainExecutable
方法创建 - 进入
instantiateMainExecutable
源码其作用是为主可执行文件创建映像,返回一个ImageLoader类型的image对象,即主程序。
其中sniffLoadCommands函数
时获取Mach-O类型文件的Load Command
的相关信息,并对其进行各种校验
第八步:执行初始化方法
- 进入
initializeMainExecutable源码
,主要是循环遍历,都会执行runInitializers
方法 - 进入
runInitializers函数
,找到如下源码,其核心代码是processInitializers函数
的调用 - 进入
processInitializers函数
的源码实现,其中对镜像列表调用recursiveInitialization函数
进行递归实例化
- 全局搜索
recursiveInitialization(const
这里需要探索两个函数一部分是notifySingle函数
,一部分是doInitialization函数
notifySingle函数
- 全局搜索
notifySingle函数
,其重点是
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
- 全局搜索
sNotifyObjCInit
并没有发现函数的实现,但是在registerObjCNotifiers函数
中对其进行了赋值
- 全局搜索
registerObjCNotifiers函数
的调用,间接探究对sNotifyObjCInit
的赋值
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
dyld::registerObjCNotifiers(mapped, init, unmapped);
}
在dyld中并没有搜索到函数_dyld_objc_notify_register函数
的调用,此函数需要在libobjc源码中搜索.
- 在objc源码中搜索
_dyld_objc_notify_register
,发现在_objc_init
源码中调用了该方法,并传入了参数,所以sNotifyObjCInit
的赋值的就是objc中的load_images
,而load_images
会调用所有的+load方法
。所以综上所述,notifySingle是一个回调函数
load_images函数实现
- 进入函数体
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
loadAllCategories();
}
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
- 进入
call_load_methods 函数
内部,可以发现其核心是通过do-while循环调用+load方法(上方注释可以品一品)
void call_load_methods(void)
{
...
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
- 进入
call_class_loads 函数
(上方注释可以品一品)
所以,load_images
调用了所有的load函数
,以上的源码分析过程正好对应堆栈的打印信息
doInitialization 函数
-
dyld
中接着recursiveInitialization
源码对doInitialization 函数
进行分析
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
CRSetCrashLogMessage2(this->getPath());
// mach-o has -init and static initializers
doImageInit(context);
doModInitFunctions(context);
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}
此处又分了两个部分doImageInit(context)函数
和doModInitFunctions(context)函数
第一部分doImageInit
- 进入
doImageInit函数
其核心主要是for循环加载方法
的调用,这里需要注意的一点是,libSystem的初始化必须先运行( libSystem initializer must run first)
第二部分doModInitFunctions
- 进入
doModInitFunctions源码
实现,这个方法中加载了所有Cxx文件
,在C++方法加断点,bt打印堆栈进行验证
但是_objc_init函数
什么时间调用还是未曾可知,load_images函数
还是未被注册,也即是程序运行+load
无从说起
_objc_init
- 在可执行objc源码工程的
objc_init
下断点打印堆栈信息
流程:doInitialization -->libSystem_initializer(libSystem.B.dylib) --> _os_object_init(libdispatch.dylib) --> _objc_init(libobjc.A.dylib)
故最终dyld加载流程,如下图所示,图中也诠释了前文中的问题:为什么是load-->Cxx-->main
的调用顺序