应用程序的加载分析
作为一个开发者,对于iOS应用程序启动过程有很多疑问,本篇就应用程序是如何加载的,做相关分析
首先,我们基本都知道load
函数,main
函数,那当程序里有C++代码
时,执行顺序又是怎样的,下面我们可以试试。
-
在
viewController
里添加load函数
-
在
main
函数里添加打印方法,并新增C++方法
-
运行,得出结果
由此,可以看出,程序执行顺序:load -> C++ -> main
,问题来了,main
作为程序入口,为啥不是一个执行,main
函数之前到底做了什么,我们继续向下探索~
在探索之前,我们先了解一下iOS代码的编译过程
和静态库
以及动态库
的概念
编译流程
-
.h
、.m
、.cpp
这些源文件
被预编译
-
预编译
:主要处理一些宏,然后进行编译
-
编译
:将预编译文件转换成汇编语言 -
汇编
:将汇编文件转换为机器语言,生成.o
文件 -
链接
:对.o
文件引用需要的库,最后生成可执行文件
静态库和动态库
- 静态库:
.a 、.lib
等后缀的库,在链接阶段,汇编生成的目标程序与引用的静态库一起链接打包到可执行文件当中。此时的静态库就不会在改变了,因为它是编译时被直接拷贝一份,复制到目标程序里,优点就是:不需要外部依赖,编译完成后,不会再变
,缺点就是:因为编译时复制了一份,总共有2份,导致程序体积变大,内存消耗大、性能消耗大
-
动态库
:.so 、.framwork
等后缀的库,不会复制一份,程序会指向动态库的引用,是在程序运行时,载入库。优点就是:减少打包后app的大小
;共享内存,节约资源,多个文件可以同时使用一个库
;实现动态更新
:可在运行阶段对库进行替换,更改,不需要重新编译。缺点就是依赖于外部环境。外部环境变更,或者缺少相关依赖的动态库,可能程序无法运行 - 动态库基本是来自系统的库,如:
UIKit 、 Foundation 、libdispatch、libobjc.dyld
等,存在沙路径里。这样在任何应用程序都可以使用同一个动态库。开发者建的库一般都是静态库。
以上编译过程,我们可以注意到最关键的地方是链接
过程,那么链接
过程是如何操作的呢
- 首先,需要一个链接器,这个链接器,就叫做
dyld
-
dyld
是什么,可以查到它的概念:它是苹果的动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作。 - 共享缓存机制:在iOS系统中,每个程序依赖的动态库都需要通过dyld(位于/usr/lib/dyld)一个一个加载到内存,然而,很多系统库几乎是每个程序都会用到的,如果在每个程序运行的时候都重复的去加载一次,势必造成运行缓慢,为了优化启动速度和提高程序性能,共享缓存机制就应运而生。所有默认的动态链接库被合并成一个大的缓存文件,放到/System/Library/Caches/com.apple.dyld/目录下,按不同的架构保存分别保存着
-
APP的启动流程
下面我们就分析dyld是如何加载的,下载dyld源码,地址:dyld源码
源码下载下来后,从何处开始分析呢。还是需要通过load
后的堆栈信息来分析执行顺序。
在load
处打断点,打印堆栈信息:
-
从打印信息可以看出,程序入口是从
_dyld_start
开始的,全局搜索_dyld_start
-
发现在
_dyld_start
无论在arm还是在x86下,都会在dyldbootstrap
函数中调用,全局搜索dyldbootstrap
-
在
dyldbootstrap
找到start
方法,最重要的代码是最后一行return代码,其核心是返回值的调用了dyld的main函数
,macho_header
是Mach-O
的头部,而dyld加载的文件就是Mach-O类型的,即Mach-O
类型是可执行文件
类型
-
点进dyld::_main的源码,在源码里发现主要做了以下几步事情
[1]配置环境变量
[2] 共享缓存
:检查是否开启了共享缓存,以及共享缓存是否映射到共享区域,例如UIKit、CoreFoundation等
[3] 主程序初始化
:调用instantiateFromLoadedImage函数实例化了一个ImageLoader对象
[4] 引入动态库
:遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载
[5] 链接主程序
:
[6]链接动态库
[7] 弱符号绑定
:
[8] 执行初始化方法
[9] 寻找主程序入口即main函数
这里,我们具体看一下第8
步,初始化的地方,
是怎么初始化整个程序的,点进initializeMainExecutable
-
我们可以看到这里主要做了一件事,就是循环遍历所有需要初始化的内容,全局搜索
runInitializers(cons
,源码如下,其再次初始化的代码是processInitializers
这个方法
-
再全局搜索
processInitializers
,得到源码
-
核心代码是这段循环遍历的部分
images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
目的是对我们所有的镜像调用recursiveInitialization
进行递归实例化,继续搜索recursiveInitialization
-
这里两个重点:
notifySingle
和hasInitializers = this->doInitialization
,其中notifySingle
上有一段注释:让objc知道我们要初始化这个映像
(let objc know we are about to initialize this image)
我们先看下notifySingle
的具体实现,
这里的重点:
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader())
,-
然后再全局搜索
sNotifyObjCInit
,发现没有找到实现,只有赋值。
-
接着再去搜索
registerObjCNotifiers
调用的地方,发现是在_dyld_objc_notify_register
里调用了
_dyld_objc_notify_register
这个函数,继续搜索,只看到它是下面这段代码,没有发现在哪调用,但是此时会调用load
方法
-
那我们就回到
objc源码
,查看是否有调用它的地方,果不其然,有调用,是在_objc_init
调用了
看到这里,我们上面分析过,sNotifyObjCInit的赋来自objc中的load_images。那么
_objc_init
是什么时候调用的呢,接下来我们回到上面说的第二个重点:this->doInitialization(context)-
进到
doInitialization
的源码实现
主要分为2步:
doImageInit
和doModInitFunctions
-
先看下
doImageInit
,
其核心主要是for循环加载方法的调用,这里需要注意的一点是,libSystem库的要求很高,需要最先初始化运行,这里看到注释libSystem initializer must run first
-
再来看看
doModInitFunctions
源码,这个方法中加载了所有Cxx
文件
说到这里,大致可以知道一个执行顺序:objc_init- > _dyld_objc_notify_register -> sNotifyObjCInit(objc_init的第二个参数loadImages) - >加载各种image等
-
具体我们可以用在程序中跑一下试试,在
load
方法打个断点来看看堆栈和整个初始化过程
-
虽然整个堆栈过程打印出来了,但是没有看到
_objc_init
的调用,再加个符号断点看一下
-
比上面多了
libSystem_initializer
之后的流程(libSystem_initializer ->libdispatch_init ->objc_init),那我们就主要来看一下libSystem_initializer
之后的流程。在·libsystem源码工程中查找
libSystem_initializer`,查看
下面来到
libdispatch_init
,到ibdispatch.dylib
开源库中中查找libdispatch_init
,源码如下
在这里调用了_os_object_init
,继续查找_os_object_init
,发现正是调用了_objc_init
- 前面我们分析过
_objc_init
里面又调用了_dyld_objc_notify_register进行注册,第二个参数是load_images
, 注册了后回到dyld
里面的notifySingle
, 然后会跳到sNotifyObjCInit = 参数2 调用sNotifyObjcInit()
,整个流程形成了一个闭环:runinit -> doinit ->lib dispatch -> _os_object_init -> libobjc.A.dylib -> _objc_init() ->Dyld中的_dyld_objc_notify_register(&map_images,load_images,unmapImages) ->回调函数
->notifySingle ->sNotifyObjcInit:
到此,整个大致流出来了,如图所示:
再回到我们最开始的问题:为啥main在最后执行
-
首先在程序加载的时候来到
objc_init
,调用_dyld_objc_notify_register
-
点进
load_images
,调用所有类的load方法
调用完load之后会来到doInitialization里面的doModInitFunctions,在doModInitFunctions会调用所有Cxx函数
-
可以用堆栈信息打印验证一下
也可以用断点跑汇编命令,查看执行顺序,在load
和Cxx
代码处打断点,往下stepover
,可以一步一步走到如下图所示的main函数里
到此也验证了执行顺序load->Cxx->main
这边我们突发奇想一下,为啥是叫main函数,那么我们尝试改一下main函数
名字,改成wlmain
会报错
所以,main是在底层dyld中写定的,到dyld里查找main的实现
由此,如果修改了main函数的名称,会报错
综上,整个程序dyld加载流程如图所示