iOS老司机的App启动优化Tips, 让启动速度提升10%

3,958 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情

前言

  • 评价一个App是不是一款出色的应用, 第一印象很重要.
  • 这就要求我们必须把App的启动速度的优先级排的很高, 设想一个场景, *点击了App图标, 3秒过去了, 依然还卡在启动状态.*这是令用户难以接受的事.
  • 要想解决问题, 首先要对问题有个全面的认识.
  • 下面我们就App启动流程及启动优化实操层面, 做一个抛砖引玉的探讨, 如有错误, 请评论区指正, 先行谢过了:)

1. iOS启动流程分析

  • App的启动可以分为两种
  1. 冷启动(Cold Launch): 从零开始, 点击App图标启动App
  • 冷启动又可以概括为3大阶段
    • dyld
    • runtime
    • main.m
  1. 热启动(Warm Launch): App之前已经启动好了, 在内存中, 后台运行, 再次点击App图标启动App
  • App启动时间的优化, 主要是针对冷启动进行.

1.1 打印查看当前App的启动时间

  • 可以通过添加XCode环境变量打印出App的启动耗时
  • Edit scheme -> Run -> Arguments , DYLD_PRINT_STATISTICS设置为1.

1.2 App冷启动, 流程分析

  • 启动时间是用户点击App图标, 到第一个界面展示的时间.
  • 以main函数作为分水岭, 启动时间其实包含了两部分,
    • main函数之前, 分析并加载动态库, 注册需要的类, Category中的方法也会注册到对应的类中, 执行必要的初始化+load方法
    • main函数到第一个界面的viewDidAppear.
  • 所以, 优化也是从两个方面进行的, 建议先优化第二部分, 因为绝大多数App的瓶颈在自己的代码里. image.png

2. App冷启动, 启动优化策略

2.1 mian函数之前的启动优化

2.1.1 什么是dyld?

  • dyld(dynamic link editor), 是Apple的动态连接器, 可以用来装载Mach-O文件(可执行文件、动态库等).
  • App冷启动时, dyld所做的事情有哪些?
  1. 装载App的可执行文件, 同时会递归加载所有依赖的动态库.
  2. 当dyld把可执行文件、动态库都装载完毕后, 会通知runtime进行下一步的处理.

2.1.2 dyld层面的优化方向

  • 减少动态库的数量
  • 合并动态库, 比如自己写的UI控件合并成自己的UIKit
  • 确认动态库是optional还是required.
    • 如果该Framework在当前App支持的所有iOS系统版本都存在,
    • 那么就设为required, 否则就设为optional, 因为option会有些额外的检查.

2.1.3 App冷启动时runtime都做了哪些事?

  1. 调用map_images进行可执行文件内容的解析和处理
  2. load_images中调用call_load_methods, 调用所有ClassCategory+load方法
  3. 进行各种objc结构的初始化如注册Objc类、初始化类对象等
  4. 调用C++静态初始化器和__attribute__((constructor))修饰的函数
  • 到此为止, 可执行文件和动态库中所有的符号(Class、Protocol、Selector、IMP等)都已经按格式成功加载到内存中, 被runtime所管理.

2.1.4 runtime加载层面的优化方向

  • 合并Category, 如UIView+FrameUIView+AutoLayout合并成一个.
  • 将不必须在+load方法中做的事, 放到+initialize中去做.

2.2 main函数之后的启动优化

  • main函数开始执行到显示出第一个页面, 这段时间做了哪些事?
  1. 执行didFinishLaunchingWithOptions方法
  2. 初始化Window, 初始化基础ViewController
  3. 获取数据
  4. 展示给用户
  • 减少创建线程, 线程不仅有创建时的时间开销, 还会消耗内存, 每个线程大约消耗1kb的内存空间.
    • 线程创建的耗时, 区间范围在4-5毫秒, 创建线程后启动线程的耗时区间为5-100毫秒, 平均在29毫秒.
    • 这是很大的时间开销, 若在应用启动时开启多个线程, 则尤为明显. 多次的上下文切换会带来开销.
    • 在开发中避免滥用多线程.
  • 合并或者删减不必要的类/分类/函数, 类越多, 函数越多, 启动越慢.
  • 在设计师可接受的范围内, 尽量使用小的图片.

2.3 AppDelegate中的优化

  • 从AppDelegate先入手优化
  • didFinishLaunchingWithOptionsapplicationDidBecomeActive
  • 优化的核心思想就是, 能延时的延时, 不能延时的尽量放到后台去优化.
    • 日志、统计等必须在App已启动就最先配置的事件, 仍留在didFinishLaunchingWithOptions里启动.
    • 项目配置、环境配置、用户信息的初始化、推送、IM等事件, 这些功能在用户进入App首屏之前是必须要加载完的, 放到开屏广告页面的viewDidAppear里.
    • 其他SDK和配置事件, 由于启动时间不是必须的, 可以放在首屏的viewDidAppear方法里, 在这里不会影响启动时间.
    • 每次用NSLog方式打印会隐式的创建一个Calendar, 因此需要删减启动时各业务的log, 或者仅针对内测版输出log.
    • 尽量不要在didFinishLaunchingWithOptions里面创建和开启多线程.

3. 总结

  1. App的启动由dyld主导, 将可执行文件加载到内存, 顺便加载所有依赖的动态库
  2. 并由runtiime负责加载成objc定义的结构
  3. 所有初始化工作结束后, dyld就会调用main函数
  4. 接下来就是UIApplictionMain函数, AppDelegate

3.1 dyld阶段

  • 减少动态库、合并一些动态库定期清理不必要的动态库
  • 减少Objc类、分类的数量、减少Selector数量, 定期清理不必要的类、分类
  • 减少C++虚函数的数量

3.2 runtime阶段

  • +initialeze方法和dispatch_once取代所有的__attribute__((constructor))、C++静态构造器、Objc的+load方法

3.3 main函数阶段

  • 在不影响用户体验的前提下, 尽可能将一些初始化操作延迟, 不要全部都放在finishLaunching方法中, 做到延迟加载, 按需加载.

发文不易, 喜欢点赞的人更有好运气👍 :), 定期更新+关注不迷路~

ps:欢迎加入笔者18年建立的研究iOS审核及前沿技术的三千人扣群:662339934,坑位有限,备注“掘金网友”可被群管通过~