刚接手一个项目,发现打开app后运行xcode会在编译后发生短暂的卡死现象,且app启动之后xcode也会有短暂的卡死现象,变现为:一直转圈圈。
这个问题肯定出在项目配置上,因为是刚启动就卡死,且我的机子是最新款,32内存。大概有了思路就开始定位。
先回顾下app的冷启动流程。
APP冷启动
加载dyld到App进程
内核会 fork 一个进程,execve 开始加载。检查 Mach-O Header。随后加载 dyld 和程序到 Load Command 地址空间,通过 dyld_stub_binder 开始执行 dyld
加载动态库(包括所依赖的所有动态库)
具体步骤如下:
加载dylib 分析每个dylib(大部分是iOS系统的),找到其Mach-O文件, 打开并读取验证有效性,找到代码签名注册到内核, 最后对dylib的每个segment调用mmap()。
一般动态库都是苹果自己的;
注意:如果想针对这一步做优化,因为系统其实对系统库的加载已经做过优化,所以针对这一步骤的优化我们可以做以下尝试:
减少非系统库的依赖。
合并已有的dylib和使用静态库(static archives),减少dylib的使用个数。
Rebase
dylib加载完成之后,它们处于相互独立的状态,需要绑定起来。
在dylib的加载过程中,系统为了安全考虑,引入了ASLR(Address Space Layout Randomization)技术和代码签名。
由于ASLR的存在,镜像(Image,包括可执行文件、dylib和bundle)会在随机的地址上加载,和之前指针指向的地址(preferred_address)会有一个偏差(slide),dyld需要修正这个偏差,来指向正确的地址。
Rebase在前,Bind在后,Rebase做的是将镜像读入内存,修正镜像内部的指针,性能消耗主要在IO。优化该阶段的关键在于减少__DATA segment中的指针数量。我们可以优化的点有:
减少Objc类数量, 减少selector数量。
减少C++虚函数数量
转而使用swift stuct(其实本质上就是为了减少符号的数量)
ObjC setup time
大部分ObjC初始化工作已经在Rebase/Bind阶段做完了,这一步dyld会注册所有声明过的ObjC类到一张全局表,将分类插入到类的方法列表里。
这个步骤的优化和前面的优化点差不多,所以我们无需在多做优化。
initializer time
到了这一阶段,dyld开始运行程序的初始化函数,调用每个Objc类和分类的
+load
方法,调用C/C++ 中的构造器函数(用attribute((constructor))修饰的函数),和创建非基本类型的C++静态全局变量。Initializers阶段执行完后,dyld开始调用main()函数。
所以我们针对这一步骤的优化点是:少在类的+load方法里做事情,尽量把这些事情推迟到+initiailize
减少构造器函数个数,在构造器函数里少做些事情
减少C++静态全局变量的个数
main time
在这一阶段的优化在于减少
didFinishLaunchingWithOptions
的工作量,所以我们的重点在于给didFinishLaunchingWithOptions
’减负’:- 我们知道在使用很多第三方库的时候,很多时候第三方库都推荐在
didFinishLaunchingWithOptions
完成初始化。但其实我们找到可以延迟加载的库,做延迟加载处理。比如推迟到最先显示的控制器上,或者异步延后加载。 - 梳理业务逻辑,把可以延迟执行的逻辑,做延迟执行处理。比如检查新版本、注册推送通知等逻辑。
- 避免在首页控制器的viewDidLoad和viewWillAppear做太多事情。
- 我们知道在使用很多第三方库的时候,很多时候第三方库都推荐在
示例:xcode -> product->scheme->run->arguments->enviroment Variables 添加DYLD_PRINT_STATISTICS : 1
Total pre-main time: 401.36 milliseconds (100.0%)
dylib loading time: 186.35 milliseconds (46.4%)
rebase/binding time: 10.47 milliseconds (2.6%)
ObjC setup time: 24.21 milliseconds (6.0%)
initializer time: 180.17 milliseconds (44.8%)
slowest intializers :(这个列出的是最慢的几个dylib文件。)
libSystem.B.dylib : 5.69 milliseconds (1.4%)
libMainThreadChecker.dylib : 25.29 milliseconds (6.3%)
那么回到我们遇到的问题,应该是initializer time。
经过分析发现,删除某个模块将会相应减少卡死时间,但又不是特定删除某个模块就解决了,也就是说,没有一个模块能逃脱的了干系,那就是全局的影响。项目配置看了下没啥问题,那就剩下prefix文件没看了。果然,prefix文件里有很多的头文件,还有造成头文件循环引用的。。。但是这个也不至于卡死,难道太多会造成这样?
慢慢解耦掉,发现*initializer time在不断减少,但是卡死现象还是有,继续。
知道删除了masonry导入后,发现卡死现象直接没了。我又把其他头文件恢复,测试只删除masonry,也是直接解决了卡死现象。那这就奇怪了,为啥masonry影响这么大呢?
但是其他项目试着在pch导入masonry,没有这么卡死的现象,仅这个项目有,虽然解决了,但是原因暂时还是没发现。
看下masonry源码。
问题1:这是应用一对一听课或一对多听课吗?
答:此应用中的上课模块,是一对多的,文字方式的授课。
问题2:应用程序提供直播在线听课或录制的视频课件吗?
答:不提供直播在线听课或者视频课件。文字形式(点击可播放文字对应的音频)的听课。效果如下附件图2-1.jpg。
问题3:目标受众是谁?
答:公司内部员工。可在“我的”页面点击“加入企业”输入邀请码BlmXnpJl以加入企业,如图3-1.jpg(测试账号已经加入过企业)。
问题4:你的应用程序访问任何付费内容或服务吗?
答:此应用不访问任何付费内容或服务,全部免费。
问题5:描述应用程序中任何付费内容或服务,以及支付方式。
答:此应用不不涉及任何付费内容或服务,无支付。
问题6:是个人客户为内容或服务付费,还是公司为其用户购买内容或服务?
答:此应用不不涉及任何付费内容或服务,无支付,输入邀请码即可加入本公司学习内容。加入方式见图3-1.jpg。
问题7:这个应用程序是用于一个公司还是多个客户公司?
答:一个公司。
问题8:该应用程序是为某一特定公司的内部使用(员工、合作伙伴等)吗?
答:是的。
问题9:用户如何取得帐户?
答:用户自行创建账号,随后在“我的”页面点击“加入企业”输入邀请码BlmXnpJl以加入企业,需要后台审核确定为本公司员工后通过,如图3-1.jpg(测试账号已经加入过企业)。
问题10:创建帐户是否涉及费用?
答:不涉及费用,全部免费。
问题11:这个应用程序将主要在哪些国家发行?
答:主要,且仅在中国发行。
mach-o
fishhook
dyld