转:iOS程序main函数之前发生了什么
原文地址:http://blog.sunnyxx.com/2014/08/30/objc-pre-main/
我是前言
一个iOS app的main()
函数位于main.m
中,这是我们熟知的程序入口。但对objc了解更多之后发现,程序在进入我们的main
函数前已经执行了很多代码,比如熟知的+ load
方法等。本文将跟随程序执行顺序,刨根问底,从dyld
到runtime
,看看main函数之前都发生了什么。
从dyld开始
动态链接库
iOS中用到的所有系统framework都是动态链接的,类比成插头和插排,静态链接的代码在编译后的静态链接过程就将插头和插排一个个插好,运行时直接执行二进制文件;而动态链接需要在程序启动时去完成“插插销”的过程,所以在我们写的代码执行前,动态连接器需要完成准备工作。
这个是在xcode中看到的Link列表:
这些framework将会在动态链接过程中被加载,另外还有隐含link的framework,可以测试出来:先找到可执行文件,我这里叫TestMain
的工程,模拟器路径下找到TestMain.app
,可执行文件默认同名,再通过otool
命令:
|
|
-L参数打印出所有link的framework(去掉了版本信息):
|
|
除了多了的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(这缩写对应的很奇怪,我感觉是DYnamic Linker Daemon呢- -?)apple的动态链接器,系统kernel做好启动程序的初始准备后,交给dyld负责,援引并翻译《mikeask这篇blog》对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担当了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、方法混合等等才能生效)
关于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函数,看上去是程序的开始,确是一段精彩的终结
References
https://www.mikeash.com/pyblog/friday-qa-2012-11-09-dyld-dynamic-linking-on-os-x.html
http://newosxbook.com/articles/DYLD.html
http://docstore.mik.ua/orelly/unix3/mac/ch05_02.htm
https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/dyld.1.html
转:iOS程序main函数之前发生了什么的更多相关文章
- iOS程序main函数之前发生了什么
我是前言 一个iOS app的main()函数位于main.m中,这是我们熟知的程序入口.但对objc了解更多之后发现,程序在进入我们的main函数前已经执行了很多代码,比如熟知的+ load方法等. ...
- Keil开发的ARM程序main函数之前的汇编分析
Keil开发的ARM程序main函数之前的汇编分析 ——BIN文件中RW段的数据移动 系统平台: STM32系列STM32F103ZE,512KB内部FLASH,64KB片内存储; FLASH地址范围 ...
- 问题:C#控制台程序参数;结果:设置与读取C#控制台应用程序Main函数中的参数args
设置与读取C#控制台应用程序Main函数中的参数args 在项目属性面版->调试->命令行参数设置.空格分隔.读取:string[] str = Environment.GetComman ...
- 从创建进程到进入main函数,发生了什么?
前几天,读者群里有小伙伴提问:从进程创建后,到底是怎么进入我写的main函数的? 今天这篇文章就来聊聊这个话题. 首先先划定一下这个问题的讨论范围:C/C++语言 这篇文章主要讨论的是操作系统层面上对 ...
- linux中应用程序main函数中没有开辟进程的,它应该在那个进程中运行呢?
1.main函数是一个进程还是一个线程? 不知道你是用c创建的,还是用java创建的. 因为它们都是以main()做为入口开始运行的. 是一个线程,同时还是一个进程. 在现在的操作系统中,都是多线程的 ...
- C 语言main 函数终极探秘(&& 的含义是:如果 && 前面的程序正常退出,则继续执行 && 后面的程序,否则不执行)
所有的C程序必须定义一个称之为main的外部函数,这个函数是程序的入口,也就是当程序启动时所执行的第一个函数,当这个函数返回时,程序也将终止,并且这个函数的返回值被看成是程序成功或失败的 ...
- 系统设计 - IOS 程序插件及功能动态更新思路
所用框架及语言 IOS客户端-Wax(开发愤怒的小鸟的连接Lua 和 Objc的框架),Lua,Objc, 服务端-Java(用于返回插件页面) 由 于Lua脚本语言,不需要编译即可运行 ...
- iOS 程序插件及功能动态更新思路
所用框架及语言 iOS客户端-Wax(开发愤怒的小鸟的连接Lua 和 Objc的框架),Lua,Objc, 服务端-Java(用于返回插件页面) 工具框架链接地址:Wax - https://gith ...
- .NET中Main函数使用小技巧
摘要:任何语言开发出来的程序,都会有一个程序入口函数,可能每个语言所使用的程序入口函数名称不一样,但是它们的作用都是一样的,都是被操作系统去调用.那么本文主要总结.NET中的程序入口函数Main使用的 ...
随机推荐
- 在 Emacs 中如何退出 Slime Mode
1.在 Slime 的 Buffer 中按逗号“,”: 2.在 Command 后输入:sayoonara 3.回车,确认. ================ 退出 SBCL 输入:(sb-ext:q ...
- Linux Tweak:交换 Caps_Lock 与 Control_R
很少使用的Caps_Lok键占据着键盘的黄金位置,不仅如此,它还经常被按错. 于是受到程序员神器HHKB启发(如图) 对于我,Linux程序员 + vimer来说: ESC取代`键,极大的方便了VIM ...
- 迟到的 WPF 学习 —— 控件
这一章书中内容比较多而杂,但每个对象的内容又相对简短,所以只挑选里边有代表性的内容做记录. 1. Label 控件:一个基础的简单的 ContentControl,Labe 支持快捷键文本的设置,可以 ...
- jquery简单封装
对Raphael画图标的一个jquery简单封装 公司要做一个项目的demo,要求地图上可以插红旗,所以就用到了Raphael. 因为是个demo,所以地图就用了一张图片,效果如下: 所以为了更好的封 ...
- zTree应用实例详讲
zTree应用实例详讲(1) 因为项目的需要,要创建一棵动态的文件树,此树除了实现异步获取子节点外,还要实现对树节点的增.删.改.查.移动.重命名.批量删除.批量移动. 每一个操作都要和数据库打交道. ...
- git shell 常用命令
git branch 查看本地所有分支 git status 查看当前状态 git commit 提交 git branch -a 查看所有的分支 git branch -r 查看远程所有分支 git ...
- 微信小程序的动画效果
前言 由于公司计划有变,所以从H5页面改成去小程序写.所以在着手开发小程序.本人也不是什么前端高手,只是一名写后端偶尔写写前端的渣渣.请前端大神们勿喷. 一.什么是微信小程序? 小程序在我的理解中只是 ...
- 近期unity ios接入的事情
1, 在接入苹果内支付的时候,遇到一个很严重的问题,使用的公司的moni2来测试的,但是在测试的过程中发现每次调用oc的内支付代码后,总会先回调一个支付成功,然后弹出输入密码框,当点击取消后,再一次 ...
- AngularCSS--关于angularjs动态加载css文件的方法(仅供参考)
AngularCSS CSS on-demand for AngularJS Optimize the presentation layer of your single-page apps by d ...
- CreateMutex 创建一个有名字的互斥量的时候hMutex=CreateMutex(NULL,TRUE,"tickets")报错
编译器报错: 不能将参数 3 从“const char [8]”转换为“LPCWSTR”,怎么改成LPCWSTR类型 更改方法: hMutex=CreateMutex(NULL,TRUE,L" ...