110

Swift 单元测试-定位内存泄露

 6 years ago
source link: https://zhuanlan.zhihu.com/p/32328038?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Swift 单元测试-定位内存泄露

简评:本文介绍了几种测试用例来定位常见的内存泄露问题。

代理模式是苹果开发中非常常见的开发模式。假设我们有一个 ViewController,用于显示用户的好友列表,为了 ViewController 能够将事件发送给所有者,我们定义了一个 delegate。例如:

class FriendListViewController: UIViewController {
   var delegate: FriendListViewControllerDelegate?
}

咋一看上面代码好像没什么问题(delegate 为 strong 引用会导致循环引用),我们使用这有问题的代码做测试。

测试用例如下:

class FriendListViewControllerTests: XCTestCase {
    func testDelegateNotRetained() {
        let controller = FriendListViewController()

        // Create a strong reference to a delegate object, then
        // assign it as the view controller's delegate
        var delegate = FriendListViewControllerDelegateMock()
        controller.delegate = delegate

        // Re-assign the strong reference to a new object, which
        // should cause the original object to be released, thus
        // setting the view controller's delegate to nil
        delegate = FriendListViewControllerDelegateMock()
        XCTAssertNil(controller.delegate)
    }
}

上面的测试用例运行的时候会报错(controller.delegate 无法被释放),这种情况解决的方法很简单将 delegate 设为 weak:

class FriendListViewController: UIViewController {
    weak var delegate: FriendListViewControllerDelegate?
}

当我们再次运行测试用例的时候就不会报错了。

TIPS:可以使用 SwiftLint ,如果你的 delegate 没有定义成 weak 他会给你一个警告。

观察者模式

观察者模式也是常用设计模式,一个 Object 可以轻松的给所有的 observers 发送事件。我们很可能不希望 retain 这些 observers,因为这些观察者通常持有这个 Object 的强引用。

举个例子,这里定义了一个 UserManager 持有所有 observers 的强引用:

class UserManager {
    private var observers = [UserManagerObserver]()

    func addObserver(_ observer: UserManagerObserver) {
        observers.append(observer)
    }
}

和代理模型一样,这种情况通常会导致我们应用内存泄露。

不过我们的测试用例可以很容易定位这个问题:

class UserManagerTests: XCTestCase {
    func testObserversNotRetained() {
        let manager = UserManager()

        // Create both a strong and a weak local reference to an
        // observer, which we then add to our UserManager
        var observer = UserManagerObserverMock()
        weak var weakObserver = observer
        manager.addObserver(observer)

        // If we re-assign the strong reference to a new object,
        // we expect the weak reference to become nil, since
        // observers shouldn't be retained
        observer = UserManagerObserverMock()
        XCTAssertNil(weakObserver)
    }
}

上面这个测试用例会报错。要解决这个问题,我们可以用 ObserverWrapper 来包装弱引用 observer,例如:

 private extension UserManager {
    struct ObserverWrapper {
        weak var observer: UserManagerObserver?
    }
}

class UserManager {
    private var observers = [ObserverWrapper]()

    func addObserver(_ observer: UserManagerObserver) {
        let wrapper = ObserverWrapper(observer: observer)
        observers.append(wrapper)
    }
}

当你需要弱引用集合中的对象,上面的技巧是非常有用的,注意清除已释放 observer 的 包装,我们可以用下列代码来清除无用的 ObserverWrapper

private func notifyObserversOfUserChange() {
    observers = observers.filter { wrapper in
        guard let observer = wrapper.observer else {
            return false
        }

        observer.userManager(self, userDidChange: user)
        return true
    }
} 

最后我们来看看如何检查闭包引起的内存泄露。

我们构建一个 ImageLoader用于加载网络上的图片:

class ImageLoader {
    func loadImage(from url: URL,
                   completionHandler: @escaping (UIImage) -> Void) {
        ...
    }
}

一个常见的错误是当操作完成后 completionHandler 没有被释放从而导致内存泄露。

我们可以用单元测试来检测释放有内存泄露的情况,在其他情况我们可以用 weak 来弱引用delegate 或 observers,但是我们不能弱引用闭包 ?。

不过我们可以使用闭包捕获的对象来代替,我们可以在测试用例中定义个无用的 Object,然后闭包捕获该 Object,最后通过检查 Object 是否被释放来判断闭包是否被释放。代码如下:

class ImageLoaderTests: XCTestCase {
    func testCompletionHandlersRemoved() {
        // Setup an image loader with a mocked network manager
        let networkManager = NetworkManagerMock()
        let loader = ImageLoader(networkManager: networkManager)

        // Mock a response for a given URL
        let url = URL(fileURLWithPath: "image")
        let data = UIImagePNGRepresentation(UIImage())
        let response = networkManager.mockResponse(for: url, with: data)

        // Create an object (it can be of any type), and hold both
        // a strong and a weak reference to it
        var object = NSObject()
        weak var weakObject = object

        loader.loadImage(from: url) { [object] image in
            // Capture the object in the closure (note that we need to use
            // a capture list like [object] above in order for the object
            // to be captured by reference instead of by pointer value)
            _ = object
        }

        // Send the response, which should cause the above closure to be run
        // and then removed & released
        response.send()

        // When we re-assign our local strong reference to a new object the
        // weak reference should become nil, since the closure should have been
        // run and removed at this point
        object = NSObject()
        XCTAssertNil(weakObject)
    }
}
原文:Using unit tests to identify & avoid memory leaks in Swift
推荐阅读:Swift - imports

极光日报,极光开发者旗下媒体。

每天导读三篇英文技术文章。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK