关于APP启动优化一点点小学习分享。

30 阅读8分钟

screenshot.png

本篇是记录一下自己的学习笔记,如有勘误望见谅.

先放上本文的学习来源链接,WWDC 16 和17, 19年 的三个session 优化启动时间 developer.apple.com/wwdc16/406 app启动时间:过去,现在,与未来 developer.apple.com/wwdc17/413 优化App启动 developer.apple.com/wwdc19/423

关于Mach-O和虚拟内存

#####Execuable(运行时 可执行文件的文件类型) 可执行文件 应用里最重要的二进制文件 也是应用扩展文件的主二进制文件

#####Dylib(动态库) Dylib是一个动态库 在其他平台上 用的是你可能会熟悉的名字: DSO和DLL

#####Bundle(捆绑包) 捆绑包是一种特殊的Dylib 是不能进行链接的 只能在运行时 用dlopen()函数打开它

#####Image(这里image是指任意上述三种文件) 例如 Dylib,可执行文件

#####FarmeWork(框架) 这里指 存储该Dylib需要的文件

Mach-O图像格式

screenshot.png

所有的段名都是由大写字母组成

每一段都是页面大小的倍数 图片该例中 TEXT段大小是3页 DATA和LINKEDIT段大小是1页 页面大小由硬件决定 arm64处理器的页面大小是16K, 其他都是4K

一种查看方式是分区 编译器常常会忽略分区 分区是段的子范围 截屏2021-02-27 上午9.24.48.png 分区不用遵循页面的大小 但是它们是不重叠的

最常见的段名是 TEXT DATA LINKEDIT 实际上几乎每一个二进制文件 都包含这三段

TEXT是文件的开头 它包含了Mach的头文件 任何机器指令 以及任何只读常量 比如C字符串
DATA段是重写段 它包含了所有的全局变量
LINKEDIT段 它不包含全局变量的函数 它包含变量函数信息 比如名称和地址

screenshot.png

####虚拟内存

虚拟内存解决的问题是 所有这些进程存在时 该如何管理所有物理内存? 所以他们添加了一个小的间接层 每一个进程都是一个逻辑地址空间 映射到RAM的某个物理页面.

这种映射不一定是一对一的 逻辑地址可以不对应任何物理RAM 也可以多个逻辑地址对应 同一物理RAM 这样带来很多种可能 能利用虚拟内存做什么呢? 首先 如果有一个逻辑地址 不映射任何物理RAM 当进程要访问该地址时 就会产生页面错误 内核将停止该线程 并试图找出解决方案

#####copy on write 内存写时复制. 在所有进程里共享DATA页面 只要进程是只读, 共享内容的全局变量, 但是一旦有进程想要 写入其DATA页面 写入时复制开始 内核会把该页面复制 放入另一个物理RAM并重定向映射 所以该进程有了该页面的副本

screenshot.png

Dylib是如何操作

Dylib必须要做的第一件事 是查看Mach头文件 在内存里 在该进程里 它将查看内存的顶盒, 由于虚拟内存的原因, 启动时那里是空的 没有内容映射到物理页面上 所以产生页面错误 到那时内核意识到 它被映射到了一个文件 所以它将读取文件的第一页放入物理RAM设置其映射 , 
从而使得Dylib可以真正通过 Mach头文件开始读取 
dyld实际上 将进程里面的虚拟内存的 text,data,LINKEDIT 实际的加载到指定的进程中去

##App的启动流程 exec() 到main ()

screenshot.png

image.png

Dyld 的工作流程 pro-main 的过程 截屏2021-02-27 上午11.04.58.png

##先分析一下启动的时候,发生了什么, loadDylbs-> bebase -> binding -> initializes -> main 其实启动的步骤就是这几步

screenshot.png

Rebase/Binding 环节的优化

在讲rebase之前,我们需要先讲一下ASLR

ASLR(Address Space Layout Randomization),地址空间布局随机化。在ASLR技术出现之前,程序都是在固定的地址加载的,这样hacker可以知道程序里面某个函数的具体地址,植入某些恶意代码,修改函数的地址等,带来了很多的危险性。ASLR就是为了解决这个的,程序每次启动后地址都会随机变化,这样程序里所有的代码地址都需要需要重新对进行计算修复才能正常访问。

