本篇主要介绍笔者在项目中所使用过的优化策略,如果有更好的优化策略大家可以在留言中分享。
本项目98%以上代码用Swift开发,当前版本为Swift5,并且用cocoapod管理。
编译加速
在开发中,不管是iOS还是其他,编译时间是一个很头疼的问题。
虽然确实能在编译的时候做其他事(比如摸鱼),但是毕竟还是会影响开发效率。
怎么让编译时间更快呢?
Xcode 配置
编译优化等级的基本原理是牺牲编译时性能,追求运行时的性能,在编译时删除无用代码,保留调试信息,函数内联等,因此提升打包速度的秘诀就是反其道而行之,牺牲运行时性能来换取编译时性能。对于日常测试打包,可以将 Optimize level 设置为 -O0, 表示不做任何优化, 以提升编译器的编译速度。 同时也不需要使用Whole Module模式进行编译,使用Incrememter即可。
BitCode 是由 LLVM 引入的一种中间代码,当这个属性设置为 YES 的时候,Xcode 在打包的时候,会将项目编译成很多个设备对应的安装包,这样在编译打包的时候就比较耗时,但对于内部打包不需要上传 AppStore, 因此我们可以关闭此特性以减少编译打包时间。
关闭 dSYM 生成
一般只有提交到AppStore的时候才需要dSYM生成。
Swift编译优化
Swift语言的类型推断的功能非常强大,但是稍不注意就会写出复制、或者不清晰的表达式,使得编译速度凭白增加不少。
怎么去检查编译时间的长度?
这里我使用了一个叫BuildTimeAnalyzer的工具检测。 GitHub - RobertGummesson/BuildTimeAnalyzer-for-Xcode: Build Time Analyzer for Swift
编译完成后点开log文件,可以看到各个文件中函数的编译时长排序:
通常而言,编译时间过长主要有以下几个原因:
- 类型推断。在复制的表达式中,类型推断时间过长。
let a = A + B - C + get(d)
// 可以改成
let a: Element = A + B - C + get(d)
---
let a = array.map( { e in
return A + B - C + get(e)
})
// 可以改成
let a = array.map( { e -> Element in
return A + B - C + get(e)
})
- 复杂的表达式。对于复杂的表达式应该拆开写。(类型推断的双刃剑?)
// 比如刚刚提到的let a = A + B - C + get(d) 可以改为
var a = A + B
a -= C
a += get(d)
---
// 或者
let a = array.map($0).filter($0).flatMap($0)
// 可以改为
var a = array.map($0)
a = a.filter($0)
a = a.flatMap($0)
如果不想用这个工具,也能配置其他命令,最后的300是指编译时间超过300ms的函数或者表达式,可以自定义。
-Xfrontend -warn-long-function-bodies=300
-Xfrontend -warn-long-expression-type-checking=300
代码瘦身
代码瘦身,最简单直接的方式就是把无用的代码删除。
网上找到了一个WHC_Scan的库,检查无效代码。
WHC_Scan
代码优化
对于OC文件:
用 @import 来替代 #import。
尽量使用@class 声明引用。
对于Swift文件: 尽可能的缩小访问控制范围, 对于类、方法、属性,尽可能的添加final、private等限定修饰符,这样可以使用 Static dispatch 代替 Dynamic dispatch。
对于库: 更多的使用静态库,减少第三方库的编译时间。
模块化代码,以库的方式导入。
执行main函数之前
启动App到执行main函数之前做了什么?
通过在Scheme中进行配置,DYLD_PRINT_STATISTICS和DYLD_PRINT_STATISTICS_DETAILS,可以查看在Main函数执行之前的启动时间。
Load dylibs
Dyld从主执行文件的header获取到需要加载的所依赖动态库列表,然后它需要找到每个 dylib,而应用所依赖的 dylib 文件可能会再依赖其他 dylib,所以所需要加载的是动态库列表一个递归依赖的集合。
优化方案:
- 核心思想是减少dylibs的引用
- 合并现有的dylibs(最好是6个以内)
- 使用静态库
Rebase、Bind
- Rebase在Image内部调整指针的指向。在过去,会把动态库加载到指定地址,所有指针和数据对于代码都是对的,而现在地址空间布局是随机化,所以需要在原来的地址根据随机的偏移量做一下修正
- Bind是把指针正确地指向Image外部的内容。这些指向外部的指针被符号(symbol)名称绑定,dyld需要去符号表里查找,找到symbol对应的实现
优化方案:
- 核心思想是减少DATA块内的指针
- 减少Object C元数据量,减少Objc类数量,减少实例变量和函数(与面向对象设计思想冲突)
- 减少c++虚函数
- 多使用Swift结构体(推荐使用swift)
Objc
- 注册Objc类 (class registration)
- 把category的定义插入方法列表 (category registration)
- 保证每一个selector唯一 (selector uniquing)
优化方案: 同上
Initializers
- Objc的+load()函数
- C++的构造函数属性函数
- 非基本类型的C++静态全局变量的创建(通常是类或结构体)
优化方案: 尽可能将load函数中所实现的方法延迟到initialize中。
总结
- 移除不需要用到的动态库
- 移除不需要用到的类
- 合并功能类似的类和扩展
- 尽量避免在+load方法里执行的操作,可以推迟到+initialize方法中。
- 使用swift。
- 使用静态库。
实践
简单来说,将use_frameworks!改为use_modular_headers!
为什么一开始会使用use_frameworks!?
在之前造成这个问题的原因主要是 Swift 的运行库没有被包含在 iOS 系统中,而是会打包进 App 中,静态库会导致最终的目标程序中包含重复的运行库。
在使用use_frameworks!之后,cocoapods将第三方库打包成一个动态Framework,拷贝到App中。
这种动态库隶属于 Emmbed Framework, 只在沙盒内共享,但可以与 App Extension共享。不要与系统的动态库搞混了。
在程序启动的时候,进行加载。
use_modular_headers!做了什么?
将各个第三方库打包成一个.a的静态库文件,再通过一个libPods.a的文件将所有的.a文件集合,最后在编译的时候会被直接拷贝一份,复制到目标程序中。
不需要在启动的时候再进行加载。
结果:dylib loading time: 从原先的700+ms降低到了300+ms。包的体积也减少。
执行main函数之后
didFinishLaunchingWithOptions
一般在didFinishLaunchingWithOptions中会有大量的代码:
- swizzing
- crash监控
- 埋点监控
- 注册类
- 配置信息
如何优化:
- 将重要的、优先级高的类放到willFinishLaunchingWithOptions中,如Crash监控、统计上报等,否则会导致信息收集的缺失。(优化初始化顺序)
- 将一些类的注册放到懒加载中,调用的时候再进行注册。
- 放到子线程中执行。
- 通过Time Profile进行代码耗时检测。
主要的操作就是这几种,但注意不是所有的都能优化。比如某些第三方库要求必须放到didFinishLaunchingWithOptions,又必须在主线程中加载。
界面初始化(今日头条方案)
- 纯代码方式而不是storyboard加载首页UI。
- 对didFinishLaunching里的函数考虑能否挖掘可以延迟加载或者懒加载,需要与各个业务方pm和rd共同check
- 对于一些已经下线的业务,删减冗余代码。
- 对于一些与UI展示无关的业务,如微博认证过期检查、图片最大缓存空间设置等做延迟加载
- 对实现了+load()方法的类进行分析,尽量将load里的代码延后调用。
- 对于viewDidLoad以及viewWillAppear方法中尽量去尝试少做,晚做,不做。
最后
在对于完全没有做过性能优化的工程来说,一次简单的优化能提升百分之20以上的速度。但如果已经做过优化,就只能一点一点深入地梳理应用的逻辑,比如这个组件优化40ms,那个请求优化20ms,这边通过将xib用代码重写,又优化10ms,需要一步步的积累,一个大型的app也需要整个团队的努力。
参考: