iOS-App的加载过程-01

2020-09-27  本文已影响0人  AndyGF

我们大家都知道, main() 函数是一个程序的入口, 那么我们来体验一下, 看看 main() 是不是app 执行的第一个函数.

我们先创建一个 iOS App 工程, 重写 ViewController 的 +load 方法, 在 main() 函数下方声明一个 C++ 静态函数, 在 +load 方法 , kcFunc函数 , main() 函数中分别打印些内容, 具体如下.

@implementation ViewController

+ (void)load {
    NSLog(@"我是ViewController 的 +load 方法: %s",__func__);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}

@end
// 内存 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() 函数之前还是做了其他的事情, 我们有三个问题:

  1. 是谁做的这些事情 ,
  2. 又做了些什么,
  3. 流程是什么样的.

接下来就开始我们的探索, 首先要找到是谁做的这些事情, 既然是要探索 main() 之前, 那么我们就应该把断点添加在 main() 函数之前, +load方法最先执行, 所以我们就先在 +load 方法中打个断点, 如图一, 把项目跑起来. 当断点停住时, 以汇编方式进行查看, 我们的重点是尽量向前查看, 我们先用 LLDB 命令 bt 在控制台输出一下堆栈信息, 如图二, 然后在看左边的堆栈, 不要犹豫, 赶紧点一下最下边的 ``, 如图三.

图一: +load 方法添加断点
图二: 查看堆栈信息

查看汇编

图三: 查看汇编 和 左边堆栈

注意: 想要看左边完整堆栈信息, 要关闭左下角的开关, 已经标记出来.
查看汇编方法参考我的另一篇文章: OC底层原理01-源码探索跟踪的三种技巧

那么重点来了, 我已经在汇编图中框出来了, 最先执行的就是dyld 这个库中的 _dyld_start 函数 , 而执行这个函数的就是 dyld, 没错, 在 main() 函数之前做事情正是 dyld.

现在我们的第一个问题已经解决了, 在 main() 函数之前做事的是 dyld, 那么 dyld 是怎么一步一步执行到 main() 函数的, 后两个问题一起探索.

很明显我们先要从 _dyld_start 这个函数入手去分析, dyld 是一个系统库, 而且大家也都知道, 这是不开源的, 所以我们只能去苹果的 Open Source 开源网站去下载源码去查看, (请自行下载, 如有需要请联系我). 本次使用 dyld-750.6 最新版本.

苹果开源网站

  1. 我们先在 dyld 这个库中全局搜索一下 _dyld_start 函数, 找到这个函数的声明和实现, 如下图, 已经标出重要信息.
_dyld_start 搜索结果

dyldStartup.s.s 结尾的文件表示是汇编代码, 这个文件里都是汇编代码,
_dyld_start 在汇编代码中前边要加一个 _, 所以我们找到的 _ _dyld_start(为表示两个_ 我在中间加了个空格)就是我们要找的函数,
4 个声明分别针对 4 种 cpu 架构.
汇编看不懂没关系, 看注释就可以

所以我们全局搜索 dyldbootstrap 找到他的命名空间, 然后再搜索 start 函数, 关键词用 start(, 搜索结果如下:

dyldbootstrap 命名空间 start 函数搜索结果 + 代码

看上文中的 图二: 查看堆栈信息 这张图片, _dyld_start后的下一个堆栈就是 dyld::_main, 项目用的是真机, 堆栈信息就没有显示 dyldbootstrap::start 信息, 所以重点是dyld_main() 函数, 其他的信息可以先忽略,接下来我们跳转进去, 先整体粗略看一下, 600+ 行代码.

dyld::_main

接下来开始分析 dyld::_main :

重点: 今天的主角来了 dyld

重点: 今天的主角来了 dyld

重点: 今天的主角来了 dyld

_main 函数的返回值 sMainExecutable 搜索结果 instantiateFromLoadedImage instantiateMainExecutable 函数实现

这个源码分析到这里就可以了, 我们的重点是 dyld 做了什么, 流程是什么, 所以不需要太细, 有兴趣的可以多研究一下, 这里不做更详细的分析.

综上可知, sMainExecutable 就是主程序.

那么接下来让我们继续回到今天的主题 dyld, 分析一下他到底做了什么 ?

我先列出来吧, 然后对照着去看看 dyld::_main 函数.

dyld:

  1. 环境变量配置
  2. 共享缓存
  3. 主程序的初始化, sMainExecutable
  4. 插入动态库
  5. 链接主程序
  6. 链接动态库
  7. 运行所有的初始化, 执行 main() 函数
版本 和 平台配置
MacOFile的路径 和 上下文路径配置
路由相关的配置 共享缓存 主程序的初始化 插入动态库 链接主程序 链接动态库 运行所有的初始化, 执行 `main()` 函数

注意 :
主程序和动态库的链接, 一定是先链接主程序

文章篇幅过长, 请看下一篇文章 iOS-App的加载过程-02, 继续对 App 的加载过程进行分析.

因为这个源码是系统级的, 是无法编译调试的, 只能是手动分析.
我们要看的是主流程, 无关紧要的东西可以忽略, 不然你会迷失在源码的海洋中, 无法自拔.
分析的时候, 要有目的, 讲方法.
期中涉及到部分汇编代码, 看不懂不要紧, 看注释就行, 注释就是汇编代码的意义.

静态库

优点: 编译好的可执行文件, 启动加载快.
缺点: 如果多处引用, 打包体积大, 占用内存大. 损耗性能.

动态库
比如 .framework , .so 结尾的, 系统级别的动态库, 在系统中只会存在一份, 非系统级别的每个 App 中只中存在一份, 如果有多处引用, 共用同一份.

优点: 占用内存小,
缺点: 加载慢,

动态库
上一篇 下一篇

猜你喜欢

热点阅读