译者:本文是 Facebook 应用的开发工程师在发现应用运行中屡禁不死的崩溃问题,通过逐步地调查和研究崩溃发生的时间点,提出了自有的解决方案,虽然文中并没有很详细的解决方案,但是对于如何排查和解决问题的方式以及对应用性能严谨的态度,是很值得我们借鉴的。
在 Facebook,我们一直致力于让应用稳定、快速、可靠。在 Facebook 的 iOS 应用上,我们已经做了很多工作去减少应用的崩溃率以及全面提高应用的稳定性。此前,大多数的崩溃都是由于常规性错误,一般都会伴随着相应代码行的栈回溯信息,并且提供了可能导致问题所在的提示信息。
当我们继续解决崩溃问题时,我们观察到需要解决的崩溃比例正在下降,但是我们注意到 App Store 指出社区继续出现令人失望的应用崩溃。我们深入研究了用户报告,并且从理论上说明内存不足(out-of-memory events (OOMs))可能正在发生。OOMs 一般发生在系统运行在低内存的环境下,OS 为了回收内存而终止应用。它既可能发生在前台,也可以是后台。我们在内部称之为 FOOMs 和 BOOMs — 当我们说应用爆炸(BOOM)了,好像很好玩的样子。
从用户的角度来看,一个前台内存不足导致的崩溃和常规的崩溃是不好分辨的。一般分为几种情况,应用异常终止,似乎消失,以及用户返回设备主屏幕。如果内存的消耗速度急速增长,那么应用会在不接到任何通知的情况下被终止掉。在 iOS 中,OS 会将内存警告发给应用,但是不能保证 OS 一定会在终止应用之前给应用发送警告信息。这就导致我们无法轻易地知道应用是否是由于内存压力而被 OS 终止。
分析问题
为了掌握应用由于 OOM 崩溃而终止的频率,我们从所有已知的途径列举应用可能终止的情况并记录他们。这样问题就转变为“导致应用重启的是什么?”
应用需要重启的原因如下:
- 应用已经更新
- 应用退出或终止
- 应用崩溃
- 用户强制退出应用
- 设备重启(包括 OS 升级)
- 应用在前台或者后台内存不足(OOM)
通过排除处理,寻找区别于其他重启原因的实例,借此我们可以找出 OOM 发生的时间。此外,我们还追踪应用进入后台和前台的时间,借此我们可以精确地把 OOMs 分为 BOOMs 和 FOOMs。
日志显示在设备处于低内存状态下,有很高的比率发生 OOMs 。当应用进程在受内存限制的设备上像驱逐一样被终止,真的非常令人沮丧。查看相关的日志记录帮助我们验证排除法的效果,并且能继续提高日志记录(我们无法准确验证所有的事例,例如应用升级)。
我们最初在减少 OOMs 所做的努力,是试图在应用不再需要内存时,就尽可能快地主动缩小应用的内存占用。不幸的是,我们没有发现 OOM 崩溃的数量没有有切实的改变,所以我们把关注点转移到大的内存分配上,开始观察那些可能被泄露的内存(没有清理干净的),尤其是潜在的循环引用。
内存使用分析
当我们开始解决内存泄露问题时,我们看到 OOM 崩溃率有所降低,但是依然没有达到我们预期。紧接着,我们深入研究 Apple 的 Instruments 应用的 memory profiler,并且注意到只要应用打开任何 web 网页,一个重复样式的 UIWebView
就会分配大量的内存。我们还发现内存经常没有回收,即使在用户离开了网页并且 web 视图被关闭的情况下。
我们试图做过大量的优化,例如清理缓存和内容,但是应用进程的内存占用在跳转向 web 视图时总是显著增长。iOS包含一个新的类 — WKWebView
— 它把大多数的工作都放在了分开的进程里,这意味着大多数跟内存相关的 web 视图使用将不会分配给我们的进程。在低内存的事件中,web 视图的进程将会被终止,但是我们的应用有很大可能会继续存活下去。在我们把应用迁移为 WKWebView
后,我们确切地看到 OOMs 发生的比率有了显著的降低。Yay!
内存分配比率
当通过 Instruments 分析内存使用时,我们还发现应用中分配了大量的临时内存(~30 MB),然后马上释放掉。如果 CPU 在这个分配过程中是空闲的,那么 OS 会终止程序。我们要禁止此类临时分配,这可以帮助我们在 30% 确定场景中减少 OOM 崩溃,我们还实验并发现,相较于重复分配和释放内存,分配一次然后管理内存对于应用的可靠性是更好的。
阻止内存恶化
即使用了 WKWebView
,我们仍然发现一点点内存泄露都能够显著地导致影响 OOM 的发生比率。在我们通常的发布计划和贡献给应用的许多的团队中,在发布的应用中捕获和阻止内存泄露是非常重要的。我们改变了扫描设备,独创性地设计了用于测试移动性能,为了记录大量进程中的常驻内存,允许扫描设备去标记恶化情况,只要它们被添加了。这已经帮助我们把 OOM 发生比率保持在比最初解决问题时低得多的水平上。
应用内部的内存分析器
上一个在这个项目中我们使用的关键技术是去构造一个应用内部的内存分析器,通过追踪所有的 Objective-C 对象的内存分配进而快速分析应用。我们把这个配置在扫描仪上,然后在里面建立我们的应用。
它是如何工作的:对于系统中的每一个类,维护一个当前活动的实例的数量。我们可以在任何点要求它打印出每一个类对象的现存数目。然后我们就可以分析这些数据任何异常的 release-to-release 用以辨认我们应用中总体上的内存分配模式,如果计数急剧变化,这一般可以验证为内存泄露。我们准备去用一种性能足够用并且不会产生对用户有影响的方法去实现。
下面简要说明我们的策略以及我们是如何追踪 NSObject 的内存分配。
我们一开始创建一个内存分配追踪类。这是个超级直接和简单的类,有统计实例数量的公共方法用于统计实例数量的增加和减少。我们使用 C++ 而不是Objective-C,是由于那样可以最小化追踪器的内存分配和 CPU 占有率。
class AllocationTracker {
static AllocationTracker* tracker();
void incrementInstanceCountForClass(Class aCls);
void decrementInstanceCountForClass(Class aCls);
std::vector<std::pair<Class, unsigned long long>> countsSnapshot();
...
}
然后我们可以使用 iOS 的方法调配技术(称为“swizzling”,使用runtime 的 class_replaceMethod
方法),用- fb_originalAlloc
和 -fb_originalDealloc
方法去替换标准的iOS方法 +alloc
和 +dealloc
。
然后我们用新实现的增加和减少的分配和释放实例数量的方法相应地替代 +alloc
和 +dealloc
。
@implementation NSObject (AllocationTracker)
+ (id)fb_newAlloc
{
id object = [self fb_originalAlloc];
AllocationTracker::tracker()->incrementInstanceCountForClass([object class]);
return object;
}
- (void)fb_newDealloc
{
AllocationTracker::tracker()->decrementInstanceCountForClass([object class]);
[self fb_originalDealloc];
}
@end
然后,当应用运行时,我们可以调用快照方法有规律地打印当前存活实例的数量。
应用可靠性
一旦我们在 Facebook 的 iOS 应用中实施更改去解决内存问题,我们会看到 (F)OOMs 和用户的应用崩溃报告有显著的降低。OOM 崩溃对于我们来说是盲点,因为没有正式的体系或者 API 可以随意检测到它们。没有人喜欢一个应用突然关闭。但是使用某些工具,或者最新的 iOS 技术,以及一些灵巧的方法去解决这个问题,能够让我们的应用更加可靠,并且保证你不会在打开 web 视图查看一篇有趣的文章(就像你在看的这篇文章)时突然关闭。
Additional thanks to Linji Yang, Anoop Chaurasiya, Flynn Heiss,
Parthiv Patel, Justin Pasqualini, Cloud Xu, Gautham Badrinathan, Ari
Grant, and many others for helping reduce the FOOM rate.