【iOS】虚拟内存  物理内存 虚拟内存 物理内存早期的数据访问是直接通过物理地址访问的容易出现内存不够用、内存数据的安全问题。为了解决内存不够用的问题我们在进程和物理内存之间增加了一个中间层即虚拟内存主要用于解决当多个进程同时存在时对物理内存的管理。提高CPU的利用率使得多个进程可以同时、按需加载。这个虚拟内存本质上就是一张虚拟地址和物理地址对应关系的映射表每个进程都有一个独立的虚拟内存地址都是从0开始的大小为固定的4G每个虚拟内存又会划分为一个一个的页iOS中是16K其余4K每次加载都是按照页为单位加载的并且进程间无法相互访问。一个进程中只有部分功能是活跃的所以只需要将这些活跃的部分加入到物理内存中避免资源浪费当CPU需要访问数据时先访问虚拟内存然后通过虚拟内存去寻址查映射表的过程如果在访问时虚拟地址的内容未加载到物理内存就会发生缺页异常阻塞当前进程此时需要先将数据载入物理内存然后再寻址进行读取。按照上面的讲解虚拟内存的起始地址、大小都是固定的不安全所以苹果引入了ASLR技术iOS4.3即地址空间随机加载技术是一种针对缓冲区溢出的安全保护技术通过对堆、栈、共享库等线形布局的随机化增强安全性。但是由于ASLR的存在导致可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定所以需要在编译时来修复镜像文件中的资源指针使指向正确的位置。所以正确的内存地址 ASLR地址 偏移值可执行文件不同操作系统其可执行文件的格式也不同系统内核读取文件到内存之后根据可执行文件的头签名判断二进制文件的格式然后决定使用什么方式加载和执行它通用二进制文件苹果中的通用二进制格式就是将多种架构的Mach-O文件打包然后根据自己CPU平台选择合适的Mach-O所以通用二进制格式也被称为胖二进制格式。可以同时存储多种架构的二进制指令使得CPU可以根据自身自动检测并选用合适的架构以最理想当方式进行读取。缺点是会占用大量的磁盘空间不相干的不会占用。Mach-O即Mach-Object文件格式的缩写用于可执行文件、动态库、目标代码的文件格式具有较强的扩展性以及更快的符号遍访问速度。Executable可执行文件Dylib动态链接库Bundle无法被链接的动态库只能在运行时使用dlopen加载image指的是executable、Dylib、Bundle的一种Framework包含dylib、资源文件和头文件的集合一个Mach-O文件主要分为三大部分header Mach-O头部存储Mach-O的cpu架构文件类型以及加载命令等信息Load Commands 加载命令描述了文件中数据的具体组织结构不同的数据类型使用不同的加载命令表示其中记录了很多信息例如动态链接器的位置、程序的入口、依赖库的信息代码的位置、符号表的位置之后等等Data数据数据中的每个段的数据都保存在这里与ELF文件中段的概念类似每个段都有一个或者多个部分放置了具体的数据与代码APP启动启动广义上的启动是点击图标到首页数据加载完成狭义上的启动是点击图标到启动图完全消失的第一帧启动的最佳时间是400ms以内因为从点击图标到显示launch screen到launch screen消失的时间间隔是400ms启动时间如果超过20s就会被系统杀掉。启动的计算方式起点进程创建时间终点第一个第一个CA::Transaction::commit()Core Animation提交事务的函数收集如修改frame、bounds、alpha、transform触发布局、动画刷新等不会立马刷新而是等到当前runloop快结束时系统统一调用启动分为冷启动和热启动冷启动系统中没有任何进程的缓存信息如重启手机后直接启动APP热启动从后台再次进入APP时就是热启动因为进程缓存还在杀掉App进程之后数据仍然存在时的启动我们所说的启动优化一般指的是冷启动情况下的启动主要分为两个部分pre-main阶段即main函数执行之前操作系统加载App可执行文件到内存执行的一系列加载链接等工作点击App - 系统创建进程 - 加载主Mach-O和dyld - dyld接管启动 - 加载依赖动态库 - rebase、bind符号修正 - objc runtime 注册类、方法、Category - 执行 load、constructor、C 静态初始化 - 调用 main() - UIApplicationMain() - 首屏渲染加载链接库从主文件的header获取到需要加载的所依赖动态库列表kernel加载时期而headre早就被内核映射过。dyld根据header中的记录找到每个依赖的dylib文件并判断是否是Mach-O类型,如果类型符合就找到代码签名并将其注册到内核中每个dylib包含若干个segmentdyld会对每个segment调用mmap()将文件映射到进程的虚拟内存中一个 dylib 本身可能依赖其他 dylib例如 UIKit 依赖 CoreFoundation。dyld 会递归解析依赖直到所有依赖树都被加载。系统 dylib 使用 dyld shared cache快速映射App 自带 dylib 需要从 App Bundle 打开和 mmap加载总数通常 100–400 个实际占用时间主要来自 App 自带和三方库。修正在加载所有的动态了链接库之后他们只是处在相互独立的状态需要将他们进行绑定即fix-ups。由于代码签名限制不能让一个dylib调用另一个dylib这时需要加很多中间层。现代code-gen被叫做动态PICposition independent code意味着代码可以被加载到间接的地址上当调用发生时code-gen实际上会在_DATA段上创建一个指向被调用者的指针然后加载指针并跳转过去所以dyld做的事就是修正指针和数据。这里fix-up包含两个部分rebease-binding启动优化pre-main阶段这个阶段主要就是dyld加载而dyld启动的第一步就是加载动态库由于缓存的存在加载系统的动态库速度较快而加载内嵌的动态库较慢所以我们可以通过减少动态库的数量实现提高启动效率。我们也可以通过合并动态库的方式但是不适合个人开发者我们也可以在rebase-binding阶段尝试优化对于OC开发来说主要的时间消耗在Class/Method的符号加载上我们可以通过下面方式实现优化减少__DAtA段中的指针数量Mach-O__DATA段中保存类、方法、selector、protocol、Category 指针。指针越多dyld rebase 和 bind 越耗时合并分类和功能相似的类减少无用的方法和类使用initialize代替load方法减少__attribute__((constructor))的使用constructor 在 main 前执行类似 load也会增加启动负担。不要创建线程创建线程本身会消耗 CPU、内存并增加调度开销。启动早期线程过多可能导致锁竞争、CPU抢占、首屏渲染策略main阶段延迟初始化不必要的UIViewController。能延迟的就延迟如一些SDK的初始化、界面的创建。不能延迟执行的尽量放在后台线程执行如数据的读取、日志发送等。启动优化总结pre-main阶段减少动态库数量合并不必要的自家动态库减少第三方动态 Framework。对无动态加载需求的库优先考虑静态链接。减少 main 前自动执行代码避免在 load、constructor、C 全局对象初始化中执行耗时任务。将初始化逻辑改为懒加载、dispatch_once 或 didFinishLaunching 后延迟执行。精简 Objective-C 元数据删除无用类、方法、Category、Protocol。合并过度碎片化的 Category减少 Runtime 扫描和方法合并成本。降低动态性Swift 中避免过度 objc 暴露和 NSObject 继承。能使用 final、struct 的地方尽量使用减少动态派发和 Runtime 元数据。控制 Method Swizzling避免启动期大量 swizzle。必须 swizzle 的逻辑应尽量轻量或延迟到对应子系统启用时执行。main阶段轻量化首屏rootViewController 保持轻量。延迟创建非首屏控制器、大型视图树和复杂模块。避免主线程重任务不在主线程做同步 I/O、网络请求、大 JSON 解析、图片解码、数据库操作。非首屏任务放后台队列并在首帧后分批执行。SDK 延迟初始化只保留启动必要 SDK。支付、分享、地图、广告、IM 等非首屏能力按需初始化。优化布局Auto Layout 约束保持完整闭环。减少首帧前反复 updateConstraints 和 layoutSubviews。降低首屏视图层级和布局复杂度。优化首屏资源图片尺寸与展示尺寸匹配。减少首屏大字体、大图片、大配置文件。首屏必要资源本地化、小体积化避免远程依赖。启动流程总结从用户点击App图标开始系统先创建进程按照info.plist做基础配置校验建立沙箱然后使用launchScreen.storyboard显示启动画面然后进入dyld阶段将可执行文件和依赖的动态库从共享缓存中加载进来执行ASLR符号binding随后运行时初始化OC注册类与分类执行C/C的全局构造器和Objc的load方法。进入main之后调用UIApplicationMain创建UIApplication启动主线程RunLoopiOS13以后通常走UIScene在scene:willConnectTo:方法中配置window和根式图控制器首个控制器会依次触发loadView、viewDidLoad、viewWillAppear动画结束之后调用viewDidAppear,然后系统移除启动图呈现首帧动画。