1.App启动流程分析
iOS App启动时,系统会调用fork和execve,fork功能创建app进程,execve加载和运行程序,然后进入Pre-main阶段,然后进入applicationDidFinishLaunching:,下面我们梳理下app启动到展示的过程:
启动app->初始化空间,创建进程->加载解析执行文件exec_activate_image->载入动态链接器load_dylinker->配置用户栈等环境->设置线程入口->加载依赖的共享库->查找并绑定符号->恢复app的入口->mian()->UIapplicationMain()—>UIapplication->appdelegate->loads info.plist->creates and manages runloop->sends applications:didFinishLaunchingWithOptions:->creates and displays
2.Pre-main优化
通过配置Environment Variables
参数DYLD_PRINT_STATISTICS:1
,可以查看Pre-main时间,我们先查看下优化之前的数据
Total pre-main time: 2.2 seconds (100.0%)
dylib loading time: 1.1 seconds (51.2%)
rebase/binding time: 101.93 milliseconds (4.4%)
ObjC setup time: 31.59 milliseconds (1.3%)
initializer time: 981.14 milliseconds (42.8%)
slowest intializers :
libglInterpose.dylib : 422.59 milliseconds (18.4%)
AFNetworking : 236.77 milliseconds (10.3%)
...
可以看到时间主要花在dylib loading
、rebase/binding
、ObjC setup
、initializer
下面我们分析下怎样去优化这几个阶段的时间消耗
- dylib loading
动态库加载,在这个阶段,系统的动态库做了高度优化,自定义动态库则需要花费更多的时间,所以合理使用自定义动态库资源 - rebase/binding
加载到内存的可执行文件都是不可用的状态,因为有ASLR的存在,在虚拟内存中的地址都是随机的,所以需要fix-ups,主要两个步骤:rebase ->binding
rebase:因为初始地址和内存地址不同,需要修正.
binding:因为动态库不编译进程序最终的二进制文件中,而是在运行的时候动态的查找调用函数的地址,调用外部符号进行绑定的过程就称作binding.
以上两个步骤主要针对的就是__DATA中的指针数量,所以这个阶段需要优化类
、方法
- ObjC setup
数据修正后将会注册objc类,如果有分类,还需要将分类定义的方法插入的方法列表中
Objc setup主要是在objc_init完成的,objc_init是在libsystem中的initialize方法libsystem_initializer中初始化了libdispatch,然后,libdispatch_init调用了os_object_init,最终调用了objc_init,runtime在objc_init中绑定了3个方法,map_2_images
,load_images
,unmap_images
.-
map_2_images
:Binding操作结束之后,发出dyld_image_state_bound通知,调用map_2_images,主要做以下几件事来完成objc setup:
-- 读取二进制文件的DATA段内容,找到与objc相关的信息.
-- 注册objc类
-- 确保selector的唯一性
-- 读取protocol以及category的信息 -
load_images
:函数作用就是调用objc的load方法,它监听dyld_image_state_dependents-initialize通知 -
unmap_image
可以理解为map_2_images的逆向操作.
以上3步都是修改__DATA segment中的内容,所以这个阶段需要优化load
、分类
-
经过以上几个步骤的初步优化,比如移除冗余文件、冗余方法、load优化、分类优化,我们再看看pre-main消耗
Total pre-main time: 1.5 seconds (100.0%)
dylib loading time: 733.71 milliseconds (46.0%)
rebase/binding time: 91.76 milliseconds (5.7%)
ObjC setup time: 28.08 milliseconds (1.7%)
initializer time: 738.51 milliseconds (46.3%)
slowest intializers :
libglInterpose.dylib : 375.18 milliseconds (23.5%)
AFNetworking : 57.57 milliseconds (3.6%)
...
可以看到pre-main减少0.7s,效果还是比较不错的
3. applicationDidFinishLaunching优化
本片文章并不是讲冷启动的耗时优化,而是在进入applicationDidFinishLaunching方法到rootViewController展示的过程,为了尽快让用户能看到app首页,我们需要尽量缩短applicationDidFinishLaunching方法的耗时,尽快加载出rootViewController数据,为此,我们从Demo中来看看,applicationDidFinishLaunching耗时到底能否影响rootViewController的展示
首先在app入口处我们获取启动时间
image.png
在rootViewController中获取appear时间
image.png
查看结果:
image.png
然后我们在applicationDidFinishLaunching中插入一个耗时操作
image.png
查看结果
image.png
可以看到,applicationDidFinishLaunching在主线程中耗时越多,rootViewController出现的就越慢
为了提升用户体验,缩短app启动白屏等待的时间,我们可以尽量将耗时操作延迟执行、异步子线程执行、优化耗时时间等
4.结合Runloop管理启动项
- 在做App优化的过程中,我设计了一个基于runloop的任务管理器,利用runloop的空闲状态执行任务GLRunloopTaskTool
- 我们可以将需要执行,但是又不是必须立即执行的任务用这个工具管理,比如某些UI渲染、某些数据加载等等
5.Instruments之App Launch
image.png在xcode11之后,Instruments中多了一个App Launch
,这个在优化分析app启动过程有非常直观的作用
-
运行该工具,会启动app并统计5s内app启动过程中所做的事情,之后解析数据,上图是我运行
App Launch
的一次分析结果 -
我们选中
.app
,选中Time Profile
,这样可以看到不同阶段的统计,我们重点关注下绿色部分Launching
过程,该部分由以下几个部分组成:- Launching-UIKit initialization
- didFinishLaunchingWithOptions()
- UIKit Scene Creation
- initial Frame Rendering
即:相关UIKit初始化、DidFinishLaunchingWithOptions
执行,创建初始画面,这正是我们优化app启动过程中重点关注的阶段
-
3连击其中一个阶段可以,可以看到该阶段所调用的方法及耗时,还可以勾选
image.pngCall Tree
中的,Separate by Thread
和Hide System Libraries
方便查看
-
App启动优化工作和App瘦身工作,在代码优化方面有很多重合工作,在App瘦身工作落地后,App启动优化阶段,我主要做了
didFinishLaunchingWithOptions
及initial Frame Rendering
优化,release环境下调试 -
从
App Launch
统计的Launching
阶段数据来看,从之前的655ms
优化到了245ms
-
启动阶段总的MainThread耗时从
1.16s
优化到了0.54s
网友评论