RIBs/Classes/LeakDetector/LeakDetector.swift (105 lines of code) (raw):

// // Copyright (c) 2017. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import RxSwift import RxRelay import UIKit /// Leak detection status. public enum LeakDetectionStatus { /// Leak detection is in progress. case InProgress /// Leak detection has completed. case DidComplete } /// The default time values used for leak detection expectations. public struct LeakDefaultExpectationTime { /// The object deallocation time. public static let deallocation = 1.0 /// The view disappear time. public static let viewDisappear = 5.0 } /// The handle for a scheduled leak detection. public protocol LeakDetectionHandle { /// Cancel the scheduled detection. func cancel() } /// An expectation based leak detector, that allows an object's owner to set an expectation that an owned object to be /// deallocated within a time frame. /// /// A `Router` that owns an `Interactor` might for example expect its `Interactor` be deallocated when the `Router` /// itself is deallocated. If the interactor does not deallocate in time, a runtime assert is triggered, along with /// critical logging. public class LeakDetector { /// The singleton instance. public static let instance = LeakDetector() /// The status of leak detection. /// /// The status changes between InProgress and DidComplete as units register for new detections, cancel existing /// detections, and existing detections complete. public var status: Observable<LeakDetectionStatus> { return expectationCount .asObservable() .map { expectationCount in expectationCount > 0 ? LeakDetectionStatus.InProgress : LeakDetectionStatus.DidComplete } .distinctUntilChanged() } /// Sets up an expectation for the given object to be deallocated within the given time. /// /// - parameter object: The object to track for deallocation. /// - parameter inTime: The time the given object is expected to be deallocated within. /// - returns: The handle that can be used to cancel the expectation. @discardableResult public func expectDeallocate(object: AnyObject, inTime time: TimeInterval = LeakDefaultExpectationTime.deallocation) -> LeakDetectionHandle { expectationCount.accept(expectationCount.value + 1) let objectDescription = String(describing: object) let objectId = String(ObjectIdentifier(object).hashValue) as NSString trackingObjects.setObject(object, forKey: objectId) let handle = LeakDetectionHandleImpl { self.expectationCount.accept(self.expectationCount.value - 1) } Executor.execute(withDelay: time) { // Retain the handle so we can check for the cancelled status. Also cannot use the cancellable // concurrency API since the returned handle must be retained to ensure closure is executed. if !handle.cancelled { let didDeallocate = (self.trackingObjects.object(forKey: objectId) == nil) let message = "<\(objectDescription): \(objectId)> has leaked. Objects are expected to be deallocated at this time: \(self.trackingObjects)" if self.disableLeakDetector { if !didDeallocate { print("Leak detection is disabled. This should only be used for debugging purposes.") print(message) } } else { assert(didDeallocate, message) } } self.expectationCount.accept(self.expectationCount.value - 1) } return handle } /// Sets up an expectation for the given view controller to disappear within the given time. /// /// - parameter viewController: The `UIViewController` expected to disappear. /// - parameter inTime: The time the given view controller is expected to disappear. /// - returns: The handle that can be used to cancel the expectation. @discardableResult public func expectViewControllerDisappear(viewController: UIViewController, inTime time: TimeInterval = LeakDefaultExpectationTime.viewDisappear) -> LeakDetectionHandle { expectationCount.accept(expectationCount.value + 1) let handle = LeakDetectionHandleImpl { self.expectationCount.accept(self.expectationCount.value - 1) } Executor.execute(withDelay: time) { [weak viewController] in // Retain the handle so we can check for the cancelled status. Also cannot use the cancellable // concurrency API since the returned handle must be retained to ensure closure is executed. if let viewController = viewController, !handle.cancelled { let viewDidDisappear = (!viewController.isViewLoaded || viewController.view.window == nil) let message = "\(viewController) appearance has leaked. Either its parent router who does not own a view controller was detached, but failed to dismiss the leaked view controller; or the view controller is reused and re-added to window, yet the router is not re-attached but re-created. Objects are expected to be deallocated at this time: \(self.trackingObjects)" if self.disableLeakDetector { if !viewDidDisappear { print("Leak detection is disabled. This should only be used for debugging purposes.") print(message) } } else { assert(viewDidDisappear, message) } } self.expectationCount.accept(self.expectationCount.value - 1) } return handle } // MARK: - Internal Interface // Test override for leak detectors. static var disableLeakDetectorOverride: Bool = false #if DEBUG /// Reset the state of Leak Detector, internal for UI test only. func reset() { trackingObjects.removeAllObjects() expectationCount.accept(0) } #endif // MARK: - Private Interface private let trackingObjects = NSMapTable<AnyObject, AnyObject>.strongToWeakObjects() private let expectationCount = BehaviorRelay<Int>(value: 0) lazy var disableLeakDetector: Bool = { if let environmentValue = ProcessInfo().environment["DISABLE_LEAK_DETECTION"] { let lowercase = environmentValue.lowercased() return lowercase == "yes" || lowercase == "true" } return LeakDetector.disableLeakDetectorOverride }() private init() {} } fileprivate class LeakDetectionHandleImpl: LeakDetectionHandle { var cancelled: Bool { return cancelledRelay.value } let cancelledRelay = BehaviorRelay<Bool>(value: false) let cancelClosure: (() -> ())? init(cancelClosure: (() -> ())? = nil) { self.cancelClosure = cancelClosure } func cancel() { cancelledRelay.accept(true) cancelClosure?() } }