美文网首页
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