バックグラウンド
iOS 起動の最適化とは、iOS デバイス上のアプリケーションの起動速度と応答性を向上させる一連のテクノロジと方法を指します。モバイル機器の普及とアプリケーション機能の複雑化に伴い、アプリケーションプログラムの応答速度に対するユーザーの要求はますます高まっており、起動の最適化は開発者が注目する重要な領域となっています。
起動と言えば、これは実際には非常に重要なユーザー エクスペリエンスのシナリオです。初期の頃、一部のブロガーは、Android と Apple が特定のアプリを開くのにかかる時間を比較し、かかった時間で判断することでデバイス間の違いを比較することを好んでいました。特定のソフトウェアを起動する場合、両方のシステムの長所と短所があります。
もちろん、特定のシナリオではユーザーにも大きな影響を与えます。以前に遭遇したシナリオを考えてみましょう。私は何年も会っていなかった友人に会い、夕食に招待することに同意しました。支払いの時期になったとき、私の友人も急いで支払いをしました。このとき、WeChat アプリを開いて始めようとしたとき、突然動かなくなり、最終的に友人に先に支払うように頼みました。恥ずかしくないですか?
そうは言っても、Apple には起動時間が長すぎる場合にウォッチドッグ メカニズムがあり、起動時間が長すぎるとウォッチドッグによって終了されてしまいますが、これもユーザーにとって致命的です。
通常、アプリケーションが通常のワークフローの一部である場合、ユーザーはアプリケーションを 1 日に複数回起動しますが、起動時間が長いとタスクの実行に遅れが生じます。
ユーザーがホーム画面でアプリのアイコンをタップすると、iOS はアプリのプロセスに制御を渡す前に、アプリの起動の準備をします。次に、アプリケーションは UI を画面に描画する準備としてコードを実行します。アプリの UI が表示された後でも、アプリはまだコンテンツを準備しているか、インタースティシャル UI を最終コントロールに置き換えている可能性があります。これらの各ステップは、アプリケーションの知覚される合計起動時間に影響を与えます。
プロセスを開始する
最終的に何が行われたかを確認し始めるプロセス全体から始めましょう
いくつかの主要な段階に分かれています
主に以下の大きな段階に分かれていることがわかります。
1. Mach-O ステージをロードする
2.ディルドステージ
3. メインステージの後: スタートアップ項目を最後の viewDidAppear 呼び出しにロードして、最初のフレームをロードします。
dyldとランタイムの観点から分割
dyld と runtime の 2 つのコア モジュールを組み合わせると、起動フェーズは主に 2 つの連携によって完了します。
詳細な手順
全体的なプロセスは次のステップに分類できます。
1. mach-o ファイルと実行可能ファイルをカーネル モードでロードします。
mach-o ファイルについて
Mach-O ファイルの概要
- Mach object的缩写,是Mac、iOS上用于存储程序、库的标准格式 ,Mach-O文件是一种叫法,就像以 .text 结尾的文件,被叫做为text文件
常见的Mach-O文件有:
- MH_OBJECT:目标文件(.o)、静态库文件(.a) 静态库其实就是N个.o合并在一起
- MH_EXECUTE:可执行文件 .app/xx
- MH_DYLIB:动态库文件 .dylib 或 .framework/xx
- MH_DYLINKER:动态链接编辑器 /usr/lib/dyld
- MH_DSYM:存储着二进制文件符号信息的文件 .dSYM/Contents/Resources/DWARF/xx(常用于分析APP的崩溃信息)
可执行文件:
- 平时编写的代码最终会被编译成为一个Mach-O格式的文件
- 开发过程中所用到的动态库(比如:UIKit、Foundation) 依赖信息也会存储在可执行文件中
二.dyld 阶段
简单介绍下dyld是什么
"dyld" 是苹果操作系统中的一个重要组件,它是动态链接器(dynamic linker)的缩写。动态链接器是操作系统加载和链接可执行文件所需的共享库的核心组件之一。
dyld 的主要功能是在程序启动时加载和链接程序所依赖的共享库,并将其映射到进程的内存空间中。它负责解析和处理共享库之间的符号依赖关系,以及处理运行时的符号重定位。
具体来说,dyld 的工作流程如下:
- 加载:当一个可执行文件(如应用程序)启动时,dyld 负责加载可执行文件和它所依赖的共享库到内存中。
- 符号解析:dyld 解析可执行文件和共享库中的符号引用,找到对应的符号定义,以便正确地链接和运行程序。
- 符号重定位:在加载和链接过程中,dyld 会处理符号重定位,将程序中的符号引用指向正确的地址。
- 启动程序:完成加载和链接后,dyld 将控制权转交给程序的入口点,使其开始执行。
dyld 的存在使得应用程序可以动态地加载和链接共享库,从而实现了代码的共享和重用。这也是为什么在 iOS 开发中,我们可以使用各种系统提供的框架和库来构建应用程序。dyld 是苹果操作系统中负责动态加载和链接共享库的组件,它在应用程序启动时发挥着关键的作用,确保程序能够正确地加载和执行所需的代码和库。
关于动态库:
- 程序运行时由系统动态加载到内存,而不是复制,供程序调用。
- 系统只加载一次,多个程序共用,节省内存。因此,编译内容更小,而且因为动态库是需要时才被引用,所以更快。 简单认识:系统的UIKit框架最终被dyld以动态库的形式加载到内存 !
dyld阶段所做的事情
load dylibs > rebase bind > objc(Notify ObjC Runtime) > initializers
1.load dylibs
装载app的可执行文件,同时会递归加载所有依赖的动态库。
-
Parse image(解析图像):在这个步骤中,dyld 解析可执行文件或共享库的二进制格式。它会读取可执行文件的头部和段(segments),以及共享库的符号表和重定位信息等。通过解析图像,dyld 能够了解文件的结构、符号引用和重定位需求。
-
Map image(映射图像):在这一阶段,dyld 将可执行文件或共享库映射到进程的内存空间中。它会分配适当的内存区域,并将二进制文件的内容加载到这些内存区域中。通过映射图像,dyld 将文件中的代码、数据和资源加载到内存,为后续的重定位和符号绑定做准备。
当dyld加载完可执行文件 和动态库 之后通知runtime进行下一步操作。
2.Rebase + bind
-
Rebase image(重定位图像):在此步骤中,dyld 处理可执行文件和共享库中的重定位信息。重定位信息描述了代码和数据的位置相对于内存中的基地址的偏移量。dyld 根据基地址和重定位信息来计算并更新代码和数据的绝对地址,以确保它们在内存中正确定位。
-
Bind image(符号绑定图像):在这个阶段,dyld 解析可执行文件和共享库中的符号引用,并将它们绑定到相应的符号定义。符号绑定是将符号引用与符号定义相关联的过程,确保程序能够正确地访问和执行所需的符号。通过符号绑定,dyld 确保程序能够正确链接并执行依赖的函数和变量。
3.objc(Notify ObjC Runtime)
- mapimages 对二进制文件内容解析处理。
- runtime在此处初始化,对class和category进行注册。
- 进行各种objc结构的初始化(objc 类被定义和注册)。
- 分类被插入到方法列表中。
- selector唯一性判断。
4.Initializers
- loadimages 调用 call_load_images 加载 类和 分类的 load方法
- 调用c++静态初始化器和__attribute(construct)修饰的函数
至此可执行文件和动态库的符号sel class protocol IMP 都已经按需加载到内存中了,被runtime管理最后,Dyld calls main()
三.进入main函数
-
接下来就是 UIApplicationMain 函数,相关的调用了,Appdelegate会依次执行 对应的生命周期方法。
-
创建整个app的autoreleasepool,初始化初始window,app界面开始展。
-
指定rootviewcontroller,调用业务代码,完成各阶段业务。
-
main页面viewDidAppear 完成页面第一帧渲染。至此启动完成。
关于启动标准
苹果的标准
-
针对启动时间的最佳标准 (400ms 是一个很好的目标)
-
最坏的情况 (不要超过20秒否则应用程序将被杀死)
关于优化方案
优化方案从各个阶段来考虑
1.加载mach-o阶段
重新排列函数符号位置,降低MACH-O文件载入内存时PageFault缺页中断频率 - 二进制重排
- 一种是抖音的方案二进制重排。(官方说会有百分之30提升,自己尝试并没有太大提升。)
- 另外是苹果推出的pgo。(大概有百分之10左右的提升)
两者类似
原理
二进制重排实际上是在windows和linux上就存在的技术,旨在将启动用到的函数方法尽可能的放置在二进制文件加载的前面,并且是将函数符号地址连续的编译在一起,以减少Page Fault的次数和频率,加快启动速度。现在这项技术已经移植运用到了移动端app上。
操作系统为了解决安全问题和效率问题,抽象出了虚拟内存页的概念。内存都是分页访问的。这里的page指的就是内存页。(就像磁盘存储的最小单位 磁盘簇,大小是4k一样) MacOS 、linux (4K为一页) iOS(16K为一页)
PageFault就是缺页中断:当app调用一个方法,发现该方法没有在内存中,此时操作系统就会立刻阻塞整个app进程,触发一个缺页中断。操作系统会从磁盘中读取这页数据到物理内存上 , 然后再将其映射到虚拟内存上 ( 如果当前内存已满 , 操作系统会通过置换页算法 找一页数据进行覆盖, 这也是为什么开再多的应用也不会崩掉 , 但是之前开的应用再打开时 , 就重新启动了的根本原因 )。
假如,app启动时期需要调用 method1、method5和method6,这三个方法分布在page1、page2和page3上。每装载一个内存页page都会发生一次PageFault(缺页终端)。通常一个PageFault的处理时间是0.1ms~1ms,取0.5ms计算。这三次处理PageFault时间是 3 * 0.5ms = 1.5ms。
2.针对dyld阶段
-
减少动态库 合并动态库 (定期清理不必要的动态库)
-
减少oc 类 分类 方法 sel(定期清理不必要的类 分类)
-
减少c++虚函数数量
-
swift尽量使用struct
3.针对objc 和 initialize 可以看做是 runtime 阶段
- 用+initialize方法和dispatch_once取代所有的__attribute__((constructor))、C++静态构造器、ObjC的+load
4. メイン機能後のステージに向けて
- ユーザーエクスペリエンスに影響を与えないことを前提に、一部の操作を可能な限り遅延させ、すべてをfinishLaunchingメソッドに入れないようにする
- 起動タスクの順序の調整と最適化
- 初期ビューの複雑さを軽減する
- オンデマンドでロードする
テストについて (サポートされている最も遅いデバイスでのテストを推奨します)
1. 2つのGithubツールを推奨します
- github.com/ming1016/GC…(SMCallTrace)
- github.com/EmergeTools…
2. Xcode とインストゥルメントにはツールが付属しています
- lldb デバッグ ツールの設定[スキームの編集] -> [実行] -> [引数] で、環境変数 DYLD_PRINT_STATISTICS を 1 に設定すると、統計結果が印刷ウィンドウに出力されます。
- 計器による打ち上げ
3. オンラインモニタリング
アクティブ埋設ポイントはプレメインとアフターメインに分かれています
Apple の指標による
要約する
以上は基本的に iOS 起動の各段階の詳細なプロセスと最適化計画について説明しました. もちろん、起動の最適化は長期的に注意を払う必要がある安定性指標でもあります. また、段階的に最適化する必要がありますプロジェクトの現状に合わせてステップを進め、最適化→モニタリング→最適化という閉ループを形成し、問題を発見し、問題を解決することで、最終的には継続すれば必ずメリットが得られます。