iOS开发启动优化
优化(首页加载时间)
- 整理启动项, 总结出耗时长的模块
- 分为2部分 main函数之前耗时和mian之后耗时
- Main前面主要做一些动态库dyld的加载和load方法的调用, 对于pre-main阶段,Apple提供了一种测量方法来计算耗时时长
- 减少依赖不必要的库,无论是动态还是静态库,如果可以的话把动态库改为静态库
- 用linkmap检测出所有的方法和类,在用AppCode找出无用代码和类,删除掉,减少load方法调用
- 少在类的+load方法里做事情,尽量把这些事情推迟到+initiailize (可以减少main之前的运行时间)
- 对于main()阶段,主要是测量main()函数开始执行到didFinishLaunchingWithOptions执行结束的耗时,就需要自己插入代码到工程中了。先在main()函数里用变量StartTime记录当前时间, 最后在didFinishLaunchingWithOptions里,再获取一下当前时间,与StartTime的差值即是main()阶段运行耗时。
- 懒加载(避免在首页控制器的viewDidLoad和viewWillAppear做太多事情,这2个方法执行完,首页控制器才能显示)
- 图片压缩,图片小了,io操作量也会变小,启动就快了
- 非必要加载业务延迟去做(比如放到首页控制器的viewDidAppear方法)
- 主页不用storyboard或者xib加载, 全部改为纯代码
- 采用性能更好的API
应用启动原理流程
- 点击图标,创建进程
- mmap 主二进制,找到 dyld 的路径 (mmap 的全称是 memory map,是一种内存映射技术,可以把文件映射到虚拟内存的地址空间里,这样就可以像直接操作内存那样来读写文件。)
- mmap dyld,把入口地址设为_dyld_start (dyld 是启动的辅助程序,是 in-process 的,即启动的时候会把 dyld 加载到进程的地址空间里,然后把后续的启动过程交给 dyld;iOS 13 开始 Apple 对三方 App 启用了 dyld3,dyld3 的最重要的特性就是启动闭包,闭包里包含了启动所需要的缓存信息,从而提高启动速度。)
- 重启手机/更新/下载 App 的第一次启动,会创建启动闭包
- 把没有加载的动态库 mmap 进来,动态库的数量会影响这个阶段
- 对每个二进制做 bind 和 rebase(Rebase:修复内部指针。这是因为 Mach-O 在 mmap 到虚拟内存的时候,起始地址会有一个随机的偏移量 slide,需要把内部的指针指向加上这个 slide。Bind:修复外部指针。这个比较好理解,因为像 printf 等外部函数,只有运行时才知道它的地址是什么,bind 就是把指针指向这个地址),主要耗时在 Page In,影响 Page In 数量的是 objc 的元数据有定义,过程:MMU 找到空闲的物理内存页面 ;触发磁盘 IO,把数据读入物理内存;如果是 TEXT 段的页,要进行解密;对解密后的页,进行签名验证)
- 初始化 objc 的 runtime,由于闭包已经初始化了大部分,这里只会注册 sel 和装载 category
- +load 和静态初始化被调用,除了方法本身耗时,这里还会引起大量 Page In
- 初始化 UIApplication,启动 Main Runloop
- 执行 will/didFinishLaunch,这里主要是业务代码耗时
- Layout,viewDidLoad 和Layoutsubviews 会在这里调用,Autolayout 太多会影响这部分时间
- Display,drawRect 会调用
- Prepare,图片解码发生在这一步
-
Commit,首帧渲染数据打包发给 RenderServer,启动结束
基于启动原理优化
t(App 总启动时间) = t1(main 调用之前的加载时间) + t2(main 调用之后的加载时间),分main函数之前和main函数之后的相关方法优化
![](https://img.haomeiwen.com/i13069754/e6d149e495476c48.png)
查看耗时情况
获得 main() 方法执行前的耗时比较简单,通过 Xcode 自带的测量方法既可以。将 Xcode 中 Product -> Scheme -> Edit scheme -> Run -> Environment Variables 将环境变量 DYLD_PRINT_STATISTICS 或 DYLD_PRINT_STATISTICS_DETAILS 设为 1 即可获得执行每项耗时:
Total pre-main time: 1.2 seconds (100.0%)
dylib loading time: 147.51 milliseconds (12.0%)
rebase/binding time: 112.82 milliseconds (9.2%)
ObjC setup time: 45.94 milliseconds (3.7%)
initializer time: 919.07 milliseconds (75.0%)
slowest intializers :
libSystem.B.dylib : 6.79 milliseconds (0.5%)
libMainThreadChecker.dylib : 34.62 milliseconds (2.8%)
libglInterpose.dylib : 353.67 milliseconds (28.8%)
TCLTV : 944.10 milliseconds (77.0%)
优化实践
启动任务拆分优化
(1) 确定在展示 UI 前必须执行的任务。
如果应用是第一次启动,那么没有必要加载任何用户偏好,如主题、刷新间隔、缓存大小等。此时是没有任何自定义值的。初始缓存肆意增长也是没问题的,因为它的增长不会超过最终的限制值。崩溃报告系统应第一个被初始化。
(2) 按顺序执行任务。
排序是非常重要的,因为任务之间可能具有相互依赖性,同时,排序还可以节省用户的宝贵时间。例如,如果先触发了访问令牌的验证操作,那么其他任务可能会并行执行,因为验证过程需要进行网络连接。但是这样就会导致一种情况:如果其他任务先完成,而验证还未完成,应用就必须等待验证完成才能继续执行。
(3) 将任务拆分为两类:一类是必须在主线程中执行的任务,另一类是可以在其他线程中执行的任务 ,然后分别执行。还可以进一步将在非主线程中执行的任务分为可以并发执行的和不能并发执行的。
(4) 其他任务可以在加载 UI 后执行或异步执行。
延迟其他子系统(如记录仪和分析方法)的初始化。在应用的后续阶段将一些操作(例如,写日志消息或跟踪事件)放入队列中,直到子系统完全完成初始化。
优化方法
(1)在t1阶段加快App启动:
- 尽量使用静态库,减少动态库的使用,动态链接比较耗时,如果要用动态库,尽量将多个dylib动态库合并成一个
- 尽量避免对系统库使用optional linking,如果App用到的系统库在你所有支持的系统版本上都有,就设置为required,因为optional会有些额外的检查
- 减少Objective-C Class、Selector、Category的数量,可以合并或者删减一些OC类(怎样删减无用代码:ViewConteroller 渗透率,hook 对应的声明周期方法即可统计;Class 渗透率,遍历运行时的所有类,通过 Objective C Runtime 的标志位判断类是否被访问;行级渗透率,需要用编译期插桩,对包大小和执行速度均有损。)
- 删减一些无用的静态变量,删减没有被调用到或者已经废弃的方法
- 将不必须在+load中做的事情尽量挪到+initialize中,+initialize是在第一次初始化这个类之前被调用,+load在加载类的时候就被调用;或者做load方法迁移
- 尽量不要用C++虚函数,创建虚函数表有开销
- 不要使用attribute((constructor))将方法显式标记为初始化器,而是让初始化方法调用时才执行。比如使用dispatch_once(),pthread_once()或 std::once()
- 在初始化方法中不调用dlopen(),dlopen()有性能和死锁的可能性
- 在初始化方法中不创建线程
(2)在t2阶段加快App启动:
- 尽量不要使用xib/storyboard,而是用纯代码作为首页UI,如果要用xib/storyboard,不要在xib/storyboard中存放太多的视图
- 使用简单的广告页作为过渡,将首页的计算操作及网络请求放在广告页展示时异步进行。
- 对application:didFinishLaunchingWithOptions:里的任务尽量延迟加载或懒加载
- 不要在NSUserDefaults中存放太多的数据,NSUserDefaults是一个plist文件,plist文件会被反序列化一次
- 避免在启动时打印过多的log,少用NSLog,因为每一次NSLog的调用都会创建一个新的NSCalendar实例
- 为了防止使用GCD创建过多的线程,解决方法是创建串行队列,或者使用带有最大并发数限制的NSOperationQueue
- 不要在主线程执行磁盘、网络、Lock或者dispatch_sync、发送消息给其他线程等操作
总结
总结起来,启动速度优化就一句话:让系统在启动期间少做一些事。当然我们得先清楚工程里做的哪些事是在启动期间做的、对启动速度的影响有多大,然后case by case地分析工程代码,通过放到子线程、延迟加载、懒加载等方式让系统在启动期间更轻松些。