-
Notifications
You must be signed in to change notification settings - Fork 31
Expand file tree
/
Copy pathDeallocationChecker.swift
More file actions
153 lines (126 loc) · 6.13 KB
/
DeallocationChecker.swift
File metadata and controls
153 lines (126 loc) · 6.13 KB
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import UIKit
@objc
public class DeallocationChecker: NSObject {
public enum LeakState {
case leaked
case notLeaked
}
public enum DisappearanceSource: CustomStringConvertible, CustomDebugStringConvertible {
case parent
case dismissed
public var description: String {
return self.debugDescription
}
public var debugDescription: String {
switch self {
case .parent:
return "parent"
case .dismissed:
return "dismissed"
}
}
}
public typealias Callback = (LeakState, UIViewController.Type, DisappearanceSource) -> ()
public enum Handler {
/// Shows alert when a leak is detected.
case alert
/// Calls preconditionFailure when a leak is detected.
case precondition
/// Customization point if you need other type of logging of leak detection, for example to the console or Fabric.
case callback(Callback)
}
@objc
public static let shared = DeallocationChecker()
private(set) var handler: Handler?
/// Sets up the handler then used in all `checkDeallocation*` methods.
/// It is recommended to use DeallocationChecker only in the DEBUG configuration by wrapping this call inside
/// ```
/// #if DEBUG
/// DeallocationChecker.shared.setup(with: .alert)
/// #endif
/// ```
/// call.
///
/// This method isn't exposed to Obj-C because we use an enumeration that isn't compatible with Obj-C.
public func setup(with handler: Handler) {
self.handler = handler
}
/// This method asserts whether a view controller gets deallocated after it disappeared
/// due to one of these reasons:
/// - it was removed from its parent, or
/// - it (or one of its parents) was dismissed.
///
/// The method calls the `handler` if it's non-nil.
///
/// **You should call this method only from UIViewController.viewDidDisappear(_:).**
/// - Parameter delay: Delay after which the check if a
/// view controller got deallocated is performed
@objc(checkDeallocationOf:afterDelay:)
public func checkDeallocation(of viewController: UIViewController, afterDelay delay: TimeInterval = 1.0) {
guard let handler = DeallocationChecker.shared.handler else {
return
}
let rootParentViewController = viewController.dch_rootParentViewController
// `UITabBarController` keeps a strong reference to view controllers that disappeared from screen. So, we don't have to check if they've been deallocated.
guard !rootParentViewController.isKind(of: UITabBarController.self) else {
return
}
// We don't check `isBeingDismissed` simply on this view controller because it's common
// to wrap a view controller in another view controller (e.g. a stock UINavigationController)
// and present the wrapping view controller instead.
if viewController.isMovingFromParent || rootParentViewController.isBeingDismissed {
let viewControllerType = type(of: viewController)
let disappearanceSource: DisappearanceSource = viewController.isMovingFromParent ? .parent : .dismissed
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: { [weak viewController] in
let leakState: LeakState = viewController != nil ? .leaked : .notLeaked
switch handler {
case .alert:
if leakState == .leaked {
self.showAlert(for: viewControllerType)
}
case .precondition:
if leakState == .leaked {
preconditionFailure("\(viewControllerType) not deallocated after being \(disappearanceSource == .parent ? "removed from its parent" : "dismissed")")
}
case let .callback(callback):
callback(leakState, viewControllerType, disappearanceSource)
}
})
}
}
@objc(checkDeallocationWithDefaultDelayOf:)
public func checkDeallocationWithDefaultDelay(of viewController: UIViewController) {
self.checkDeallocation(of: viewController)
}
// MARK: - Private
private func showAlert(for viewController: UIViewController.Type) {
let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = UIViewController()
window.makeKeyAndVisible()
let message = "\(viewController) is still in memory even though its view was removed from hierarchy. Please open Memory Graph Debugger to find strong references to it."
let alertController = UIAlertController(title: "Leak Detected", message: message, preferredStyle: .alert)
alertController.addAction(.init(title: "OK", style: .cancel, handler: nil))
window.rootViewController?.present(alertController, animated: false, completion: nil)
}
}
extension UIViewController {
@available(*, deprecated, message: "Please switch to using methods on DeallocationChecker. Also remember to call setup(with:) when your app starts.")
@objc(dch_checkDeallocationAfterDelay:)
public func dch_checkDeallocation(afterDelay delay: TimeInterval = 2.0) {
print("Please switch to using methods on DeallocationChecker. Also remember to call setup(with:) when your app starts.")
DeallocationChecker.shared.checkDeallocation(of: self, afterDelay: delay)
}
@available(*, deprecated, message: "Please switch to using methods on DeallocationChecker. Also remember to call setup(with:) when your app starts.")
@objc(dch_checkDeallocation)
public func objc_dch_checkDeallocation() {
print("Please switch to using methods on DeallocationChecker. Also remember to call setup(with:) when your app starts.")
DeallocationChecker.shared.checkDeallocationWithDefaultDelay(of: self)
}
fileprivate var dch_rootParentViewController: UIViewController {
var root = self
while let parent = root.parent {
root = parent
}
return root
}
}