├── LICENSE ├── LeakInspector.swift ├── LeakInspectorAlertProvider.swift ├── README.md └── assets └── screenshot.png /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Two Bit Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /LeakInspector.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @objc protocol LeakInspectorDelegate { 4 | func didLeakReference(_ ref: AnyObject, name: String) 5 | } 6 | 7 | @objc protocol LeakInspectorIgnore { 8 | 9 | } 10 | 11 | @objcMembers 12 | class LeakInspector: NSObject { 13 | 14 | private class RefWatch: CustomStringConvertible { 15 | weak var ref: AnyObject? 16 | let name: String 17 | let ignore: Bool 18 | var failedChecks = 0 19 | var description: String { return "LeakInspector.RefWatch(name: \(name), ignore: \(ignore), failedChecks: \(failedChecks))" } 20 | 21 | init(ref: AnyObject, name: String, ignore: Bool) { 22 | self.ref = ref 23 | self.name = name 24 | self.ignore = ignore 25 | } 26 | } 27 | 28 | static var delegate: LeakInspectorDelegate? { 29 | didSet { 30 | _ = sharedInstance // forces the shared instance to initialize 31 | } 32 | } 33 | 34 | private static let sharedInstance = LeakInspector() 35 | private var refsToWatch = [RefWatch]() 36 | private var classesToIgnore = [AnyObject.Type]() 37 | private let simulator = TARGET_IPHONE_SIMULATOR == 1 38 | private let frequency = 3 // seconds 39 | 40 | private override init() { 41 | super.init() 42 | if simulator { 43 | swizzleViewDidLoad() 44 | scheduleToRun() 45 | } 46 | } 47 | 48 | class func watch(_ ref: AnyObject) { 49 | if sharedInstance.simulator { 50 | register(ref, name: String(describing: ref.self), ignore: false) 51 | } 52 | } 53 | 54 | class func ignore(_ ref: AnyObject) { 55 | if sharedInstance.simulator { 56 | unregister(ref) 57 | register(ref, name: String(describing: ref.self), ignore: true) 58 | } 59 | } 60 | 61 | class func rewatch(_ ref: AnyObject) { 62 | // If you've ignored a ref and want to start watching it again 63 | if sharedInstance.simulator { 64 | unregister(ref) 65 | register(ref, name: String(describing: ref.self), ignore: false) 66 | } 67 | } 68 | 69 | class func ignoreClass(_ type: AnyObject.Type) { 70 | if sharedInstance.simulator { 71 | if Thread.isMainThread { 72 | sharedInstance.ignoreClass(type) 73 | } else { 74 | DispatchQueue.main.async { 75 | self.sharedInstance.ignoreClass(type) 76 | } 77 | } 78 | } 79 | } 80 | 81 | private class func unregister(_ ref: AnyObject) { 82 | if sharedInstance.simulator { 83 | if Thread.isMainThread { 84 | sharedInstance.unregister(ref) 85 | } else { 86 | DispatchQueue.main.async { 87 | self.sharedInstance.unregister(ref) 88 | } 89 | } 90 | } 91 | } 92 | 93 | private class func register(_ ref: AnyObject, name: String, ignore: Bool) { 94 | if sharedInstance.simulator { 95 | if Thread.isMainThread { 96 | sharedInstance.register(ref, name: name, ignore: ignore) 97 | } else { 98 | DispatchQueue.main.async { 99 | self.sharedInstance.register(ref, name: name, ignore: ignore) 100 | } 101 | } 102 | } 103 | } 104 | 105 | private func ignoreClass(_ type: AnyObject.Type) { 106 | for index in 0.. Bool { 140 | guard !String(describing: ref).contains("UIApplicationRotationFollowingController") else { return false } 141 | guard !String(describing: ref).contains("UIInputWindowController") else { return false } 142 | guard !String(describing: ref).contains("_UIAlertControllerTextFieldViewController") else { return false } 143 | guard !String(describing: ref).contains("UISystemKeyboardDockController") else { return false } 144 | guard !String(describing: ref).contains("UICompatibilityInputViewController") else { return false } 145 | guard !String(describing: ref).contains("UIAlertController") else { return false } 146 | guard !String(describing: ref).contains("GCKUIGuestModePairingViewController") else { return false } 147 | 148 | if ref is LeakInspectorIgnore { 149 | return false 150 | } 151 | 152 | for clazz in classesToIgnore { 153 | if type(of: ref) === clazz.self { 154 | return false 155 | } 156 | } 157 | 158 | if type(of: ref) === UIViewController.self { 159 | return false 160 | } 161 | 162 | for refWatch in refsToWatch { 163 | if ref === refWatch.ref { 164 | return false 165 | } 166 | } 167 | return true 168 | } 169 | 170 | private func scheduleToRun() { 171 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.seconds(frequency)) { 172 | self.checkForLeaks() 173 | self.scheduleToRun() 174 | } 175 | } 176 | 177 | private func checkForLeaks() { 178 | var removeRefs = [RefWatch]() 179 | 180 | // Check all the objects to verify they've been deinit'd/dealloc'd 181 | for refWatch in refsToWatch { 182 | if let ref: AnyObject = refWatch.ref { 183 | if (hasRefLikelyLeaked(refWatch)) { 184 | refWatch.failedChecks += 1 185 | if (refWatch.failedChecks > 1) { 186 | // Make objects fail twice before we report them to get async objects a chance to dealloc 187 | alertThatRefHasLeaked(ref, name: refWatch.name) 188 | removeRefs.append(refWatch) 189 | } 190 | } else { 191 | refWatch.failedChecks = 0 192 | } 193 | } else { 194 | removeRefs.append(refWatch) 195 | } 196 | } 197 | 198 | // Remove objects that we no longer need to track 199 | for refWatch in removeRefs { 200 | for (index, aRefWatch) in refsToWatch.enumerated() { 201 | if refWatch === aRefWatch { 202 | refsToWatch.remove(at: index) 203 | break 204 | } 205 | } 206 | } 207 | } 208 | 209 | private func hasRefLikelyLeaked(_ refWatch: RefWatch) -> Bool { 210 | if (refWatch.ignore) { 211 | return false 212 | } 213 | 214 | if let controller = refWatch.ref as? UIViewController { 215 | if controller.parent == nil && controller.navigationController == nil && controller.presentingViewController == nil && controller.view.window?.rootViewController != controller { 216 | return true 217 | } 218 | } else { 219 | return true 220 | } 221 | 222 | return false 223 | } 224 | 225 | private func alertThatRefHasLeaked(_ ref: AnyObject, name: String) { 226 | NSLog("Leak Inspector: detected possible leak of %@", name) 227 | if let delegate = LeakInspector.delegate { 228 | delegate.didLeakReference(ref, name: name) 229 | } 230 | } 231 | 232 | private func swizzleViewDidLoad() { 233 | method_exchangeImplementations( 234 | class_getInstanceMethod(UIViewController.self, #selector(UIViewController.viewDidLoad))!, 235 | class_getInstanceMethod(UIViewController.self, #selector(UIViewController.loadView_WithLeakInspector))! 236 | ) 237 | } 238 | } 239 | 240 | extension UIViewController { 241 | @objc func loadView_WithLeakInspector() { 242 | LeakInspector.watch(self) 243 | loadView_WithLeakInspector() 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /LeakInspectorAlertProvider.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @objc class LeakInspectorAlertProvider: NSObject, LeakInspectorDelegate { 4 | 5 | private weak var alertController: UIAlertController? 6 | 7 | func didLeakReference(_ ref: AnyObject, name: String) { 8 | // dismiss any already visible alert 9 | if let alertController = self.alertController { 10 | alertController.dismiss(animated: false, completion: nil) 11 | } 12 | 13 | let title = "Leak Inspector" 14 | let message = "Detected possible leak of \(name)" 15 | let ok = "OK" 16 | 17 | let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) 18 | alertController.addAction(UIAlertAction(title: ok, style: .default, handler: nil)) 19 | alertController.show() 20 | self.alertController = alertController 21 | } 22 | 23 | } 24 | 25 | extension UIAlertController { 26 | 27 | func show() { 28 | present(animated: true, completion: nil) 29 | } 30 | 31 | func present(animated: Bool, completion: (() -> Void)?) { 32 | let keyWindow: UIWindow? = { 33 | if #available(iOS 13, *) { 34 | return UIApplication.shared.windows.first { $0.isKeyWindow } 35 | } else { 36 | return UIApplication.shared.keyWindow 37 | } 38 | }() 39 | if let rootVC = keyWindow?.rootViewController { 40 | present(from: rootVC, animated: animated, completion: completion) 41 | } 42 | } 43 | 44 | private func present(from controller: UIViewController, animated: Bool, completion: (() -> Void)?) { 45 | if let presentedViewController = controller.presentedViewController { 46 | present(from: presentedViewController, animated: animated, completion: completion) 47 | } else if let navVC = controller as? UINavigationController, let visibleVC = navVC.visibleViewController { 48 | present(from: visibleVC, animated: animated, completion: completion) 49 | } else if let tabVC = controller as? UITabBarController, let selectedVC = tabVC.selectedViewController { 50 | present(from: selectedVC, animated: animated, completion: completion) 51 | } else { 52 | controller.present(self, animated: animated, completion: completion) 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LeakInspector 2 | 3 | An iOS memory leak detection tool to help you prevent leaking UIViewControllers (and any other object you want to track to make sure it gets deallocated when expected). It only runs when running in the iOS simulator and short circuits on device so you don't need to worry about it running in your production app. 4 | 5 | ![screenshot.png](assets/screenshot.png) 6 | 7 | LeakInspector is written in Swift but works on Objective-C projects too. This project was inspired by [Square's Leak Canary](https://github.com/square/leakcanary) for Android. At [Two Bit Labs](http://twobitlabs.com) we use Leak Canary on our Android projects and have been so impressed with how it helped us catch memory leaks during the development cycle that we wanted a similar tool for the iOS apps we work on. 8 | 9 | ## How to use it 10 | 11 | At the moment we don't have Cocoapods or Carthage support so you'll need to git submodule it or just download a zip and drop the 2 swift files directly into your project: 12 | 13 | * [Download the latest zip file](https://github.com/twobitlabs/LeakInspector/archive/master.zip) 14 | 15 | ### Initialization 16 | 17 | In your application delegate's didFinishLaunchingWithOptions, register the alert provider before any controllers are created: 18 | 19 | Swift: 20 | 21 | ``` 22 | LeakInspector.delegate = LeakInspectorAlertProvider() 23 | ``` 24 | 25 | Objective-C: 26 | 27 | ``` 28 | [LeakInspector setDelegate:[LeakInspectorAlertProvider new]]; 29 | ``` 30 | 31 | That's it. When running on the simulator LeakInspector will now warn you if it thinks a controller might have leaked! 32 | 33 | The default *LeakInspectorAlertProvider* will show a UIAlertController whenever a possible leak is found. If you'd rather do something else when a leak is found just set the delegate to be your own app delegate or some other class that implements the *LeakInspectorDelegate* protocol. LeakInspector keeps a strong reference to its delegate so that you can set it and forget it (but something to keep in mind if you implement your own delegate). 34 | 35 | Leak Inspector keeps a weak reference to any objects it tracks and checks for leaks every 3 seconds. If it finds a controller that has not deallocated but has a nil navigationController and nil parentController it will flag it as a possible leak. 36 | 37 | You may have legitimate cases in your app where you remove a controller but want to hang on to it, have those controllers implement the empty *LeakInspectorIgnore* protocol, or call *LeakInspector.ignore(someObject)*, or call *LeakInspect.ignore(SomeClass.self)* or *[LeakInspector ignore:[SomeClass class]]* in Objective-C. 38 | 39 | ### Watching other kinds of objects for memory leaks 40 | 41 | When you are done using an object and want to be sure it gets deallocated just register it with LeakInspector. For example if you have some object that you know does a lot with blocks, GCD, etc that you want to make sure always gets cleaned up just track it: 42 | 43 | Swift: 44 | 45 | ``` 46 | pollingManager.stopPolling() 47 | LeakInspector.watch(pollingManager) 48 | pollingManager = nil 49 | ``` 50 | 51 | Objective-C: 52 | 53 | ``` 54 | [self.pollingManager stopPolling]; 55 | [LeakInspector watch:self.pollingManager]; // Start tracking it and Leak Inspector will warn you if it doesn't get deallocated 56 | self.pollingManager = nil; 57 | ``` 58 | 59 | If you're registering multiple objects of the same class, there's a second version of the watch method that lets you pass an identifying name so that if a leak is discovered it will help you disambiguate which object leaked: 60 | 61 | ``` 62 | LeakInspector.watch(pollingManagerA, name: "News Feed PollingManager") 63 | LeakInspector.watch(pollingManagerB, name: "Message PollingManager") 64 | ``` 65 | 66 | ### Controllers with unusual lifecycles 67 | 68 | If you have a controller that you keep a strong reference to beyond the normal iOS lifecycle you can tell LeakInspector to ignore it: 69 | 70 | Swift: 71 | 72 | ``` 73 | LeakInspector.ignore(controller) 74 | // then later in say deinit of the referencing class you can rewatch it to 75 | // make sure it really does deallocate at that point 76 | LeakInspector.rewatch(controller) 77 | ``` 78 | 79 | ## I found a leak, now what? 80 | 81 | Check any blocks to make sure they use weak self references, make sure any delegate properties in your own code are weak, etc. Please help out by sending a pull request with better troubleshooting steps for this section! 82 | 83 | ## Contributing 84 | 85 | We love pull requests with bug fixes, features, and documentation updates! 86 | 87 | Feel free to add your name and link under the contributors section as part of your pull request so that way if we merge it you get credit as well! 88 | 89 | ## Contributors 90 | - [Two Bit Labs](http://twobitlabs.com/) 91 | - [Todd Huss](https://github.com/thuss) 92 | - [Chris Pickslay](https://github.com/chrispix) 93 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twobitlabs/LeakInspector/54e6b57b253e0f6c989952d1cc1156003bfedbf1/assets/screenshot.png --------------------------------------------------------------------------------