文章转载自 http://blog.sunnyxx.com/2014/08/30/objc-pre-main/,侵权必删除

前言

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

从dyld开始

动态链接库

iOS中用到的所有系统framework都是动态链接的,类比成插头和插排,静态链接的代码在编译后的静态链接过程就将插头和插排一个个插好,运行时直接执行二进制文件;而动态链接需要在程序启动时去完成“插插销”的过程,所以在我们写的代码执行前,动态连接器需要完成准备工作。

这个是在xcode中看到的Link列表:

这些framework将会在动态链接过程中被加载,另外还有隐含link的framework,可以测试出来:先找到可执行文件,我这里叫TestMain的工程,模拟器路径下找到TestMain.app,可执行文件默认同名,再通过otool命令:

除了多了的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.dyliblibSystem.B.dylib的替身,哪天想升级直接换成libSystem.C.dylib然后再替换替身就行了
  • 减少可执行文件体积:相比静态链接,可执行文件的体积要小很多

dyld

dyld - the dynamic link editor(这缩写对应的很奇怪,我感觉是DYnamic Linker Daemon呢- -?)apple的动态链接器,系统kernel做好启动程序的初始准备后,交给dyld负责,援引并翻译《mikeask这篇blog》对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是开源的,github地址,我们可以从源码一探究竟。

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

  1. 调用dyldbootstrap::start()方法(省去参数)
  2. 上个方法返回了main函数地址,填入参数并调用main函数

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

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

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

ImageLoader

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

  1. 在程序运行时它先将动态链接的image递归加载 (也就是上面测试栈中一串的递归调用的时刻)
  2. 再从可执行文件image递归加载所有符号

当然所有这些都发生在我们真正的main函数执行前。

Runtime与+load

刚才讲到libSystem是若干个系统lib的集合,所以它只是一个容器lib而已,而且它也是开源的,里面实质上就一个文件,init.c,细节不说了,由libSystem_initializer逐步调用到了_objc_init,这里就是objc和runtime的初始化入口。

除了runtime环境的初始化外,_objc_init中绑定了新image被加载后的callback:

1
2
3
dyld_register_image_state_change_handler(dyld_image_state_bound,1/*batch*/, &map_images);
dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);

可见dyld担当了runtimeImageLoader中间的协调者,当新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、方法混合等等才能生效)

关于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这个分支,还有像GCDXPC等重头的系统库初始化分支没有提及(当然,有缓存机制在,它们也不会玩命初始化),总结起来就是main函数执行之前,系统做了茫茫多的加载和初始化工作,但都被很好的隐藏了,我们无需关心。


孤独的main函数

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

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

iOS程序main函数之前发生了什么相关推荐

  1. iOS 程序 main 函数之前发生了什么

    一个 iOS App 的 main 函数位于 main.m 中,这是我们熟知的程序入口.但对 objc 了解更多之后发现,程序在进入我们的 main 函数前已经执行了很多代码,比如熟知的 + load ...

  2. iOS 程序 main函数之前发生什么

    自己总结: 1.系统读取可执行文件,初始化运行环境. 2.开启缓存策略,加载链接相关的依赖库,初始化每个依赖库,在这一步runtime被初始化. 3.程序可执行文件初始化,runtime对项目中的类进 ...

  3. linux main是什么进程,从创建进程到进入main函数,发生了什么?

    从创建进程到进入main函数,发生了什么? 从创建进程到进入main函数,发生了什么? 前几天,读者群里有小伙伴提问:从进程创建后,到底是怎么进入我写的main函数的? 今天这篇文章就来聊聊这个话题. ...

  4. 从创建进程到进入 main 函数,发生了什么?

    作者 | 轩辕之风O 来源 | 编程技术宇宙(ID:xuanyuancoding) 头图 |  CSDN 下载自东方IC 前几天,读者群里有小伙伴提问:从进程创建后,到底是怎么进入我写的 main 函 ...

  5. 设置与读取C#控制台应用程序Main函数中的参数args

    在项目属性面版->调试->命令行参数设置.空格分隔. 读取:string[] str = Environment.GetCommandLineArgs(); Main函数是C#应用程序的入 ...

  6. c语言main函数参数详解,c/c++程序main函数参数解析,以及参数传递的不同方法

    1. 程序运行时传递参数的不同方法: 1)通过命令参数传递参数: 2) 通过txt文件读取参数. 2. 方法举例 1)通过命令参数传递参数,即解析main函数的参数. main函数可以不带参数,也可以 ...

  7. IOS 程序插件及功能动态更新思路┊

    http://www.cocoachina.com/bbs/read.php?tid=129723&keyword=%B2%E5%BC%FE 带图片版 CSDN路径-- http://blog ...

  8. iOS 程序插件及功能动态更新思路

    所用框架及语言 iOS客户端-Wax(开发愤怒的小鸟的连接Lua 和 Objc的框架),Lua,Objc, 服务端-Java(用于返回插件页面) 工具框架链接地址:Wax - https://gith ...

  9. Linux C: 为什么C都必须有一个main函数

    gcc的编译过程分为三步: 第一步 将 *.c 文件分别通过编译器解析成汇编语言    *.s     . 第二步将  *.s 文件分别通过汇编器生产目标文件  *.o      . 第三步将 c.o ...

最新文章

  1. 假如时光倒流,你依然会变成现在的你
  2. 客户端升级为select模型
  3. 计算机狐狸标志的程序,小狐狸等分线计算工具
  4. Pandas入门2(DataFunctions+Maps+groupby+sort_values)
  5. python性能测试模块_技巧python模块性能测试-阿里云开发者社区
  6. “芯生力量,智能持久”出门问问全新发布TicWatch Pro 3
  7. 零基础入门语义分割-Task5 模型训练与验证
  8. 【转贴】二节棍精典棍花动作详解
  9. [Android] Android开机启动Activity或者Service方法
  10. Hadoop和Hbase版本选择
  11. caffe学习系列:网络融合
  12. bugku bingo题解
  13. 使用迅雷下载百度云盘数据
  14. XCOM Enemy Unknown
  15. 一个屌丝程序猿的人生(六十九)
  16. Java面向对象程序设计综合练习2(编程题)
  17. PS运动模糊+径向模糊+高斯模糊
  18. JAVA语言编程练习--图形界面+文件输入输出流--实现简单的用户注册登录系统
  19. The 2018 JUST Collegiate Programming Contest H题 Cube
  20. labelme标注结果可视化(持续补充)

热门文章

  1. 思维导图软件哪个免费更好用?MindManager、iMindMap、MindMappe
  2. 十年前他在马王堆送快递,如今当上航空主管,拯救客户亿元大单
  3. 物联网定位技术|实验报告|实验一 Wi-Fi指纹定位
  4. 微信支付宝借刷脸支付抢占线下支付市场
  5. tomcat用c语言开发服务,apache tomcat是什么语言开发的
  6. mysql+php+模板+条目_html模板
  7. 【七七八八】简单的对话python代码实现
  8. Day036 《电影院售票系统》项目全码
  9. 全球直播的罗胖跨年演讲背后技术支撑故事——罗辑思维首席架构师方圆访谈...
  10. 阿里云AI训练营第五天