Memory warning: 大隐隐于市
对 memory warning (内存警告) 的处理是一个在开发中极其容易被忽略的问题,究其原因,最重要莫过于 warning 并不那么容易发生,开发中尤其如此。即使发生了,在我们分析崩溃日志的时候,也未必能够明察与之是否有关,再进一步说,就算知道了十有八九是 memory warning 导致了崩溃,有时候要准确定位根源仍需耗费一番功夫。
常见原因
由内存原因导致的崩溃类型多不胜数,不过在这里只打算讨论内存不足导致应用被强制终结这一种。比较常见的是下述几种:
- 自然消耗,处理媒体资源尤其容易消耗内存。
- 未处理 memory warning
- 对 memory warning 的处理不正确
- memory leak (内存泄漏),确切来说 memory leak 不是直接原因,而是一个加速器。
实例
之所以认识到这个问题的重要性,是因为最近在看崩溃日志的时候,发现有很多个原因近似,但发生场所不太相同的崩溃,不过总体来说发生率只有 0.0X%。因为夹杂着一些 RxSwift 的代码,真正发生崩溃的位置变得很难判断,真实原因更是扑朔迷离。所谓家家有本难念的经,相似的,短短代码都有说不尽的故事,所以就事论事,来说说到底发生了什么。
首先,我们有一个封装了 UICollectionView
的 UIViewController
,也就是 BaseCollectionViewController
,在其中定义了一个可以“保证”不是 nil
且不为 weak
的 collectionVIew
:
// 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
:
// 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 来进行测试了。
- 模拟器 --> Debug --> Simulate Memory Warning (
Shift + Command + M
): 模拟器调试选项,结合断点模拟 warning UIApplication.shared.perform(Selector(("_performMemoryWarning")))
: 在代码中指定位置触发 warning
最后
内存管理着实不是一个简单的问题,上述内容不过浮于浅表。如果想要稍微再多了解一点,推荐看一看 WWDC 2018 的 Session 416 ,有很多内容刷新了我的既存认知...