iOS开发-main函数之前app做了哪些事


一般情况下,App 的启动分为 冷启动热启动

冷启动是指, App 点击启动前,它的进程不在系统里,需要系统新创建一个进程分配给它启动的情况。这是一次完整的启动过程。
热启动是指 ,App冷启动后用户将 App 退后台,在 App 的进程还在系统里的情况下,用户重新启动进入 App 的过程,这个过程做的事情非常少。

main之前

main() 函数执行前,系统主要会做下面几件事情:

1.加载可执行文件(App 的.o文件的集合)

Mach-O 是针对不同运行时可执行文件的文件类型。对于Fat文件来说,可以拆分为各个架构的thin文件,thin文件再进行解包就是.o文件,对一个.o文件进行file命令我们会看到如下:

prep_cif.o: Mach-O 64-bit object arm64

如果对Fat文件,Thin文件概念比较模糊看 这里

使用MachOView进行查看,其结构如下
在这里插入图片描述
这是一个单个Object,一般来说我们会直接查看某个架构的.a文件。


几乎所有 Mach-O 都包含这三个段(segment)__TEXT,__DATA
__LINKEDIT

  • __TEXT
    包含 Mach header,被执行的代码和只读常量(如C 字符串)。只读可执行(r-x)

  • __DATA
    包含全局变量,静态变量等。可读写(rw-)

  • __LINKEDIT
    包含了加载程序的『元数据』,比如函数的名称和地址。只读(r–)

我们可以查看app程序下的二进制文件
在这里插入图片描述

  • Mach-O Universal文件
    FAT 二进制文件,将多种架构的Mach-O文件合并而成。它通过 Fat Header 来记录不同架构在文件中的偏移量,Fat Header 占一页的空间。
    按分页来存储这些segementheader会浪费空间,但这有利于虚拟内存的实现。

2.加载动态链接库,进行 rebase 指针调整和 bind 符号绑定;

下面的步骤构成了 dyld 的时间线:

Load dylibs -> Rebase -> Bind -> ObjC -> Initializers

  • 加载 Dylib
    从主执行文件的 header 获取到需要加载的所依赖动态库列表,而 header 早就被内核映射过。然后它需要找到每个dylib,然后打开文件读取文件起始位置,确保它是 Mach-O 文件。接着会找到代码签名并将其注册到内核。然后在dylib 文件的每个segment上调用 mmap()。应用所依赖的 dylib 文件可能会再依赖其他 dylib,所以 dyld 所需要加载的是动态库列表一个递归依赖的集合。一般应用会加载 100400dylib 文件,但大部分都是系统 dylib,它们会被预先计算和缓存起来,加载速度很快。

  • Fix-ups
    在加载所有的动态链接库之后,它们只是处在相互独立的状态,需要将它们绑定起来,这就是 Fix-ups。代码签名使得我们不能修改指令,那样就不能让一个 dylib 的调用另一个 dylib。这时需要加很多间接层。现代 code-gen 被叫做动态 PIC(Position Independent Code),意味着代码可以被加载到间接的地址上。当调用发生时,code-gen 实际上会在 __DATA 段中创建一个指向被调用者的指针,然后加载指针并跳转过去。所以 dyld 做的事情就是修正(fix-up)指针和数据。

Fix-up 有两种类型,rebasingbinding
RebasingBinding
Rebasing:在镜像内部调整指针的指向
Binding:将指针指向镜像外部的内容
可以通过MachOView查看 rebasebind等信息,下图可以看到我们常用的NSLog

在这里插入图片描述
我们使用PIC的概念和fishhook就可以进行hook外部dylib的函数,例如NSLog,见 博客

  • Rebasing
    在过去,会把dylib 加载到指定地址,所有指针和数据对于代码来说都是对的,dyld就无需做任何fix-up了。如今用了 ASLR 后悔将 dylib加载到新的随机地址(actual_address),这个随机的地址跟代码和数据指向的旧地址(preferred_address)会有偏差,dyld
    需要修正这个偏差(slide),做法就是将 dylib 内部的指针地址都加上这个偏移量,偏移量的计算方法如下:

ASLR(Address Space Layout Randomization):地址空间布局随机化,镜像会在随机的地址上加载。

slide = actual_address - preferred_address
  • Binding
    Binding 是处理那些指向 dylib 外部的指针,它们实际上被符号(symbol)名称绑定,也就是个字符串。之前提到 __LINKEDIT
    段中也存储了需要bind的指针,以及指针需要指向的符号。dyld
    需要找到 symbol 对应的实现,这需要很多计算,去符号表里查找。找到后会将内容存储到__DATA段中的那个指针中。Binding看起来计算量比Rebasing 更大,但其实需要的 I/O 操作很少,因为之前 Rebasing 已经替 Binding 做过了。

