性能优化之路
我在开发 iOS 的过程中,逐渐形成了一些对性能优化的认识,准备整理出来。恳请各位斧正。
在我的眼中,App 的性能就像是 App 运行过程中的货币。我们开发者就像一个组织者,手机本身的性能就是可以提供给我们的演出经费,手机版本就像舞台。
出现了新的可以增加性能的技术的时候,我们就要想办法多弄点钱;有些演员价钱过高,我们要考虑是否换个人;有的设备过于陈旧了,我们要考虑是否继续优化或者干脆换一种方案;为了得知哪些地方花得过多了,我们则需要想办法去监控“花钱”的过程。性能优化到最后的结果,就仿佛在有限的资金支持下,给观众表演出一场精彩的、没有失误的戏剧。
这篇不打算直接讲某一个具体优化技巧,而是想先把我对 iOS 性能优化这件事的整体理解整理出来。
优化有几个关键点要记住
- 不要过早的优化 一方面,现在 iOS 平台的整体性能普遍还不错,如果花费过多精力过早优化,反而可能影响当前开发任务。
- 不要过度的优化 我这里举一个我曾经犯过的错误。我曾经遇到过一个实现高精度倒计时的任务,我在考察的过程中,发现了相对高精度的 GCD 倒计时是使用 select 函数来实现的。我就比较“丧心病狂”的用 select 函数实现了一个倒计时的单例,但是最后应用的时候发生了许多危险的线程错误。事情到了最后还是乖乖的选择了 GCD 作为我的高精度倒计时实现方案。
- 重视多角度性能监控 不言而喻,只有真正的面对到了真实发生的性能问题,我们才能针对性的做出优化,而不是浪费时间在并不重要的地方。
- 尽量减少性能监控影响 我们在使用性能监控工具的时候,往往性能监控工具本身就会影响到性能。这就仿佛是一种“观察者效应”。我们要减少这种可能危险。
- 平衡各种性能优化 App 的性能有很多种类。我们要明白,我们的目标是给观众表演出一场精彩的、没有失误的戏剧。但是在真正的场景中,由于很多客观原因,比如说手机版本过老(舞台太陈旧),手机性能不足(钱也少了),总是要做出各种各样的权衡,舍弃某些地方的“经费支持”(优化),以达到整体演出的完整。
- 理解优化过程的内部底层实现机制
假如你不知道 App 的启动过程是分为
main()之前和之后两个阶段,可能你一直优化main()之后的部分,却收效甚微;假如你不知道一个 view 从创建到展示出来的过程、CPU、GPU、RunLoop 在其中各自做了什么,你就很难做到如丝般顺滑;假如你不知道NSSet/NSDictionary底层实现了 hash,你可能就无法选择正确的集合去承载数据。
我们要优化那些方面
假设我们开发了一个 App,那么到底有哪些方向值得优化?为了知道这些方向里哪些是真问题,又需要做哪些监控?下面我先按自己的理解粗分几类,欢迎补充。
A. 启动和加载
1.启动时间优化和监控 App 的第一次打开、冷启动、热启动、App 升级后启动等等,都是可以优化的点。这里我们一般更着重优化第一种情况。通常来说,首次启动优化好了,后面几种情况也会跟着受益,不过很多时候还是要根据业务场景单独处理。
2.app 包大小的优化 App size 是一项很关键的指标,现在很多巨型应用超过
100M的比比皆是。包大到一定程度之后,只能通过 Wi‑Fi 下载;即使可以使用蜂窝网络下载,也会让人十分心疼流量。我们这时候就要分析 ipa 包大小,去除无用图片、无用代码,更换图片格式,检查第三方库依赖,合并类似方法等等。
3.网络优化 合并接口,选择合适的协议,优化
SSL/TLS握手前后的过程,减少不必要的往返和等待。
4.页面打开过程优化 我们从上一个页面进入下一个页面的时候,会经过转场动画、下载数据、绘制图片等过程。为了让用户使用起来方便,我们需要对其中的每一步都进行优化。
B. 运行时体验
5.数据读写优化 这里针对的面很多,数据库的读写,集合类的选择和读写,都是属于这一类的。
6.UI 卡顿优化 这个实际上是种种卡顿的集合,也是相对而言内容最多的一部分,我们需要结合实际状况来优化。
7.内存优化 避免内存爆炸,避免因内存过大被杀死,避免循环引用等等。
8.线程优化 这个算不算是优化我还有些疑虑,但出于很多优化方案最终都要靠多线程去落实,我还是把它放在这里。与其说这是“优化”,不如说这是我们在实际操作中必须注意的一些事情。比如:UI 的创建和刷新一定要在主线程,DB 操作、日志记录、网络回调要落到各自合适的线程,合理创建线程,并及时回收等等。
C. 长期资源消耗
9.耗电优化 耗电优化我们可以放到后面再说。相对而言,在解决了上面的很多问题之后,耗电往往也会跟着得到改善。而且这一项通常不会像“启动慢、页面卡、容易崩”那样第一时间被用户明显感知,所以可以放在后面持续处理。
监控
没有监控,优化很多时候就只能靠猜。
对 App 相关指标的监控和记录至关重要,我把这分为三种:
1.开发中实时监控 我们在开发中往往会写出很多 bug,所以在开发阶段就直接把问题报告出来,非常重要。我之前为了做这件事,实现过一个悬浮在 App 最上层、可以拖动的小按钮,实时展示 FPS,并把内存泄漏监控、卡顿监控、内存爆炸监控等工具都塞进去,打包时再从工程里扔出去即可。这样在开发过程中就能随时改,当然它本身也会带来一些副作用,但我觉得利远远大于弊。我接下来的工作,就是准备把它开源出来。
2.日志系统 有些线上发生的错误,比如应用被杀死、应用卡顿、应用崩溃等等,我们需要有工具把它记录下来并发送回来。可以选择第三方工具,但在条件允许的情况下,自己做一套往往会更贴合自己的业务。这里我有一个惨痛的教训:最好采用 mmap 的方式存储关键数据。这个我以后会专门写一篇,解释为什么这件事让我记忆深刻。
3.埋点 埋点相对而言更适合统计用户行为和业务路径,但很多时候也可以顺手承担一部分监控作用。这里我比较推荐用 AOP 的方式去做无痕埋点,尽量减少对业务代码的侵入。
总结
随着时间的推移,问题总是会被我们遇到,也总是会遇到各种性能瓶颈。我们需要想出办法,做出一些真正有用的辅助工具,帮助我们定位问题、解决问题。很多优化技巧都会过时,但“先建立性能观,再建立监控,再去做针对性优化”这条路,我觉得大体不会错。