0%

iOS OOM监测

虽然大部分问题在开发中就可以避免,但是有些隐蔽的问题还是需要一些工具来监测

Xcode自带的工具是测试阶段的首选,这里就不讲了。

内存泄漏

这是一个很容易导致 OOM 的问题,在 App 中内存泄漏大部分是由于循环引用引起的。

我们需要在对象应该被释放之前,传递给一个延时执行的方法,来判断一段时间后对象是否有被销毁:

1
2
3
4
5
6
7
8
9
10
11
public class MemoryChecker {
public static func verifyDealloc(_ object: AnyObject?) {
#if DEBUG
DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak object] in
if let object = object {
fatalError("Class Not Deallocated: \(String(describing: object.classForCoder ?? object)))")
}
}
#endif
}
}

控制器是一个很重的对象,如果控制器退出没有释放,那么会造成很大的内存泄漏,所以在监测 NavigationControllerpopdeinit 方法以及 ViewControllerdismiss 方法:

1
2
3
4
5
6
7
8
9
10
11
12
class NavigationController: UINavigationController {

deinit {
viewControllers.forEach{ MemoryChecker.verifyDealloc($0) }
}

override func popViewController(animated: Bool) -> UIViewController? {
let viewController = super.popViewController(animated: animated)
MemoryChecker.verifyDealloc(viewController)
return viewController
}
}

也可以覆盖 UITableView,监测 TableView 释放后, cell 是否有内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

class WeakBox<A: AnyObject> {
weak var unbox: A?

init(_ value: A) {
unbox = value
}
}

class TableView: UITableView {
private var allCells: [WeakBox<UITableViewCell>] = []

private func appendIfNotPresent(_ cell: UITableViewCell) {
if allCells.first(where: { $0.unbox === cell }) == nil {
allCells.append(WeakBox(cell))
}
}

override func dequeueReusableCell(withIdentifier identifier: String, for indexPath: IndexPath) -> UITableViewCell {
let cell = super.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
appendIfNotPresent(cell)
return cell
}

deinit {
allCells.forEach { MemoryChecker.verifyDealloc($0.unbox) }
}
}

当然有时候控制器会释放,但是它持有的某些 View 内存泄漏,所以实际应用中会进行递归遍历控制器与view的所有强引用变量。

第三方库

MLeaksFinder

MLeaksFinder 就是通过类似上面逻辑来判断是否有内存泄漏,它会重载 NSObjectwillDealloc 方法,对需要检测的对象进行延时 2 秒后检测,如果还存在,那么就打印对应的持有栈信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (BOOL)willDealloc {
NSString *className = NSStringFromClass([self class]);
if ([[NSObject classNamesWhitelist] containsObject:className])
return NO;

NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey);
if ([senderPtr isEqualToNumber:@((uintptr_t)self)])
return NO;

__weak id weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
__strong id strongSelf = weakSelf;
[strongSelf assertNotDealloc];
});

return YES;
}

MLeaksFinder 对代码没有入侵,只要导入头文件即可,是因为它使用了Category 进行一部分方法的重写以及使用方法替换了 UINavigationController 中的一系列 pop push 方法与 UIViewController 中的 dismiss viewDidDisappear

缺点是对于 Swift 类无法监测

FBRetainCycleDetector

这是用来检测循环引用的,将想要检测的对象传入探测器,会对对象的强引用对象进行递归遍历,默认查找10层,最后在整个有向图中应用 DFS 算法查找环。如果有向图存在环,则说明目标对象存在循环引用。

MLeaksFinder中也集成了 FBRetainCycleDetector ,在检测到内存泄漏后,可以直接点击 Retain Cycle 将检测到泄漏的对象提交给 FBRetainCycleDetector 监测是否有循环引用

OOM监测

由OOM引起的崩溃程序无法捕获,也就没办法统计反馈。

2015年 facebook 提出了一种方案,在程序启动时,根据上一次程序终止保存的状态,排除:

  • 该应用程序已升级。
  • 应用程序调用退出或中止。
  • 该应用程序崩溃了。
  • 用户向上滑动以强制退出应用程序。
  • 设备重新启动(包括操作系统升级)。

这些问题,那么就判定为发生了 OOM,再根据App最后是否在后台判断是 FOOM 还是 BOOM

但是这种判断没法检测所有场景,容易发生误判。

OOMDetector

腾讯开源的工具,通过Hook IOS系统底层内存分配与释放的方法,跟踪并记录进程中每个对象内存的分配信息,包括分配堆栈、累计分配次数、累计分配内存等,这些信息也会被缓存到进程内存中。在内存触顶的时候,组件会定时Dump这些堆栈信息到本地磁盘,这样如果程序爆内存了,就可以将爆内存前Dump的堆栈数据上报到后台服务器进行分析。

虽然此工具性能高,但是持续的监控内存还是会 cpu 性能、内存占用造成一定的影响。

字节跳动的方案

定时监测 App 内存占用,超过设置的阈值后启动内存分析,挂起所有其他线程,对内存进行快照,采集所有节点信息,然后构建引用关系,生成日志后恢复所有线程状态,然后对日志进行压缩 上传处理,因为在采集的时候会挂起所有非采集线程,所以整个 App 会卡住,需要限制触发频率与上限。

好处是在程序正常运行时,几乎不会对性能产生影响,只有在临近 OOM 时,会进行内存信息采集。

最后

由于 OOM 无法直接捕获,只能通过这些间接的方法来实现,具体上线项目使用什么方案,按项目情况自己选择。