Memory warning: 大隐隐于市

对 memory warning (内存警告) 的处理是一个在开发中极其容易被忽略的问题,究其原因,最重要莫过于 warning 并不那么容易发生,开发中尤其如此。即使发生了,在我们分析崩溃日志的时候,也未必能够明察与之是否有关,再进一步说,就算知道了十有八九是 memory warning 导致了崩溃,有时候要准确定位根源仍需耗费一番功夫。

常见原因

由内存原因导致的崩溃类型多不胜数,不过在这里只打算讨论内存不足导致应用被强制终结这一种。比较常见的是下述几种:

  • 自然消耗,处理媒体资源尤其容易消耗内存。
    • 未处理 memory warning
    • 对 memory warning 的处理不正确
  • memory leak (内存泄漏),确切来说 memory leak 不是直接原因,而是一个加速器。

实例

之所以认识到这个问题的重要性,是因为最近在看崩溃日志的时候,发现有很多个原因近似,但发生场所不太相同的崩溃,不过总体来说发生率只有 0.0X%。因为夹杂着一些 RxSwift 的代码,真正发生崩溃的位置变得很难判断,真实原因更是扑朔迷离。所谓家家有本难念的经,相似的,短短代码都有说不尽的故事,所以就事论事,来说说到底发生了什么。

首先,我们有一个封装了 UICollectionViewUIViewController,也就是 BaseCollectionViewController,在其中定义了一个可以“保证”不是 nil 且不为 weakcollectionVIew

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// BaseCollectionViewController.swift
fileprivate(set) var collectionView: UICollectionView!
// ...
override func didReceiveMemoryWarning() {
if isViewLoaded && view.window == nil && collectionView.isDescendant(of: view) {
collectionView.removeFromSuperview()
collectionView.delegate = nil
collectionView.dataSource = nil
collectionView = nil
}
super.didReceiveMemoryWarning()
}

override func loadView() {
super.loadView()
// init collectionView in code
}

此外,还拥有一个继承自 BaseCollectionViewController 的 XXViewController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// XXViewController
class XXViewController: BaseCollectionViewController {
override func didReceiveMemoryWarning() {
if isViewLoaded && view.window == nil {
// ...
}
super.didReceiveMemoryWarning()
}

private func reloadData() {
// fetch data from server then reload collection view
// ...
collectionView.reloadData()
}
}

实际的运用情景是,在 XXViewController` 中存在 pop 一个新 ViewController 的情况,这个时候,XXViewController需要重新获取数据来刷新页面并更新 collectionView 的contentOffset。按道理来说,刷新数据在下一次 view will appear 的时候进行会更保险,但是由于被强烈要求调整contentOffset` 这个过程不可见,所以…这样一来,bug 就“应运而生”了。

当新 ViewController 位于最顶端的时候,XXViewController 就不再被 window 所持有。由于它继承自 BaseCollectionViewController,didReceiveMemoryWarning 发生时 collectionView 会在不知不觉中被置为 nil,如果这一切发生在 collectionView.reloadData() 之前,那噩梦就诞生了。

这个时候,或许会反思 controller view 之间的继承关系到底合理么,调整 contentOffset 不可见到底合理么,collectionView = nil 到底有没有意义。我认为这些都是合理的着手点,对于这类问题,找到原因其实就已经解决了 80% 了。

实践

未雨绸缪这句话在这里非常适用,提前去思考一些问题有助于我们规避 memory warning 带来的麻烦:

  • 当接收到 memery warning 时我们该做什么?
  • view controller 不被 window 持有的时候需要做什么?
  • 当一个 view controller 被恢复的时候,loadView, viewDidLoad 会默认被调用,但是自定义的 init 方法们就不一定了,这样仍能顺利完成初始化吗?
  • 是否存在显著的内存泄漏?

当这些问题都梳理清楚的时候,也就大大减少了后顾之忧。

除了上面提到过的 didReceiveMemoryWarning,还有一个比较常用的方法: applicationDidReceiveMemoryWarning,后者是 App delegate 的方法,所以我认为可以用来管理一些全局对象,或是缓存之类的。除此之外,还可以在必要的时候监听名为 UIApplicationDidReceiveMemoryWarningNotification 的通知来做一些特殊的处理。

触发 memory warning

那么,当准备就绪的时候,我们就可以通过触发 memory warning 来进行测试了。

  1. 模拟器 –> Debug –> Simulate Memory Warning (Shift + Command + M): 模拟器调试选项,结合断点模拟 warning
  2. UIApplication.shared.perform(Selector(("_performMemoryWarning"))): 在代码中指定位置触发 warning

最后

内存管理着实不是一个简单的问题,上述内容不过浮于浅表。如果想要稍微再多了解一点,推荐看一看 WWDC 2018 的 Session 416 ,有很多内容刷新了我的既存认知…