美文网首页
iOS-App的加载过程-01

iOS-App的加载过程-01

作者: AndyGF | 来源:发表于2020-09-27 16:16 被阅读0次

我们大家都知道, 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() 函数之前还是做了其他的事情, 我们有三个问题:

  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.xcconfig是配置文件,
  • dyld2.cpp 里都是注释, 只有
  • dyldStartup.s 文件中既有声明, 又有调用, 左边红框标记出来的就是所有的声明.
_dyld_start 搜索结果

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(, 搜索结果如下:

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

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

dyld::_main

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

重点: 今天的主角来了 dyld

重点: 今天的主角来了 dyld

重点: 今天的主角来了 dyld

  • 600+ 行代码. 我的天呀, 这可怎么看, 不要慌, 仔细品, 先从整体看, _main 函数有个 uintptr_t 类型的返回值, 像这种有返回值的函数, 很明显返回值 很重要, 我们不妨试试倒着看看, 先看返回值.
_main 函数的返回值
  • 最终的返回值 result 在程序里都被赋值, 我们来看第 6792 行这个 sMainExecutable(主可执行文件), 一看就是重点. 我们顺着往前边找, 找什么呢 ? 找 sMainExecutable 被赋值的地方, 或者直接搜索 sMainExecutable =试试, 找到第 6577 行的结果如图, 是他的初始化.
sMainExecutable 搜索结果
  • 那么他是怎样初始化的呢 ? 让我们一起点进去看一下, 如下图:
instantiateFromLoadedImage
  • 此时此刻, 我们已经来到了 sMainExecutable 的初始化函数中, 看主要代码也就是下面这行初始化代码, 那我们就重点看一下, 这是初始化了一个ImageLoader, 我们继续点进去 instantiateMainExecutable 看一下都做了些什么.
    ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
instantiateMainExecutable 函数实现
  • instantiateMainExecutable 最终返回了一个主程序.

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

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

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

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

dyld:

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

版本 和 平台配置
MacOFile的路径 和 上下文路径配置
路由相关的配置
  • 2. 共享缓存

共享缓存
  • 3. 主程序的初始化

主程序的初始化
  • 4. 插入动态库

插入动态库
  • 5. 链接主程序

链接主程序
  • 6. 链接动态库

链接动态库
  • :7. 运行所有的初始化, 执行 main() 函数

运行所有的初始化, 执行 `main()` 函数

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

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

  • 补充扩展 :

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

  • 需要了解一下 静态库 和 动态库 的区别, 在本文中不是很重要, 但是需要简单介绍一下, 他们的优缺点都两者相比较而言.
    静态库
    比如以 .a , .lib 结尾的, 他是编译好的可执行文件, 启动加载快, 在哪里引用, 就会在哪里完整的编译一份, 相当于把代码复制过去, 插入当前代码, 也就是说, 如果有多个地引用, 他在程序中就可能存在多份.
静态库

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

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

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

动态库
  • 编译过程 :


    编译过程

相关文章

网友评论

      本文标题:iOS-App的加载过程-01

      本文链接:https://www.haomeiwen.com/subject/pdxquktx.html