无论OC中的Block还是Swift中的闭包Closure,经常因为使用不当从而造成循环引用从而导致内存泄漏,如何解闭包决循环引用问题,何时需要使用弱引用weak,又何时才该使用strong,一番研究后略有小感~
对于代理的循环引用及swift中delegate的使用请看:
Swift的代理delegate
该文章主要讲解:
使用闭包如何造成的循环引用问题
怎么使用weak解决循环引用问题
闭包内如何、何时使用strong
-
问题一
在这里我模拟一个老师Teacher
和学生Student
互动的场景,Teacher
提问askQuestion
Student
,Student
回答问题giveAnswer
,把问题的答案通过闭包回调giveAnswerClosure
给Teacher
,最后由Teacher
在回调的闭包giveAnswerClosure
中判断答案是否正确
Student.swift文件
class Student {
var name: String?
var giveAnswerClosure: ((Int) -> Void)?
// 学生回答问题
func giveAnswer() {
// 调用闭包给出答案
giveAnswerClosure?(1)
}
deinit {
print("deinit---Student")
}
}
Teacher.swift文件
class Teacher: NSObject {
var student: Student?
var isRight: Bool? // 答案是否正确
override init() {
super.init()
student = Student()
// 闭包回调
student?.giveAnswerClosure = { answer in
// 答案是1
self.isRight = answer == 1 ? true : false
}
}
// 提问问题
func askQuestion() {
// 学生回答
student?.giveAnswer()
}
deinit {
print("deinit---Teacher")
}
}
ViewController.swift文件
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let teacher = Teacher()
teacher?.askQuestion()
}
}
ViewController
对Teacher
是弱引用,viewDidLoad
方法执行完之后就不再对Teacher
进行引用,Teacher
就应该被释放掉了,还有Student
也是。
然而运行后却发现,Teacher
和Student
两者的deinit方法都没走,也就是说这两个对象都没有被释放掉!
原因分析:
(1)student
作为teacher
的一个属性,也就是说teacher
是对student
进行了强引用
class Teacher {
var student: Student
}
(2)因为student
闭包中调用了self.isRight
,因此闭包持有了self
对象,使得student
的闭包强引用着self
,这个self
也就是teacher
student?.giveAnswerClosure = { answer in
self.isRight = answer == 1 ? true : false
}
所以总结来说就是
teacher
是对student
进行了强引用
,同时student
的闭包内又对teacher
进行强引用
,因此两者之间形成了循环引用,导致teacher
对象无法释放,同样student
对象也无法释放。
解决方法
使用weak打破循环引用
// 写法一
student?.giveAnswerClosure = { [weak self] answer in
self?.isRight = answer == 1 ? true : false
}
// 写法二
weak var weakSelf = self
student?.giveAnswerClosure = { answer in
weakSelf?.isRight = answer == 1 ? true : false
}
使用weak之后,虽然teacher
对student
还是强引用,但是self?.isRight
对self
(也就是teacher
)就变成弱引用了,因此不再构成循环引用
所以当ViewController
的viewDidLoad
执行完毕后,就对teacher
不再进行引用,所以teacher
的retainCount变成了0后,teacher
就被释放了,然后student
也就被释放掉了
这时候,控制台便打印出了我们想要的结果:
deinit---Teacher
deinit---Student
-
问题二
如果把teacher
中的student
由全局变量,改为局部变量,并且在student
闭包中调用student
的name
,结果会怎样?
因为student
现在是局部变量,所以无法再在原来的askQuestion
中调用student?.giveAnswer()
,然后通过student
的giveAnswer
方法调用giveAnswerClosure
闭包,因此改为在初始化方法中手动调用giveAnswerClosure
。
Teacher.swift文件
class Teacher: NSObject {
var isRight: Bool? // 答案是否正确
override init() {
super.init()
let student = Student()
student.name = "Tony"
// 闭包回调
student.giveAnswerClosure = { [weak self] answer in
self?.isRight = answer == 1 ? true : false
print(student.name ?? "") // 增加对student的name属性的调用
}
// 手动调用student的闭包
student.giveAnswerClosure?(1)
}
}
结果运行后会发现,控制台打印信息为:
Tony
deinit---Teacher
也就是说在ViewController
的ViewDidLoad
方法执行完后,teacher
对象被释放了,但是student
对象却没有被释放!
原因分析:
1)teacher为什么被释放了?
其实,虽然现在student
对象改为了局部变量,但是它仍然是被self
强引用的,但是由于我们weak self
了,所以闭包中调用的self
是被weak
了的,teacher
并不会被student
强引用,因此当teacher
生命周期结束后就被释放了。
2)student为什么没有被释放?
这是因为,如果在闭包内,使用了外面的强引用student
对象(也就是student.giveAnswerClosure
闭包内使用了print(student.name ?? "")
,闭包内会自动产生一个强引用,引用着student
对象!所以student
不会被释放!
解决方法:
跟前边使用weak
解决self
的循环引用一样,只需要把student weak
一下,让student
在闭包中不再被强引用就OK了~
student.giveAnswerClosure = { [weak self, weak student] answer in
self?.isRight = answer == 1 ? true : false
print(student?.name ?? "")
}
控制台打印信息:
Tony
deinit---Student
deinit---Teacher
-
问题三
现在我们在问题二的基础上继续添加一个需求,要求在闭包回调的2秒后再打印学生的姓名。
于是代码修改如下:
student.giveAnswerClosure = { [weak self, weak student] answer in
self?.isRight = answer == 1 ? true : false
// 2秒后再打印学生的姓名
DispatchQueue.global().asyncAfter(deadline: .now() + 2, execute: {
print(student?.name ?? "nil")
})
}
结果执行完后打印信息为:
deinit---Student
deinit---Teacher
nil
可以发现打印出来的name
是空的!
原因很简单,因为asyncAfter
代码块内的代码是延迟2秒之后执行的,并且不会阻塞线程,所以任务会一直往下走,很快student
就被释放了,所以当2秒后打印的时候student
已经是nil。
解决方法:使用strong
student.giveAnswerClosure = { [weak self, weak student] answer in
self?.isRight = answer == 1 ? true : false
let strongStudent = student // 使用strong
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
print(strongStudent?.name ?? "nil")
})
}
deinit---Teacher
Tony
deinit---Student
首先我们要先搞明白这几个问题:
- 在问题二中,其闭包内是使用的闭包外面的强引用
student
对象,从而导致闭包内会自动产生一个强引用,引用着student
对象。 - 而本问题三中的
strongStudent
是闭包内部的强引用,而不是闭包外部强引用。 - 所以本问题三中的闭包内声明的强引用
strongStudent
不管怎么访问都是不会干扰外部的对象,也不会自动产生一个强引用造成循环引用。
因此在2秒后的延迟执行时,由于闭包中强引用strongStudent
的存在,可以正常打印出name
。
但是由于GCD
的asyncAfter
代码块内部用到print(strongStudent?.name ?? "nil")
,用到了闭包外部的强引用对象strongStudent
,所以asyncAfter
内部会自动生成一个强引用,引用着外部对象strongStudent
。
因此当2秒后执行完打印语句后,asyncAfter
代码块执行完毕,这时GCD
系统内部不会再去强引用asyncAfter
的闭包,然后asyncAfter
也不再强引用strongStudent
,所以strongStudent
被释放,至此student
对象内所有资源被释放完毕,最终student
对象释放。
因此在实际开发中,在会导致循环引用的闭包中有时(就像刚才那样)会出现要使用的对象为空的情况,如果我们又必须需要使用这个对象进行一些很必要的操作时,那么对于这种情况就要果断使用strong了。
Game Over 😄