我们大家都知道, main()
函数是一个程序的入口, 那么我们来体验一下, 看看 main()
是不是app 执行的第一个函数.
我们先创建一个 iOS App 工程, 重写 ViewController 的 +load
方法, 在 main()
函数下方声明一个 C++ 静态函数, 在 +load
方法 , kcFunc
函数 , main()
函数中分别打印些内容, 具体如下.
- ViewController
@implementation ViewController
+ (void)load {
NSLog(@"我是ViewController 的 +load 方法: %s",__func__);
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
@end
- main()
// 内存 main() dyld image init 注册回调通知 - dyld_start -> dyld::main() -> main()
// rax
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
NSLog(@"我是 main() 函数");
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
// load -> Cxx -> main
__attribute__((constructor)) void kcFunc(){
printf("我是 C++ 函数: %s \n",__func__);
}
把项目跑起来, 打印输出结果如下图 :
打印结果
那么问题来了, 为什么 main()
函数最后执行, load 方法最先执行, 这就说明 在 main()
函数之前还是做了其他的事情, 我们有三个问题:
- 是谁做的这些事情 ,
- 又做了些什么,
- 流程是什么样的.
接下来就开始我们的探索, 首先要找到是谁做的这些事情, 既然是要探索 main()
之前, 那么我们就应该把断点添加在 main()
函数之前, +load
方法最先执行, 所以我们就先在 +load
方法中打个断点, 如图一, 把项目跑起来. 当断点停住时, 以汇编方式进行查看, 我们的重点是尽量向前查看, 我们先用 LLDB 命令 bt
在控制台输出一下堆栈信息, 如图二, 然后在看左边的堆栈, 不要犹豫, 赶紧点一下最下边的 ``, 如图三.
图二: 查看堆栈信息
查看汇编
注意: 想要看左边完整堆栈信息, 要关闭左下角的开关, 已经标记出来.
查看汇编方法参考我的另一篇文章: OC底层原理01-源码探索跟踪的三种技巧
那么重点来了, 我已经在汇编图中框出来了, 最先执行的就是dyld
这个库中的 _dyld_start
函数 , 而执行这个函数的就是 dyld
, 没错, 在 main()
函数之前做事情正是 dyld.
现在我们的第一个问题已经解决了, 在 main()
函数之前做事的是 dyld
, 那么 dyld 是怎么一步一步执行到 main()
函数的, 后两个问题一起探索.
很明显我们先要从 _dyld_start
这个函数入手去分析, dyld
是一个系统库, 而且大家也都知道, 这是不开源的, 所以我们只能去苹果的 Open Source 开源网站去下载源码去查看, (请自行下载, 如有需要请联系我). 本次使用 dyld-750.6 最新版本.
- 我们先在
dyld
这个库中全局搜索一下_dyld_start
函数, 找到这个函数的声明和实现, 如下图, 已经标出重要信息.
-
dyld.xcconfig
是配置文件, -
dyld2.cpp
里都是注释, 只有 -
dyldStartup.s
文件中既有声明, 又有调用, 左边红框标记出来的就是所有的声明.
dyldStartup.s
以.s
结尾的文件表示是汇编代码, 这个文件里都是汇编代码,
_dyld_start
在汇编代码中前边要加一个_
, 所以我们找到的_ _dyld_start
(为表示两个_
我在中间加了个空格)就是我们要找的函数,
4 个声明分别针对 4 种 cpu 架构.
汇编看不懂没关系, 看注释就可以
-
__dyld_start:
就是函数入口, -
// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
表示调用到dyldbootstrap
中的start
函数,
所以我们全局搜索 dyldbootstrap
找到他的命名空间, 然后再搜索 start
函数, 关键词用 start(
, 搜索结果如下:
看上文中的 图二: 查看堆栈信息
这张图片, _dyld_start
后的下一个堆栈就是 dyld::_main
, 项目用的是真机, 堆栈信息就没有显示 dyldbootstrap::start
信息, 所以重点是dyld
的 _main()
函数, 其他的信息可以先忽略,接下来我们跳转进去, 先整体粗略看一下, 600+ 行代码.
接下来开始分析 dyld::_main
:
重点: 今天的主角来了 dyld
重点: 今天的主角来了 dyld
重点: 今天的主角来了 dyld
- 600+ 行代码. 我的天呀, 这可怎么看, 不要慌, 仔细品, 先从整体看,
_main
函数有个uintptr_t
类型的返回值, 像这种有返回值的函数, 很明显返回值
很重要, 我们不妨试试倒着看看, 先看返回值.
- 最终的返回值
result
在程序里都被赋值, 我们来看第 6792 行这个sMainExecutable
(主可执行文件), 一看就是重点. 我们顺着往前边找, 找什么呢 ? 找sMainExecutable
被赋值的地方, 或者直接搜索sMainExecutable =
试试, 找到第 6577 行的结果如图, 是他的初始化.
- 那么他是怎样初始化的呢 ? 让我们一起点进去看一下, 如下图:
- 此时此刻, 我们已经来到了
sMainExecutable
的初始化函数中, 看主要代码也就是下面这行初始化代码, 那我们就重点看一下, 这是初始化了一个ImageLoader
, 我们继续点进去 instantiateMainExecutable 看一下都做了些什么.
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
-
instantiateMainExecutable
最终返回了一个主程序.
这个源码分析到这里就可以了, 我们的重点是 dyld 做了什么, 流程是什么, 所以不需要太细, 有兴趣的可以多研究一下, 这里不做更详细的分析.
综上可知, sMainExecutable
就是主程序.
那么接下来让我们继续回到今天的主题 dyld
, 分析一下他到底做了什么 ?
我先列出来吧, 然后对照着去看看 dyld::_main
函数.
dyld:
- 环境变量配置
- 共享缓存
- 主程序的初始化,
sMainExecutable
- 插入动态库
- 链接主程序
- 链接动态库
- 运行所有的初始化, 执行
main()
函数
-
1. 环境变量: 主要有 版本, 平台, 当前MacOFile的路径, 当前上下文路径, 路由.
MacOFile的路径 和 上下文路径配置
路由相关的配置
-
2. 共享缓存
-
3. 主程序的初始化
-
4. 插入动态库
-
5. 链接主程序
-
6. 链接动态库
-
:7. 运行所有的初始化, 执行
main()
函数
注意 :
主程序和动态库的链接, 一定是先链接主程序
文章篇幅过长, 请看下一篇文章 iOS-App的加载过程-02, 继续对 App 的加载过程进行分析.
-
补充扩展 :
因为这个源码是系统级的, 是无法编译调试的, 只能是手动分析.
我们要看的是主流程, 无关紧要的东西可以忽略, 不然你会迷失在源码的海洋中, 无法自拔.
分析的时候, 要有目的, 讲方法.
期中涉及到部分汇编代码, 看不懂不要紧, 看注释就行, 注释就是汇编代码的意义.
- 需要了解一下 静态库 和 动态库 的区别, 在本文中不是很重要, 但是需要简单介绍一下, 他们的优缺点都两者相比较而言.
静态库
比如以.a
,.lib
结尾的, 他是编译好的可执行文件, 启动加载快, 在哪里引用, 就会在哪里完整的编译一份, 相当于把代码复制过去, 插入当前代码, 也就是说, 如果有多个地引用, 他在程序中就可能存在多份.
优点: 编译好的可执行文件, 启动加载快.
缺点: 如果多处引用, 打包体积大, 占用内存大. 损耗性能.
动态库
比如 .framework
, .so
结尾的, 系统级别的动态库, 在系统中只会存在一份, 非系统级别的每个 App 中只中存在一份, 如果有多处引用, 共用同一份.
优点: 占用内存小,
缺点: 加载慢,
-
编译过程 :
编译过程
网友评论