OC 底层探索 12、应用程序加载
方法等是如何加载到内存中的呢,或者说类的加载都做了什么?在此之前,我们先探索 APP 从响应用户点击到完全启动的过程 即 应用程序加载 都做了什么事情。
首先我们准备一个 iOS 的 APP 工程,ViewController.m 中添加 load 方法,main.m中添加一个 C++ 的 方法,代码如下图:
执行流程: load --> C++ --> main() .
为何 load 和 C++ 方法在函数之前呢?
在此之前,我们要先大概回顾下编译流程。
一、编译过程
/* 书籍推荐:《程序员的自我修养》*/
.h .m 等源文件的预编译 --> 编译 --> 汇编 --> 链接 --> 可执行文件
1、动静态库
静态库:.a .lib 等文件 - 编译器链接
在链接阶段,将 汇编生成的一些目标文件,与引进的一些库链接在一起,打包成可执行文件,也称为静态链接,链接的是静态库。
但静态库会同时生成多份,对内存和性能消耗比较大。
动态库:.framework .so 等文件 - 运行期链接
动态库,在编译时并不会编译到目标代码中,而是在程序被载入的时候,把相同的库用一份共享库的实例将其将在进去。
同样的库只存在一份大大节省了内存(增量更新),共享内存节约了资源,通过更新动态库达到更新程序的效果;同时减小了整个APP打包后的大小。例如:UIKint UIFoundation CoreFoundation libobjc 等。
这些动态库如何加载呢?通过 dyld 链接器进行链接。
2、dyld 动态链接器
app 启动 --> 下层的很多动静态库:镜像文件(images) --> 由 dyld 进行处理:从加载的内存中读出来,读到相应的表中;然后加载主程序 --> 之后进行相应的 link --> 库的初始化(例 runtime 的 _objc_init) --> 这些必然要通过 dyld 来处理的,下面进行 dyld 的探究。
二、源码分析
1、主流程
dyld-750.6 源码下载,源码文件很多,我们如何入手呢?
如上图,我们已知 load 方法首先执行,可打印下堆栈信息,可知,在此之前最初走了 _dyld_start .
1、全局搜索 dyldbootstrap 方法
命名空间包含整个代码文件,我们找到start,从上面打印的堆栈信息中也可得知:dyldbootstrap::start() 之后走 dyld::_main()。
2、dyld::_main():
代码有点长 6192~6828, _main 函数返回值 是return result,搜索 result,找到下面几处赋值:
我们直接从 6780 行开始读代码,可知 result = (uintptr_t)sMainExecutable->getEntryFromLC_xxx. 可执行主程序。(第6796行是个特殊特性的判断,我们暂不管它)
2.1)文件内搜索 sMainExecutable:
由上源码,可知 instantiateFromLoadedImage 方法主要做的是镜像文件加载,继续进入方法:instantiateMainExecutable:可以看到是一些 command 处理。我们打开工具 machOView,将可执行文件拖入,如下图:
简单来讲,即:instantiateFromLoadedImage 过程主要是做了:初始化创建了一个 imageLoader,加在了 image 里面,然后返回了一个主程序 sMainExecutable.
dyld 做了什么(源码这里不再贴过来):
- 环境变量配置 -
- 共享缓存 -
- 主程序的初始化 -
- 插入动态库 - .
- link 主程序 -
- link 动态库
- dyld 的 main() 函数
version、platform、path 上下文context 等 --> mapSharedCache() --> 6577行 sMainExecutable --> 6641行 for循环的- loadInsertedDylib() --> 6659行 link(): sMainExecutable/images --> run all initializers: initializeMainExecutable() --> dyld 的 main: notifyMonitoringDyldMain() .
2、initializeMainExecutable() 初始化
initializeMainExecutable() 源码:
1 void initializeMainExecutable() 2 { 3 // record that we've reached this step 4 gLinkContext.startedInitializingMainExecutable = true; 5 6 // run initialzers for any inserted dylibs 7 ImageLoader::InitializerTimingList initializerTimes[allImagesCount()]; 8 initializerTimes[0].count = 0; 9 const size_t rootCount = sImageRoots.size(); 10 if ( rootCount > 1 ) { 11 for(size_t i=1; i < rootCount; ++i) { 12 sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]); 13 } 14 } 15 16 // run initializers for main executable and everything it brings up 17 sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]); 18 19 // register cxa_atexit() handler to run static terminators in all loaded images when this process exits 20 if ( gLibSystemHelpers != NULL ) 21 (*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL); 22 23 // dump info if requested 24 if ( sEnv.DYLD_PRINT_STATISTICS ) 25 ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]); 26 if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS ) 27 ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]); 28 }
1、递归进行 runInitializers() .
2、跳到 processInitializers():
3、搜索 recursiveInitialization():
3.1)搜索 notifySingle():
查找 sNotifyObjCInit:
sNotifyObjCInit 是通过函数 registerObjCNotifiers() 传过来的,
_dyld_objc_notify_register 在 dyld 源码中找不到,我们去 objc 源码中查找试试:
找到了 _objc_init 里面去了,那我们要探索的 initial 在哪呢?
通过 3.1 我们知道 notifySingle 回调,下面继续探索初始化流程。
3.2)回到 recursiveInitialization() 代码
1598行:
doImageInit():
通过 dyld 源码流程分析,可对应下面堆栈信息的执行流程:
下载 libsystem 源码 - libsyscall_initializer() 初始化部分源码:
__attribute__((constructor)) static void libSystem_initializer(int argc, const char* argv[], const char* envp[], const char* apple[], const struct ProgramVars* vars) { ...... // 更多代码这里不全部展示了 // No ASan interceptors are invoked before this point. ASan is normally initialized via the malloc interceptor: // _dyld_initializer() -> tlv_load_notification -> wrap_malloc -> ASanInitInternal _dyld_initializer(); _libSystem_ktrace_init_func(DYLD); libdispatch_init(); _libSystem_ktrace_init_func(LIBDISPATCH); ...... // 更多代码这里不全部展示了 }
从上源码可以看到 _dyld_initializer() --> libdispatch_init() .
下载 libdispatch 源码 - libdispatch_init() 部分源码:
void libdispatch_init(void) { ...... // 更多代码这里不做展示 _dispatch_hw_config_init(); _dispatch_time_init(); _dispatch_vtable_init(); _os_object_init(); _voucher_init(); _dispatch_introspection_init(); }
_os_onjc_init(): --> _objc_init()
由此,整个执行流程实现了一个闭环(下图堆栈信息来自 objc 源码工程):
总结:应用程序整个加载流程:
dyld_start --> ...... --> libsystem: libsyscall_initializer() --> _dyld_initializer() --> libdispatch_init() --> libDispatch: _os_onjc_init() --> libobjc.A.dylib: _objc_init() --> _dyld_objc_notify_register(参数1, 参数2) 回调函数
notifySingle() --> sNotifyObjCInit() --> registerObjCNotifiers() : 参数2
3、下面继续探索
我们通过文章顶部已知,方法的执行顺序是 load --> C++ --> main 下面对其原因进行简单探究。
1)load 方法调用
_objc_init() 源码 中 load_images:
跳转 call_load_methods():
调 call_class_loads():
2)Cxx 方法调用
doInitialization 源码中 doModInitFunctions(): 全部 Cxx 函数调用
通过堆栈信息验证:
3)main 函数
如下,dyldbootstrap::start() 执行完毕后,跳转到 寄存器 rax 的位置,rax 即 main 函数:
tip: main 函数作为入口函数,它是一个写定的函数,它的名字是固定的main。
文章上面 dyld_start 源码中可知:
汇编源码中:call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue) --> LC_MAIN case, set up stack for call to main()
以上。