3.Objc 运行时的初始处理,包括 Objc 相关类的注册、category 注册、selector 唯一性检查等;

  • ObjC Runtime
    Objective-C 中有很多数据结构都是靠 RebasingBinding(fix-up)的,比如 Class 中指向元类的指针指向方法的指针
    ObjC是个动态语言,可以用类的名字来实例化一个类的对象。这意味着 ObjC Runtime 需要维护一张映射类名与类的全局表。当加载一个 dylib 时,其定义的所有的类都需要被注册到这个全局表中。
    C++ 中有个问题叫做易碎的基类(fragile base class)ObjC 就没有这个问题,因为会在加载时通过fix-up动态类中改变实例变量的偏移量。

易碎的基类:如果一个程序员无论何时修改了一个类,无论修改的是公共接口部分还是私有成员的声明部分,他都必须再次编译包含头文件的所有文件,这就是易碎的基类问题。因为其成员的偏移量有改变,而ObjC会进行fix-up来修复实例变量的偏移。

ObjC 中可以通过定义类别(Category)的方式改变一个的方法。有时你想要添加方法的类在另一个 dylib 中,而不在你的镜像中(也就是对系统或别人的类动刀),这时也需要做些fix-up

ObjC中的 selector 必须是唯一的。

  1. 去除不必要的类,可以通过infer等编译时工具进行检测,去掉不必要的代码,减少库imagemapped
  2. 所以减少category的数量也是能够一定量上提高app的运行速度

4.初始化,包括了执行 +load() 方法、attribute((constructor)) 修饰的函数的调用、创建 C++ 静态全局变量。

  • +load()

会分别执行每个分类+load方法

  • attribute((constructor))
#include <stdio.h>
#include <stdlib.h>
 
 
void static __attribute__((constructor)) before_main()
{
    printf("before main\n");
}
 
void static __attribute__((destructor)) after_main()
{
    printf("after main\n");
}
 
int main(int argc, char** argv)
{
    printf("hello world!\n");
}

__attribute__((constructor))修饰的函数在main函数之前执行

__attribute__((destructor))修饰的函数在main函数之后执行

  • C++ 静态全局变量在类加载之前被初始化,限于类中使用。

启动时间优化

Xcode为我们提供了获取各个dylib加载时间的方法,在Xcode 中 Edit scheme -> Run -> Auguments 将环境变量 DYLD_PRINT_STATISTICS 设为 1

相应地,这个阶段对于启动速度优化来说,可以做的事情包括:

  • 减少动态库加载。每个库本身都有依赖关系,苹果公司建议使用更少的动态库。静态库的加载时间更短,但是当我们的ExtensionApp需要使用同一部分代码时,我们需要将其封装动态库 (See this blog)。动态库同时也能解决二进制包文件过大的问题,苹果对app包大小判断是不将动态库包大小计算在内的。
  • check framework应设为optionalrequired,如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查;
  • 减少加载启动后不会去使用的类或者方法。
  • 合并或者删减一些OC类,关于清理项目中没用到的类,可以借助AppCode代码检查工具:
  • 删减一些无用的静态变量
  • 删减没有被调用到或者已经废弃的方法
  • 尽量不要用C++虚函数(创建虚函数表有开销)
  • 避免使用 attribute((constructor)),可将要实现的内容放在初始化方法中配合 dispatch_once 使用。
  • +load()方法里的内容可以放到首屏渲染完成后再执行,或使用 +initialize()方法替换掉。因为,在一个 +load() 方法里,进行运行时方法替换操作会带来 4 毫秒的消耗。不要小看这4 毫秒,积少成多,执行+load()方法对启动速度的影响会越来越大。
  • 控制C++ 全局变量的数量。

二进制重排

二进制重排是虚拟内存基于页和段来加载读取的原理,抖音团队对二进制重排有一篇文章进行了讲解 抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%


参考文章:
https://www.jianshu.com/p/534a37f588f2
https://www.jianshu.com/p/54d842db3f69
https://blog.automatic.com/how-we-cut-our-ios-apps-launch-time-in-half-with-this-one-cool-trick-7aca2011e2ea


就像女人关注你的细节一样😅,细节重要,可不要忘了决定成败的是你本身🙄

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值