main 函数之前发生了什么

一个 iOS App 的main函数位于 main.m 中,这是我们熟知的程序入口。但对 objc 了解更多之后发现,程序在进入我们的 main 函数前已经执行了很多代码,比如熟知的+ load方法等。本文将跟随程序执行顺序,刨根问底,从dyld到runtime,看看 main 函数之前都发生了什么。

从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

除了多了的CoreGraphics(被 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 作用顺序的概括:

从 kernel 留下的原始调用栈引导和启动自己

将程序依赖的动态链接库递归加载进内存,当然这里有缓存机制

non-lazy 符号立即 link 到可执行文件,lazy 的存表里

Runs static initializers for the executable

找到可执行文件的 main 函数,准备参数并调用

程序执行中负责绑定 lazy 符号、提供 runtime dynamic loading services、提供调试器接口

程序main函数 return 后执行 static terminator

某些场景下 main 函数结束后调 libSystem 的_exit函数

得益于 dyld 是开源的,github 地址,我们可以从源码一探究竟。

一切源于dyldStartup.s这个文件,其中用汇编实现了名为__dyld_start的方法,汇编太生涩,它主要干了两件事:

调用dyldbootstrap::start()方法(省去参数)

上个方法返回了 main 函数地址,填入参数并调用 main 函数

这个步骤随手就能验证出来,设置一个符号断点断在_objc_init:

这个函数是runtime的初始化函数,后面会提到。程序运行在很早的时候断住,这时候看调用栈:

看到了栈底的dyldbootstrap::start()方法,继而调用了dyld::_main()方法,其中完成了刚才说的递归加载动态库过程,由于libSystem默认引入,栈中出现了libSystem_initializer的初始化方法。

ImageLoader

当然这个image不是图片的意思,它大概表示一个二进制文件(可执行文件或 so 文件),里面是被编译过的符号、代码等,所以 ImageLoader 作用是将这些文件加载进内存,且每一个文件对应一个ImageLoader实例来负责加载

两步走:

在程序运行时它先将动态链接的 image 递归加载 (也就是上面测试栈中一串的递归调用的时刻)

再从可执行文件 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函数:

清楚的看到整个调用栈和顺序:

dyld 开始将程序二进制文件初始化

交由 ImageLoader 读取 image,其中包含了我们的类、方法等各种符号

由于 runtime 向 dyld 绑定了回调,当 image 加载到内存后,dyld 会通知 runtime 进行处理

runtime 接手后调用 map_images 做解析和处理,接下来 load_images 中调用 call_load_methods 方法,遍历所有加载进来的 Class,按继承层级依次调用 Class 的 +load 方法和其 Category 的 +load 方法

至此,可执行文件中和动态库所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被 runtime 所管理,再这之后,runtime 的那些方法(动态添加 Class、swizzle 等等才能生效)

关于 +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 方法就会被调用(Reveal SDK 就是利用这一点,只要引入到工程中就能工作)

简单总结

整个事件由 dyld 主导,完成运行环境的初始化后,配合 ImageLoader 将二进制文件按格式加载到内存,

动态链接依赖库,并由 runtime 负责加载成 objc 定义的结构,所有初始化工作结束后,dyld 调用真正的 main 函数。

值得说明的是,这个过程远比写出来的要复杂,这里只提到了 runtime 这个分支,还有像GCD、XPC等重头的系统库初始化分支没有提及(当然,有缓存机制在,它们也不会玩命初始化),总结起来就是 main 函数执行之前,系统做了茫茫多的加载和初始化工作,但都被很好的隐藏了,我们无需关心。

孤独的 main 函数

当这一切都结束时,dyld 会清理现场,将调用栈回归,只剩下:

孤独的 main 函数,看上去是程序的开始,确是一段精彩的终结

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容