还记得在您的 iOS 或 tvOS 应用中遇到一些奇怪的问题,最终被发现是由某些内存泄漏引起的吗?现在不用再担心了!LeakedViewControllerDetector 帮助您在任何 UIKit 应用中查找泄露的 Views 和 ViewControllers。无论何时发生泄漏,您都会立即知道!最棒的是,您几乎不需要对您的代码进行任何更改:只需设置一次,即可一劳永逸。对于每个 UIKit 应用来说,它都是一个极大的帮助!
当 ViewController 关闭但未释放时,会弹出一个包含屏幕截图的警报
如果泄漏自行解决,警报会更新
检测器还检测泄露的 Views
可以禁用警报,这样您的用户就不会看到它们。
首先,使用 Github URL https://github.com/Janneman84/LeakedViewControllerDetector
通过 SPM 安装此软件包。我建议使用 main 分支。确保该库已链接到目标。
或者您可以直接复制/粘贴 LeakedViewControllerDetector.swift
文件到您的项目中,但不建议这样做,因为您将无法收到更新。
现在,如果您使用了 SPM,则将 import 添加到 AppDelegate
import LeakedViewControllerDetector
然后将以下代码添加到 application(_:didFinishLaunchingWithOptions:)
在 AppDelegate
类中
LeakedViewControllerDetector.onDetect() { leakedViewController, leakedView, message in
#if DEBUG
return true //show warning alert dialog
#else
//here you can log warning message to a server, e.g. Crashlytics
return false //don't show warning to user
#endif
}
就是这样!泄漏检测器现在已经启动并运行了。但是,为了充分利用此软件包,我建议您研究接下来的两个部分,我在其中详细介绍了所有内容。
大多数泄漏检测无需更改您的代码即可工作。但是,仍然可能需要进行一些小的更改
您需要手动将 View 的 removeFromSuperview()
替换为 removeFromSuperviewDetectLeaks()
,每次您想要移除一个 view 并确保它及其所有子视图都被释放 (deinnited)
//once the view is removed it will warn you if it (or any of its subviews) haven't deinnited:
someView.removeFromSuperviewDetectLeaks()
当然,只有在 View *应该*在移除后释放时才使用此方法,否则您可能会收到虚假警告。
您代码的其余部分需要符合两个简单的事情。首先,确保如果您覆盖 ViewController 中的 viewDidLoad()
、viewDidAppear()
和/或 viewDidDisappear()
,始终调用 super
。这是常见的做法,所以您可能已经这样做了,但现在这样做至关重要。
override func viewDidLoad() {
super.viewDidLoad() //don't forget this!
...
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) //don't forget this!
...
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated) //don't forget this!
...
}
其次,如果您想从 UIViewController 数组中删除项目,例如在 UINavigationController、UITabBarController 或 UIPageViewController 中,请不要使用 remove(at:)
,而应使用 removeFromparent()
代替。
//navigationController?.viewControllers.remove(at:3)
navigationController?.viewControllers[3].removeFromParent()
//tabBarController?.viewControllers?.remove(at:3)
tabBarController?.viewControllers?[3].removeFromParent()
就是这样!泄漏检测器现在已完全运行。如果您的应用程序运行正常,您将不会注意到任何事情。现在是调整回调以符合您的喜好的时候了。
正如您在快速入门中看到的,建议区别对待调试版本和发布版本。如果您正在调试,则最好收到一个弹出对话框,警告您存在问题。但是您不希望您的用户看到此消息,因此您改为进行日志记录。让我们逐步了解回调的参数和返回值。
LeakedViewControllerDetector.onDetect(detectionDelay: 1.0) { leakedViewController, leakedView, message in
return true
}
detectionDelay
是 View 或 ViewController 及其子视图关闭后,在触发警告之前释放自身的时间(以秒为单位)。如果您收到许多释放警告,您可能需要增加此数字。默认值 1.0s 应该可以防止大多数警告。您可以考虑对调试版本使用更短的延迟。
回调提供以下内容
leakedViewController
,请注意这是一个可选值。如果先前泄漏的 VC 被释放 (自行解决),则会再次触发此回调,但在这种情况下,leakedViewController 和 leakedView 都将为 nil。leakedView
,与 leakedViewController 相同,但用于 Views。message
,一个字符串,可用于打印到控制台或记录到您选择的服务器回调需要返回一个可选的 Bool 值
true
以显示带有警告消息的警报对话框。注意:如果您在 iPad 上使用多个窗口,则显示警报的窗口不一定是发生泄漏的窗口。false
以不显示警报对话框,建议在发布版本中使用nil
。如果您想忽略某些类或实例的警告,通常会使用此方法。如果由于某种原因,您想忽略某些 Views 或 ViewControllers 的警告,请确保返回 nil
LeakedViewControllerDetector.onDetect() { leakedViewController, leakedView, message in
//return nil to ignore:
if let leakedViewController {
if leakedViewController is IgnoreThisViewController {return nil}
if type(of: leakedViewController).description() == "_IgnoreThisPrivateViewController" {return nil}
if leakedViewController.view.tag == -1 {return nil}
}
if let leakedView {
if leakedView is IgnoreThisView {return nil}
if type(of: leakedView).description() == "_IgnoreThisPrivateView" {return nil}
if leakedView.tag == -1 {return nil}
}
return true
}
该软件包已经自行忽略了一些 ViewControllers,您可以在 LeakedViewControllerDetector.ignoredViewControllerClassNames
、LeakedViewControllerDetector.ignoredViewClassNames
和 LeakedViewControllerDetector.ignoredWindowClassNames
中找到它们。您可以根据需要添加和删除这些数组中的元素。这样您就可以防止它们首先触发警告。
如果您使用 Crashlytics,您可以像这样记录警告消息
import FirebaseCrashlytics
let error = NSError(domain: Bundle.main.bundleIdentifier!,
code: 8, //whatever number you fancy
userInfo: [NSLocalizedDescriptionKey: message])
Crashlytics.crashlytics().record(error: error)
当您将所有内容结合在一起时,您最终会得到类似这样的东西
#if DEBUG
let delay = 0.2
#else
let delay = 1.0
#endif
LeakedViewControllerDetector.onDetect(detectionDelay: delay) { leakedViewController, leakedView, message in
//return nil to ignore warnings of certain Viewscontrollers/Views
if let leakedViewController {
// UIImagePickerController tends to leak for ~5 second when you close it
if leakedViewController is UIImagePickerController {return nil}
if leakedViewController is IgnoreThisViewController {return nil}
if type(of: leakedViewController).description() == "_IgnoreThisPrivateViewController" {return nil}
if leakedViewController.view.tag == -1 {return nil}
}
if let leakedView {
if leakedView is IgnoreThisView {return nil}
if type(of: leakedView).description() == "_IgnoreThisPrivateView" {return nil}
if leakedView.tag == -1 {return nil}
}
#if DEBUG
print(message)
return true //show alert
#else
//log leak message to server:
let error = NSError(domain: Bundle.main.bundleIdentifier ?? "bundleIdentifier",
code: 8,
userInfo: [NSLocalizedDescriptionKey: message])
Crashlytics.crashlytics().record(error: error)
return false //don't show alert
#endif
}
仅调试示例
#if DEBUG
LeakedViewControllerDetector.onDetect() { leakedViewController, leakedView, message in
//insert ignore code here
print(message)
return true //show alert
}
#endif
将 import 行也放在 DEBUG 标签之间也很诱人。但是,如果您在代码中的任何地方使用 removeFromSuperviewDetectLeaks()
,您仍然需要它。如果未设置检测回调,则对此方法的调用将仅作为常规 removeFromSuperview()
执行。
本质上,它所做的只是这个
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
//when self is nil the ViewController has deinnited, so no leak
guard let self = self else { return }
//if all these properties are nil the ViewController is considered to have leaked
if self.view.window == nil && self.parent == nil && self.presentedViewController == nil && (view == nil || view.superview == nil) {
print("Leaked ViewController detected: \(self)")
}
}
}
当然,为了捕捉所有边缘情况,还有更多的事情要做。泄漏的 Views 以类似的方式检测。随意查看源代码,它很小。除非您正在做一些花哨的事情,否则这种方法非常有效。
在回调内部引用 self
通常是内存泄漏的原因。此代码将使 ViewController、View 或任何其他对象保持活动状态 10 秒钟
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
print(self)
}
如果您在 10 秒钟结束前关闭 ViewController,这将触发内存泄漏警告。10 秒钟后,您将看到另一个警告,告诉您 ViewController 已经自行释放。这种情况对于最终完成或超时的慢速网络请求也很典型。
这些问题很容易通过使用 [weak self]
来解决,正如您可能知道的那样
DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in
guard let self = self else {return}
print(self)
}
在像这样观察通知时,不要忘记也使用 [weak self]
,否则 ViewController 将永远保留在内存中
NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "SomeNotification"), object: nil, queue: nil) { [weak self] notification in
print(self)
}
或者,您可以像这样使用选择器进行观察,这不会导致内存泄漏
NotificationCenter.default.addObserver(self, selector: #selector(someMethod), name: Notification.Name("SomeNotification"), object: nil)
确保始终将委托声明为弱变量,否则很可能发生内存泄漏
weak var delegate: MyViewControllerDelegate?
有时您想设置 PresentationController 委托,例如,如果您想实现 presentationControllerShouldDismiss()
。但是,如果您设置了此委托,如果您的 ViewController 是父项(如 NavigationController)的子项,则它不会触发委托方法,而是会导致永久性内存泄漏
self.presentationController?.delegate = self
相反,最好总是使用这个
self.presentingViewController?.presentedViewController?.presentationController?.delegate = self
此技巧确保委托始终设置为父项,无论它是什么。
这是一个隐蔽的问题。如果您的 ViewController 位于 NavigationController 中,并且您在其关闭后立即设置其 presentationController 委托,它将使其自身及其子项永远保留在内存中
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
//it's tempting to do this, but don't
navigationController?.presentationController?.delegate = nil
}
因此,使用此代码显示带有 ViewController 的 NavigationController。然后关闭 NavigationController,您将看到内存警告。
有时您想在 UIAlertController 的 action 回调中引用 UIAlertController。在这种情况下,请确保您使用 unowned
或 weak
,否则警报将永远留在内存中。如果您确定它不会为 nil(这里就是这种情况),则可以使用 unowned
,它基本上与显式解包 weak
相同
//since we're referencing alert inside the callbacks use unowned or weak or it will never deinit
let alert = UIAlertController.init(title: "Retain test", message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction.init(title: "Unowned", style: .default) { [unowned alert] action in
print(alert)
})
alert.addAction(UIAlertAction.init(title: "Weak", style: .default) { [weak alert] action in
print(alert!) //explicitly unwrapping weak alert works basically the same as using unowned
})
self.present(alert, animated: true)
在某些情况下,在应用程序中点击断点和/或使用视图层次结构调试器可能会导致(看起来像是)内存泄漏和其他奇怪的问题。我还没有任何可重现的步骤,但如果我有的话,我会在这里列出它们。如果您了解更多关于这方面的信息,请在 Issues 部分告诉我。
在 iPhone 上使用 SplitViewController 确实会导致一些特殊的行为。如果您从 master 推送或显示 detail VC,然后关闭它,VC 不会被移除,而是会保持在内存中活动状态。如果您推送/显示一个新的 detail VC,那么之前的 VC 实际上将被移除。此软件包考虑了这种行为。请记住,如果您关闭 detail VC,它将在您打开新的 detail VC 之前一直保持活动状态。这严格来说不是内存泄漏,但如果您不了解这种行为,它是相关问题的常见原因。有趣的是,在 iPhone 上,初始的 detail VC *永远*不会释放,所以...是的...
例如,如果您有一个长的 TableView,并且向下滑动,它将在停止之前继续滚动一段时间。现在,如果您在此发生时关闭 VC,TableView 将在内存中保留长达一秒钟。这适用于所有形式的 ScrollViews。似乎没有办法防止这种情况。该软件包旨在在这种特定情况下至少等待一秒钟,然后检查泄漏,以防止误报。
iOS 并非没有缺陷,我注意到它自身也有一些内存泄漏。一个一直困扰我的是 UITextViews。如果您关闭 editable,打开 isSelectable,启用链接检测,并将 URL 添加到文本中,如果您长按 URL,您会得到一个很好的预览。但是,一旦您这样做,UITextView 将永远卡在内存中!我不知道如何解决这个问题。在 iOS 17 中,可以通过子类化 TextView 并覆盖 removeFromSuperview()
来解决此问题
//in UITextView subclass
override func removeFromSuperview() {
let wasSelectable = isSelectable
isSelectable = false
super.removeFromSuperview()
isSelectable = wasSelectable
}
文本字段似乎在 iOS 17 中存在释放问题,主要是如果设置了 textContentType
。如果您有一个带有多个文本字段的 VC,请使用至少一个文本字段,然后关闭 VC。然后,它的所有文本字段都会泄漏,直到您使用不同的文本字段。我目前不知道如何处理这个问题。
您是否知道此处未列出的其他泄漏原因?请在 Issues 部分告诉我,以便我可以添加它们。
此软件包只知道是否发生泄漏,但它不知道 *为什么*:这取决于您来弄清楚。上一节中的列表应该可以帮助您找到罪魁祸首。如果您还收到释放警告,则可能意味着它与网络调用或某些动画有关。在某些情况下,您可能会收到 ViewControllers 和 Views 的警告。首先专注于修复 ViewController,然后任何 View 警告通常也会消失。一个好的策略是不断解除 View 或 ViewController(主要是 ViewDidLoad()
),直到泄漏停止发生。或者先完全解除它,然后一点一点地放回去,或者介于两者之间的东西(二分搜索)。
你可以将 deinit{}
添加到任何对象中,以监控它是否被正确释放。一个典型的用法是 deinit{print("deinit \(self)")}
。观察你的控制台,看看当对象应该被释放时,这条打印语句是否出现。 如果你收到了内存泄漏警告,你可能想将它添加到类中,以确认它确实发生了泄漏,或者确认修复是否有效。
你可以使用 Xcode 的 Instruments 来查找内存泄漏。然而,Instruments 使用起来可能很复杂,并且只在你专门搜索泄漏时才有效。这个包的优点是它总是有效,你不需要一直想着它。此外,它在你的用户使用应用时也能工作,所以你可以知道何时在实际使用中发生了内存泄漏。 请注意,这个包只检测泄漏的 ViewControllers 和 Views,而不是像 Instruments 那样检测其他对象。 ViewControllers 往往是大多数泄漏的罪魁祸首,所以这是一个好的开始。
对于大多数应用来说,性能不应该成为问题,因为代码已经过优化。 如果你担心性能问题,你可以选择仅在调试版本中检查泄漏。 如果你遇到性能问题或其他任何问题,请务必在 Issues 部分告知我。
这个包没有被设计或测试用于 SwiftUI。 因为 SwiftUI 更多的是基于结构体,所以内存泄漏在那里并不常见。
这个包在某些情况下可能会产生误报或漏报。 它不保证能捕获每一个内存泄漏,并且它只检测泄漏的 ViewControllers 及其 Views,而不是其他对象。 如果你遇到麻烦,请前往 Issues 部分。 这个包使用了以下方法的 Method Swizzling:UIViewController 的 viewDidAppear()
、viewDidDisappear()
、removeFromParent()
和 UISplitViewController 的 showDetailViewController()
。
MIT
这个包是否帮助你找到了任何泄漏? 请在 Issues 部分的用户评价线程中留言!