image.png
一个iOS App
的 main
函数位于main.m
中,这是我们熟知的程序入口。但对objc
了解更多之后发现,程序在进入我们的main函数前已经执行了很多代码,比如熟知的+load
方法等。
简单总结
-
系统先读取
App
的可执行文件(Mach-O
文件),从里面获得dyld
的路径,然后加载dyld
,dyld
去初始化运行环境。 -
开启缓存策略,加载程序相关依赖库(其中也包含我们的可执行文件),并对这些库进行链接,最后调用每个依赖库的初始化方法,在这一步,
runtime
被初始化。 -
当所有依赖库的初始化后,轮到最后一位(程序可执行文件)进行初始化,在这时
runtime
会对项目中所有类进行类机构初始化,然后调用所有的load
方法。最后dyld
返回main
函数地址,main
函数被调用,我们便来到程序入口main函数。
一. 从dyld开始
Mach-O文件
Mach-O
文件格式是OS X
与iOS
系统上的可执行文件格式,像我们编译过程产生的.O
文件,以及程序的可执行文件,动态库等都是Mach-O
文件,它的结构如下:
-
Header
: 保存了一些基本信息,包括了该文件运行的平台、文件类型、LoadCommands
的个数等。
-LoadCommands
: 可以理解为加载命令,在加载Mach-O
文件时会使用这里的数据来确定内存的分布以及相关的加载命令。比如我们的main
函数的加载地址,程序所需的dyld
的文件路径,以及相关依赖库的文件路径。
-Data
:这里包含了具体的代码、数据等。
我们可以通过Mach-O
文件查看器MachOView
查看一个项目编译后的可执行文件内容:
可以看出:
-
dyld
的路径在LC_LOAD_DYLINKER
命令里,一般都是在/usr/lib/dyld
路径下。 -
LC_MAIN
指的是程序main
函数加载地址 -
LC_LOAD_DYLIB
指向的都是程序依赖库加载信息。 - 如果我们程序使用到
AFNetworking
,这里就会多出一条名LC_LOAD_DYLIB(AFNetworking)
的命令。如下图:
可以看出我们比较常用的三方库: AFNetworking
,IQKeyboard
等。
系统加载程序可执行文件后,通过分析文件来获得dyld
所在路径来加载dyld
,然后就将后面的事情交给dyld
.
动态链接库
iOS
中用到的所有系统framework
都是动态链接的,类比成插头和插排,静态链接的代码在编译后的静态链接过程就将插头和插排一个个插好,运行时直接执行二进制文件;而动态链接需要在程序启动时有需要再去完成插好相关的插头和插排,所以在我们写的代码执行前,动态连接器需要完成准备工作。
这个是在Xcode
中看到的Link
列表:
这些framework
将会在动态连接过程中被加载,另外还有隐含link的framework,可以测试出来:先找到可执行文件,我这里叫TestMain
的工程,模拟器路径下找到TestMain.app
,可执行文件默认同名,在通过otool
命令:
$ otool -L TestMain
-L
参数打印出所有link
的framework
(去掉了版本信息如下)
TestMain:
/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics
/System/Library/Frameworks/UIKit.framework/UIKit
/System/Library/Frameworks/Foundation.framework/Foundation
/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
/usr/lib/libobjc.A.dylib
/usr/lib/libSystem.dylib
出了多了CoreFoundation
(被UIKit依赖)外,有两个默认添加的lib: libobjc
即objc
和runtime
, libSystem
中包含了很多系统级别的lib
,列几个熟知的。
- libdispatch(GCD)
- libsystem_c(C语言库)
- libsystem_blocks(Block)
- libCommonCrypto(加密库,比如常用的md5)
这些lib
都是dylib
格式相当于windows
中的dll
,系统使用动态链接好处:
-
代码共用: 很多程序都动态链接了这些
lib
,但是它们在内存和磁盘中只有一份 -
易于维护:由于被依赖的
lib
是程序执行时才link
的,所以这些lib
很容易做更新,比如libSystem.dylib
是libSystem.B.dylib
的替身,哪天想升级直接换成libSystem.C.dylib
然后再替换替身就可以 -
减少可执行文件体积,相比静态链接,动态链接在编译时不需要打包进去,所以可执行文件的体积要小很多。
dyld
dyld(the dynamic link editor)
, Apple
的动态链接器,系统kernel
做好启动程序的初始准备后,交给dyld
负责,dyld
作用顺序的概括:
1. 从kernel留下的原始调用栈引导和启动自己
2. 将程序依赖的动态链接库递归加载进内存,当然这里有缓存机制
3.non-lazy符号立即link到可执行文件,lazy的存表里
4.Runs static initializers for the executable
5. 找到可执行文件的main函数,准备参数并调用
6. 程序执行中负责绑定lazy符号、提供runtime dynamic loading services、提供调试器接口。
7. 程序main函数return后执行static terminator
8. 某些场景下main函数结束后调libSystem的_exit函数。
由于dyld
是开源的,我们可以看到dyldStartup.s
这个文件,其中用汇编实现名为_dyld_start
的方法,汇编太生涩,它主要做了这件事:
1. 调用dyldbootstrap::start()方法(省去参数)
2.上一个方法返回了main函数地址,填入参数并调用main函数。
这个步骤可以通过设置一个符号断点断在_objc_init
:
这个函数是runtime
的初始化函数。程序运行在很早的时候断住,这时候看调用栈:
看到栈底的dyldbootstrap::start()
方法,继而调用了dyld::_main()
方法,其中完成了刚从说的递归加载动态库过程,由于libSystem
默认引入,栈中出现了libSystem_initializer
的初始化方法。
我们可以看下_main
函数:
这里的_main
函数是dyld
的函数,并非我们程序里的main
函数。
1. sMainExecutable = instantiateFromLoadedImage(....)与loadInsertedDylib(...)
这一步 dyld
将我们可执行文件以及插入的lib
加载进内存,生成对应的image
.
sMainExecutable
对应着我们的可执行文件,里面包含了我们项目中所有新建的类。
insertDylib
一些插入的库,他们配置在全局的环境变量sEnv
中,我们可以在项目中设置环境变量DYLD_PRINT_ENV
为1
,来打印该sEnv
的值。
运行log
如下:
可以看出插入的库为:libBacktraceRecording.dylib
和libViewDebuggerSupport
.
有时我们会在三方App
的Mach-O
文件中通过修改DYLD_INSERT_LIBRARIES
的值来加入我们自己的动态库,从而注入代码,hook
别人的App
.
2. link(sMainExecutable,...)
和 link(image, ...)
对上面生成的image
进行链接。其主要有对image
进行load(加载)
、rebase(基地址复位)
,bind(外部符号绑定)
,我们可以查看源码:
-
recursiveLoadLibraries(context, prefightOnly,loaderRPaths)
递归加载所有依赖库进内存
-recursiveRebase(context)
递归对自己以及依赖库进行复基位操作。在以前,程序每次加载其在内存中的堆栈地址都是一样的,这意味着你的方法,变量等地址每次都一样的,这使得程序很不安全,后面就出现ASLR(Address space layout randomization
,地址空间配置随机加载),程序每次启动后地址都会随机变化,这样程序里所有的代码地址都是错,需要重新对代码地址进行计算修复才能正常访问。
-
recursiveBind(context, forceLazyBound,neverUnload)
对库中所有nolazy
的符号进行bind
,一般情况下多数符号都是lazybind
的,他们在第一次使用的时候才进行bind
.
3.initializeMainExecutable()
这一步主要是调用所有image
的initalizer
方法进行初始化。这里的initalizers
方法并非名为Initalizers
的方法,而是C++静态对象初始化构造器,atribute(constructor)
进行修饰的方法,在LmageLoader
类中initializer
函数指针锁指向该初始化方法的地址。
我们可以在程序中设置环境变量DYLD_PRINT_INITALIZERS
为1
来打印出程序的各种依赖库的initializer方法。
运行程序,系统log
打印如下:
可以看到每个依赖库对应着一个初始化方法,名称各有不同。
这里最开始调用的libSystem.dylib
的initializer function
比较特殊,因为runtime
初始化就在这一阶段,而这个方法其实和简单,我们可以在这里看到init.c
源码,主要方法如下:
其中libdispatch_init
里调用了到runtime
初始化方法_objc_init.
我们可以在程序中打个符号断点来验证。
运行程序,然后断点命中,我们来看下调用栈:
objc_init调用栈.png
我们可以看到_objc_init
调用顺序,先libSystem_initializer
调用libdispatch_init
,再到_objc_init
初始化runtime
.
runtime
初始化后不会闲着,在_objc_init
中注册了几个同志,从dyld
这里接手几个活,其中包括初始化相应依赖库里的类结构,调用依赖库里所有load
方法。
就拿sMainExcuateable
来说,它的initializer
方法是最后调用的,当initializer
方法被调用前dyld
会通知runtime
进行类结构初始化,然后再通知调用+load
方法,这些目前都发生在main
函数前,但是由于lazy bind
机制,依赖库多数都是在使用时才进行bind
,所以这些依赖库的类结构初始化都是发生在程序里第一次使用到该依赖库时才进行。
ImageLoader
当然这个image
不是图片的意思,它大概表示一个二进制文件(可执行文件或so文件),里面是被编译过的符号、代码等,所以imageLoader
作用是将这些文件加载进内存,且每一个文件对应一个imageLoader
实例来负责加载。
两步走:
1.在程序运行时它先将动态链接的image递归加载(也就是上面ImageLoader的递归调用)
2.再从可执行文件image递归加载所有符号
当然所有这些都发生在我们真正的main函数执行之前。
runtime 与 +load
刚才讲到libSystem
是若干个系统lib
的集合,所以它只是一个容器lib
而已,而且它也是开源的,里面实质上就是一个文件: init.c 由libSystem_initializer逐步调用到了_objc_init
,这里就是objc
和runtime
的初始化入口。
除了runtime
环境的初始化外,_objc_init
中绑定了新image
被加载后的callback
:
dyld_register_image_state_change_handler(
dyld_image_state_bound, 1, &map_images);
dyld_register_image_state_change_handler(
dyld_image_state_dependents_initialized, 0, &load_images);
可见dyld
担当了runtime
和imageLoader
中间的协调者,当新image
加载进来后交由runtime
去解析这个二进制文件的符号表和代码。继续上面的断点法,断住神秘的+load
函数。
清楚的看到整个调用栈和顺序:
1. dyld开始将程序二进制文件初始化
2. 交由imageLoader读取image,其中包含了我们的类,方法等各种符号
3.由于runtime向dyld绑定了回调,当image加载到内存后,dyld会通知runtime进行处理
4. runtime接手后调用map_images做解析和处理,接下来load_images中调用call_load_methods方法,遍历所有加载进来的Class,按继承层级依次调用Class的+load方法和Category的+load方法。
至此,可执行文件中和动态库所有的符号(Class, Protocol,Selector,IMP,...
)都已经按格式成功加载到内存中,被runtime
所管理,再这之后,runtime
的那些方法(动态添加Class,swizzie
等等才能生效)
关于+load方法的几个QA
Q:重载自己Class
的+load
方法需不需要调父类
A:runtime
负责按继承顺序递归调用,所以我们不能调用super
Q: 在自己Class
的+load
方法时能不能替换系统framework
(比如UIKit
)中某个类的方法实现
A:可以,因为在动态链接过程中,所有依赖库的类是优先于自己的类加载的
Q:重载+load
时需要手动添加@autoreleasepool
吗?
A:不需要,在runtime
调用+load
方法前后是加了objc_autoreleasePoolPush()
和objc_autoreleasePoolPop()
的。
Q:想让一个类的+load
方法被调用是否需要在某个地方import
这个文件
A:不需要,只要这个类的符号被编译到最后的可执行文件中,+load
方法就会被调用.
总结
-
整个事件由
dyld
主导,完成运行环境的初始化后,配合ImageLoader
将二进制文件按格式加载到内存 -
动态链接依赖库,并由
runtime
负责加载成objc
定义的结构,所有初始化工作结束后,dyld
调用真正的main
函数。 -
值得说明的是,这个过程远比写出来复杂,这里只提到了
runtime
这个分支,还有像GCD、XPC
、等重头的系统库初始化分支没有提及(当然这里还有缓存机制) -
总结:在main函数执行之前,系统做了茫茫多的加载和初始化工作,但是被很好隐藏了。
孤独的main函数
当所有前期初始化工作结束是,dyld会清理现场,将调用栈回归,只剩下:
image.png孤独的main
函数,看上去像是程序的开始!
网友评论