本文将讨论一下在Swift中,closure中引用self时的循环引用问题,以及unowned和weak的用法。
关于iOS内存管理中经典的循环引用问题在这篇文章中就不再描述,可以参考onevcat的这篇文章:《内存管理,weak 和 unowned》
我们知道当一个闭包作为类成员的时候,如果这个闭包内访问了self
那么就会引起循环引用,这时我们需要用unowned
或者weak
来避免循环引用。具体内容在onevcat的文章中有详细描述。
在这篇文章里我想讨论一下当闭包作为函数参数时会是个什么情况。为此我们写一个vc如下所示:
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Second ViewController"
}
deinit {
print("deinit \(self.title ?? "default value")")
}
@IBAction func getData(_ sender: Any) {
ApiManager.getAsyncMockData {(dict: NSDictionary) in
let action = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
let actionController = UIAlertController(title: "Second ViewController Alert", message: dict.description, preferredStyle: .alert)
actionController.addAction(action)
self.present(actionController, animated: true, completion: nil)
}
self.navigationController?.popViewController(animated: true)
}
}
class ApiManager: NSObject {
class func getMockData() -> NSDictionary? {
if let path = Bundle.main.path(forResource: "data", ofType: "json") {
if let jsonData = NSData.init(contentsOfFile: path) {
do {
let dict = try JSONSerialization.jsonObject(with: jsonData as Data, options: .mutableContainers)
return dict as? NSDictionary
} catch {
print("Json parse error: ", error)
}
}
}
return nil
}
class func getAsyncMockData(complete: @escaping (_ result: NSDictionary) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if let dict = self.getMockData() {
complete(dict)
}
}
}
}
在这里模拟了异步获取数据并返回的情况。在界面上按下Button调用getData()
,该函数中会异步获取数据,并做一些操作。在调用ApiManager
之后直接pop自己,也就是说当异步调用数据返回的时候这个vc已经被释放了。此时会发生什么呢?在模拟器上运行这段代码,界面返回后并没有发生什么,看上去一切正常。但是控制台打出了一行字Warning: Attempt to present <UIAlertController: 0x7fb1b204da00> on <MVVMExample.SecondViewController: 0x7fb1b0623e40> whose view is not in the window hierarchy!
并且没有出现deinit
字样。我们来分析一下,控制台打出这段warning说明self.present()
被执行了,而没有deinit
说明vc没有被释放。为了验证这一点,我们使用Instruments里的Leak工具来查看这里是否有内存泄漏。结果显示这个地方果然有内存泄漏。
由于我们这里是用的self.present()
这种对view hierarchy的操作导致了内存泄漏,那么如果是一些比较轻的操作呢?
private func printTitle() {
print("I am \(self.title ?? "default value")")
}
@IBAction func getData(_ sender: Any) {
ApiManager.getAsyncMockData {(dict: NSDictionary) in
self.printTitle()
}
self.navigationController?.popViewController(animated: true)
}
我们将代码改成这样,并且运行。控制台打印出如下信息
I am Second ViewController
deinit Second ViewController
在这里vc完成了回调的闭包操作后正确释放,似乎并没有任何问题。但是如果异步回调闭包里面有大量计算或者会阻塞主线程的操作,那么将会影响到App的运行。举个典型的例子,从网络获取数据,处理数据并reload tableView。这种情况下就会影响到主线程。
显然,我们希望vc在pop之后立即释放。于是很自然的就想到了使用weak
@IBAction func getData(_ sender: Any) {
ApiManager.getAsyncMockData { [weak self] (dict: NSDictionary) in
let action = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
let actionController = UIAlertController(title: "Second ViewController Alert", message: dict.description, preferredStyle: .alert)
actionController.addAction(action)
self?.present(actionController, animated: true, completion: nil)
}
self.navigationController?.popViewController(animated: true)
}
再次运行。正如我们所期望的,vc正确释放并且闭包中对self的操作并没有执行。
那么如果我们使用[unowned self]
会怎么样?结果是Crash。因为unowned
相当于以前的unsafe_unretained
,当对象被释放时,unowned引用地址不会被指向nil
,而是维持原内存地址,而实际上该地址上的对象已经被释放,此时去访问这个地址,程序当然会崩溃。而weak
的对象在释放后会指向nil
,这样不会造成crash。
如何正确选择这两者的使用,Apple给我们的建议是如果能够确定在访问时不会已被释放的话,尽量使用 unowned,如果存在被释放的可能,那就选择用 weak。