代码地址都需要需要重新对进行计算修复才能正常访问。

出于这个原因,所以我们有了Rebase/Binding #####Rebase 什么是Rebase? rebasing:主要就是调整镜像内部指针的指向。 #####Binding Binding: 将指针指向镜像外部的内容。

针对这个环节的优化主要还是以下.

减少OC类的数量,合并大多数的模型类,属性,CATEGORY
使用C++ Virtual 的虚函数
使用Swift 的结构体

为了缩短启动时间 可以采用的方法有 减少已有dylib的数量.

减少已有ObjC类的数量 以及删除静态初始化器

还有可用更多Swift语言加快速度 因为Swift真的很强大 Swift有全局变量 并且会被初始化 它们确保在使用前被初始化 但是其方法不是用初始化器 在后台 使用一次dispatch_once() 使用了一种调用点初始化器 所以转为Swift语言 将会做到这一点

最后 不鼓励使用dlopen() 它会带来细微的性能问题 很难诊断

使用Instruments 去检测启动时间 screenshot.png

dyld 的预绑定 我们使用预绑定技术 为系统中的所有dylib和你的程序 找到固定地址 动态加载器将会 加载这些地址的所有内容 如果成功 将会编辑 所有这些二进制数据 以获得所有预计算地址 然后下次 当它将所有数据放入相同地址时 不必进行任何其它额外的工作 这会大幅提高速度

但是这也意味着 每次启动时会编辑你的二进制数据 这并不是很好的做法 至少从安全性来说是如此

##dyld2 dyld 2是dyld的 完全重写版本 正确支持C++初始化器语义, 扩展mach-o格式 并且更新dyld 从而获得高效率的C++库支持 它具有完整的本机dlopen 和dlsym实现 具有正确的语义 弃用了旧版API 这些旧版API仍然位于macOS中 没有加入到任何其它平台上 dyld的设计目标是提高速度

##dyld3

image.png

dyld3的特点是进程外的且有缓存的,启动app前把很多耗时操作提前处理好了。据统计,在冷启动时,dyld3比dyld2快20%。

Dyld3分为out-of-process,和in-process。

out-process会做:

分析Mach-O Headers 分析以来的动态库 查找需要的Rebase和Bind的符号 将上面的分析结果写入缓存。

in-process会做:

读取缓存的分析结果 验证分析结果 加载Mach-O文件 Rebase&Bind Initializers

dyld的东西比较深奥,建议大家再多去找找资料,dyld3核心就是在app启动之前就已经将二进制加入到缓存中,从而提升速度的.

这里差不多也看完了2017 session 的启动优化, 核心是dyld3的优化,能够在开发者这边操作的注意事项与2016 大多数也是通用的 1.减少动态库的依赖 2.减少指针的使用。(指针地址对齐的操作消耗时间)

  1. 优化Objc建立时间。这一步包含四个步骤: 3.1注册类 3.2更新类实例变量偏移(例如SDK更新) 3.3注册Category 3.4 选择器的唯一性

4: 初始化。在iOS平台下,如果项目使用Objc编写,尽量少使用+load方法,如果非要使用, 替换为+initialize,延迟加载。如果项目已经使用Swift编写,那就没什么优化的了,Apple暗地里帮我们调用了他们自己的initializer(dispatch_once), 确保Swift class不会被初始化多次。

关于优化第一帧

优化个人业务 在最小化工作时 应该推迟与 生成第一帧无关的任何内容 这意味着推迟 未显示的视图或尚未使用的 预加热功能等内容 还应该避免阻塞主线程 比如网络 I/O 文件 I/O 或其他 因为这会影响启动 将其移动到后台线程 应该注意 减少内存使用量分配和操作 内存可能需要时间 接下来 优先工作

使用Instrment 去检测UI

screenshot.png

如图所示,根据时间定位耗时的代码.

[图片上传中...(screenshot.png-85bb1a-1614424149540-0)]

本篇是自己看完wwdc的笔记式的内容.三篇session部分内容重复, 后面有时间再进行整理. 多谢观看