前言
创建project,ViewController重写load方法,main函数添加一个c++函数,查看调用顺序
@implementation ViewController
+ (void)load {
NSLog(@"%s", __func__);
}
//。。。
@end
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
NSLog(@"123123123");
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
__attribute__((constructor)) void slFunc() {
printf("来了 : %s \n", __func__);
}
2023-02-13 10:01:06.913135+0800 嗷嗷[2677:54883] +[ViewController load] 来了 : slFunc
2023-02-13 10:01:06.913956+0800 嗷嗷[2677:54883] 123123123
所以运行顺序是load -> C++方法 -> main,所以main函数并不都是最先执行的。
iOS编译过程和库
app启动前涉及编译过程和动态库、静态库。
编译过程
静态库
编译时被直接拷贝一份,复制到目标程序里的
- 优点:编译完成,就没有作用了,目标程序无外部依赖,可直接运行。
- 缺点:静态库会有两份,会使目标程序体积增加,影响内存、性能和速度。
动态库
目标程序只会存储指向动态库的引用,运行时才被载入。
- 优点:1.减少打包之后app的大小;2.共享内存,同一份库可以被多个程序使用;3.更新动态库可以实现更新程序,运行时才载入,可随时替换库,不用重新编译代码。
- 缺点:会有一部分性能损失,依赖外部环境,若环境缺了动态库,或版本不对,程序将不能运行。
图示如下,(图转载自Style_月月 ,感谢🙏)
dyld加载流程分析
dyId
dyld:苹果的动态链接器(
the dynamic link editor),app被编译打包成可执行文件Mach-O后,交由dyld负责链接,加载程序。
app启动流程如下图,(图转载自Style_月月 ,感谢🙏)
app启动的起始点
于load方法处加个断电,bt输出堆栈信息
所以App启动是从dyld
中的dyld_start
开始的。
dyld::_main函数源码分析
1.在dyld-852源码中,找到arm64相关汇编,接着会调用一个C++方法dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
2.搜索dyldbootstrap,找到 start方法,发现核心是返回值调用了dyld::_main。
namespace dyldbootstrap {
。。。
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{
// Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);
// if kernel had to slide dyld, we need to fix up load sensitive locations
// we have to do this before using any global variables
rebaseDyld(dyldsMachHeader);
// kernel sets up env pointer to be just past end of agv array
const char** envp = &argv[argc+1];
// kernel sets up apple pointer to be just past end of envp array
const char** apple = envp;
while(*apple != NULL) { ++apple; }
++apple;
// set up random value for stack canary
__guard_setup(apple);
#if DYLD_INITIALIZER_SUPPORT
// run all C++ initializers inside dyld
runDyldInitializers(argc, argv, envp, apple);
#endif
_subsystem_init(apple);
// 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);
}
。。。
dyld::_main实现
- 根据环境变量设置相应的值以及获取当前运行架构
- 检查是否开启了共享缓存,以及共享缓存是否映射到共享区域,例如
CoreFoundation、UIKit
等。 - 主程序初始化:调用
instantiateFromLoadedImage
函数实例化了一个ImageLoader
对象 - 插入动态库:遍历环境变量
DYLD_INSERT_LIBRARIES
,调用loadInsertedDylib
加载 - 链接主程序
- 链接动态库
- 弱符号绑定:sMainExecutable->weakBind(gLinkContext);
- 执行初始化方法:initializeMainExecutable
- 寻找入口main函数,从
Load Command
读取LC_MAIN
入口
执行初始化方法:initializeMainExecutable
查看initializeMainExecutable源码,核心主要是循环执行runInitializers
runInitializers的主要方法是processInitializers
进入processInitializers
函数的源码实现,其中对镜像列表进行递归实例化 ,实例化函数为recursiveInitialization
recursiveInitialization主要分成两部分(
notifySingle、doInitialization)
notifySingle
notifySingle的重点主要是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
搜索 sNotifyObjCInit,发现在registerObjCNotifiers函数中有赋值操作
继续搜索 registerObjCNotifiers,发现了_dyld_objc_notify_register,这个函数在之前探究OC初始化的时候见过
去objc源码探究_dyld_objc_notify_register,果然是在_objc_init中调用了,接下来吧重心放在load_images
load_images
进入load_images源码
进入call_load_methods,这里在循环调用call_class_loads
进入call_class_loads,可以得出结论:load_images
中调用了所有的load
函数。
load的源码链为:_dyld_start
--> dyldbootstrap::start
--> dyld::_main
--> dyld::initializeMainExecutable
--> ImageLoader::runInitializers
--> ImageLoader::processInitializers
--> ImageLoader::recursiveInitialization
--> dyld::notifySingle
(是一个回调处理) --> sNotifyObjCInit
--> load_images(libobjc.A.dylib)
doInitialization 函数
doInitialization分成(doImageInit、doModInitFunctions)
- doImageInit主要是循环加载方法的调用,必须先运行libSystem初始化
- doModInitFunctions主要用于加载所有Cxx文件
寻找主入口函数
汇编调试,先来到了load方法
continue,来到c++函数slFunc
最后来到main函数
总结
dyld加载流程如下,同时说明了为什么调用顺序是load-->Cxx-->main。