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

我是前言

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


从dyld开始

动态链接库

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

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

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

1
$ otool -L TestMain

-L参数打印出所有link的framework(去掉了版本信息):

1
2
3
4
5
6
7
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(这缩写对应的很奇怪,我感觉是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担当了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、方法混合等等才能生效)

关于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函数,看上去是程序的开始,确是一段精彩的终结


时间: 2024-08-11 06:02:07

iOS程序main函数之前发生了什么的相关文章

iOS 从main函数开始

app程序的入口,同样是main函数 main函数为 int main(int argc, char * argv[]),C系语言大多都是这个样子,argc是命令行总的参数个数,argv是参数的数组,值得一提的是argv中第一个参数为app的路径+全名. 然后就是main中的代码 @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } autor

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

所用框架及语言 iOS客户端-Wax(开发愤怒的小鸟的连接Lua 和 Objc的框架),Lua,Objc, 服务端-Java(用于返回插件页面) 工具框架链接地址:Wax - https://github.com/probablycorey/wax  Netty - https://netty.io/  用做Http服务器,返回页面 由于Lua脚本语言,不需要编译即可运行,这点是我的这个思路可以执行的大前提,再加上苹果允许像Lua这样的脚本的存在,这一思路才能得以实现.个人感觉这一思路有点类似于

Main()函数

Main函数是C#应用程序的入口点,执行Main()就是执行应用程序 Main函数有四种不同形式 1 static void Main() 2 static void Main(string[] args) 3 static int Main() 4 static int Main(string[] args) args为命令行参数,可以在调试中添加和更改 int返回值一般情况下返回0代表“正常”的终止(即应用程序已经执行完毕,并安全终止) 1 //在命令行中加入 256 myfile.txt"

【iOS】被忽略的main函数

如同任何基于C的应用程序,程序启动的主入口点为iOS应用程序的main函数.在iOS应用程序,main函数的作用是很少的.它的主要工作是控制UIKit framework.因此,你在Xcode中创建任何新的项目都配备了一个默认的主函数.除了少数特例外,你永远不应该改变这个函数的实现. 1 #import <UIKit/UIKit.h> 2 3 #import "AppDelegate.h" 4 5 int main(int argc, char * argv[]) 6 {

第十四周项目4-1:处理C++源代码的程序-查找main()函数

问题及代码: /* *Copyright (c)2015,烟台大学计算机与控制工程学院 *All rights reserved. *文件名称:project.cpp *作 者:陈文青 *完成日期:2015年6月16日 *版 本 号:v1.0 * *问题描述:(1)读入一个C++程序,判断其中是否只有一个main()函数,输出"暂时没有发现问题",或者"没有main()函数",或者"不能定义多个main()函数": 提示1:简单处理,可以只比较判

iOS 温故知新之 Main函数和UIApplication

如同C/C++程序都有一个入口函数Main函数一样,iOS app开发也有相同的入口main函数. 然而今天发现在修改老版本的程序发现,跟新版xcode创建出来的main函数有一点不同 这是现在使用Xcode6.3创建的main函数代码: int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate cl

C语言中的main函数为什么被称作程序入口

以前在学Python时,对于类前的__main__判断有过了解,后来在学习C语言时发现其实都是互通的.所有的程序入口,比如main或者WINmain,在很多编程语言中都以主函数的方式出现.下面为大家整理了一些程序入口的基本概念. 首先,我们的程序进入到入口函数之前,是发生了很多事情的.操作系统的安排,启动运行时库,运行时库再初始化好环境,然后启动你的入口函数,你的程序才正常的运行起来.等你的程序运行结束后,就退回到运行时库,然后再退回到操作系统,然后系统再调度其他程序执行. 在系统把使用权交给我

WPF实践系列1 of n 自定义WPF程序的Main函数

关于开篇 由于业务需要参与到一个Window下的WPF项目中.因为之前的工作环境一直在Linux下,C和C++作为主力开发语言, 因此加入新项目对自己来说是变化,同时也是挑战:学点新东西拓宽下视野. 关于WPF实践系列 项目是个中型的Windows客户端,需要通过网络和SQL服务器交互.由于有多版本兼容需求(xp-win10)和高分屏需求,选了WPF方案,在Visual Studio 2017下进行开发. 博主目前对WPF和 .Net这些知识仅是略知一二.取名实践系列,意指在过程中边做边学.主要

android看不见main函数怎么办?程序异常了,可以不提示“xxx软件停止运行”吗?

今天遇到了这个问题,分享一下解决方案. android没有main 函数,自然也就不存在main里面添加异常处理来实现全局异常捕获的方案.那android程序有全局异常补货的解决方案吗? 答案是有的: 那就是你得继承android工程里面的application,如: public class ReaderApplication extends Application implements Thread.UncaughtExceptionHandler{......} 并实现线程异常补货接口:T