1曙咽、What's the closure?
作為iOS
開發(fā)者對于Objective-C
中的Block
一定非常熟悉,在其他開發(fā)語言中嚣崭,也把closure
也稱作lambdas
等。簡答來說懦傍,閉包就是一個獨立的函數(shù)雹舀,一般用于捕獲和存儲定義在其上下文中的任何常量和變量的引用。
closure
的語法如下:
{ (parameters) -> return type in
statements
}
closure
能夠使用常量形式參數(shù)粗俱、變量形式參數(shù)和輸入輸出形式的參數(shù)说榆,但不能設(shè)置默認值〈缛希可變形式參數(shù)也可以使用签财,但需要在行參列表的最后使用。元組也可以被用來作為形式參數(shù)和返回類型偏塞。
實際上全局函數(shù)和內(nèi)嵌函數(shù)也是一種特殊的閉包唱蒸,(關(guān)于函數(shù)的相關(guān)概念可參考官方文檔The Swift Programming Language: Functions),閉包會根據(jù)其捕獲值的情況分為三種形式:
- 全局函數(shù)是一個有名字但不會捕獲任何值的閉包
- 內(nèi)嵌函數(shù)是一個有名字且能從其上層函數(shù)捕獲值的閉包
- 閉包表達式是一個輕量級語法所寫的可以捕獲其上下文中常量或變量值的沒有名字的閉包
2灸叼、各種不同類型的閉包
如果需要將一個很長的閉包表達式作為函數(shù)最后一個實際參數(shù)傳遞給函數(shù)神汹,使用尾隨閉包將增強函數(shù)的可讀性。尾隨閉包一般作為函數(shù)行參使用古今。如系統(tǒng)提供的sorted
,map
等函數(shù)就是一個尾隨閉包屁魏。
2.1、尾隨閉包(Trailing Closure)
尾隨閉包雖然寫在函數(shù)調(diào)用的括號之后捉腥,但仍是函數(shù)的參數(shù)蚁堤。使用尾隨閉包是,不要將閉包的參數(shù)標簽作為函數(shù)調(diào)用的一部分但狭。
let strList = ["1","2","3","4","5"]
let numList: [Int] = strList.map { (num) in
return Int(num) ?? 0
}
2.2、逃逸閉包 (Escaping Closure)
當閉包作為一個實際參數(shù)傳遞給一個函數(shù)的時候撬即,并且它會在函數(shù)返回之后調(diào)用立磁,我們就說這個閉包逃逸,一般用@escaping
修飾的函數(shù)形式參數(shù)來標明逃逸閉包剥槐。
- 逃逸閉包一般用于異步任務(wù)的回調(diào)
- 逃逸閉包在函數(shù)返回之后調(diào)用
- 讓閉包
@escaping
意味著必須在閉包中顯式地引用self
// 逃逸閉包
func requestServer(with URL: String,parameter: @escaping(AnyObject?, Error?) -> Void) {
}
// 尾隨閉包
func requestServerTrailing(losure: () -> Void) {
}
class EscapingTest {
var x = 10
func request() {
// 尾隨閉包
requestServerTrailing {
x = x + 1
}
// 逃逸閉包
requestServer(with: "") { (obj, error) in
x = x + 1
}
}
}
Reference to property 'x' in closure requires explicit use of 'self' to make capture semantics explicit
修改代碼:
requestServer(with: "") { [weak self] (obj, error) in
guard let self = `self` else {
return
}
self.x = self.x + 1
}
- 逃逸閉包的實際使用
如我要設(shè)計一個下載圖片管理類唱歧,異步下載圖片下載完成后再返回主界面顯示,這里就可以使用逃逸閉包來實現(xiàn),核心代碼如下:
struct DownLoadImageManager {
// 單例
static let sharedInstance = DownLoadImageManager()
let queue = DispatchQueue(label: "com.tsn.demo.escapingClosure", attributes: .concurrent)
// 逃逸閉包
// path: 圖片的URL
func downLoadImageWithEscapingClosure(path: String, completionHandler: @escaping(UIImage?, Error?) -> Void) {
queue.async {
URLSession.shared.dataTask(with: URL(string: path)!) { (data, response, error) in
if let error = error {
print("error===============\(error)")
DispatchQueue.main.async {
completionHandler(nil, error)
}
} else {
guard let responseData = data, let image = UIImage(data: responseData) else {
return
}
DispatchQueue.main.async {
completionHandler(image, nil)
}
}
}.resume()
}
}
// 保證init方法在外部不會被調(diào)用
private init() {
}
}
下載圖片并顯示:
let path = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Finews.gtimg.com%2Fnewsapp_match%2F0%2F12056372662%2F0.jpg&refer=http%3A%2F%2Finews.gtimg.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1618456758&t=3df7a5cf69ad424954badda9bc7fc55f"
DownLoadImageManager.sharedInstance.downLoadImageWithEscapingClosure(path: path) { (image: UIImage?, error: Error?) in
if let error = error {
print("error===============\(error)")
} else {
guard let image = image else { return }
print("圖片下載完成颅崩,顯示圖片: \(image)")
let imageView = UIImageView(image: image)
imageView.layer.cornerRadius = 5
}
}
以上的代碼雖然能夠完成對圖片的下載管理几于,事實上在項目中下載并顯示一張圖片的處理要復(fù)雜的多金蜀,這里不做更多贅述痹仙,可參考官方的demo: Asynchronously Loading Images into Table and Collection Views
2.3、自動閉包 (Auto Closure)
- 自動閉包是一種自動創(chuàng)建的用來把座位實際參數(shù)傳遞給函數(shù)的表達式打包的閉包
- 自動閉包不接受任何參數(shù)朦佩,并且被調(diào)用時尖滚,會返回內(nèi)部打包的表達式的值
- 自動閉包能過省略閉包的大括號喉刘,用一個普通的表達式來代替顯式的閉包
- 自動閉包允許延遲處理,因此閉包內(nèi)部的代碼直到調(diào)用時才會運行漆弄。對于有副作用或者占用資源的代碼來說很用
如我有個家庭作業(yè)管理類睦裳,老師需要統(tǒng)計學生上交的作業(yè)同時處理批改后的作業(yè),為了演示自動閉包撼唾,我用以下的代碼來實現(xiàn):
enum Course {
case spacePhysics // 空間物理
case nuclearPhysics // 原子核物理
case calculus // 微積分
case quantumMechanics // 量子力學
case geology // 地質(zhì)學
}
struct StudentModel {
var name: String = String()
var course: Course!
init(name: String, course: Course) {
self.name = name
self.course = course
}
}
// MARK: - 自動閉包
class StudentManager {
var studentInfoArray: [StudentModel] = [StudentModel]()
// 某個學生交了作業(yè)
func autoAddWith(_ student: @autoclosure() -> StudentModel) {
studentInfoArray.append(student())
}
// 老師批改完了某個學生的作業(yè)
func autoDeleteWith(_ index: @autoclosure() -> Int) {
studentInfoArray.remove(at: index())
}
}
其中廉邑,autoAddWith
表示學生某個學生交了作業(yè),autoDeleteWith
表示老師批改完了某個學生的作業(yè)倒谷。一般調(diào)用方式為:
let studentManager: StudentManager = StudentManager()
// Kate Bell 交了作業(yè)
studentManager.autoAddWith(StudentModel(name: "Kate Bell", course: .spacePhysics))
// Kate Bell 交了作業(yè)
studentManager.autoAddWith(StudentModel(name: "Kate Bell", course: .nuclearPhysics))
// Anna Haro 交了作業(yè)
studentManager.autoAddWith(StudentModel(name: "Anna Haro", course: .calculus))
// 老師批改完了第一份作業(yè)
studentManager.autoDeleteWith(0)
2.4蛛蒙、自動 + 逃逸 (Autoclosure + Escaping )
如果想要自動閉包逃逸,可以同時使用@autoclosure
和@escaping
來標志恨锚。
func autoAddWith(_ student: @autoclosure @escaping() -> StudentModel) {
studentInfoArray.append(student())
}
3宇驾、閉包捕獲值
前面簡單介紹了尾隨閉包、逃逸閉包猴伶、自動閉包的概念和基本使用课舍,這里來說閉包是如何捕獲值的。在Swift中他挎,值類型變量一般存儲于棧(Stack)中筝尾,而像func class closure
等引用類型存儲于堆(Heap)內(nèi)存中。而closure
捕獲值本質(zhì)上是將存在棧(Stack)區(qū)的值存儲到堆(Heap)區(qū)办桨。
為了驗證closure
可以捕獲哪些類型的值筹淫,用下面的代碼做一個測試:
class Demo: NSObject {
var test = String()
}
// 常量
let index = 10086
// 變量
var number = 1008611
// 引用類型
let demo = Demo()
var capturel = {
number = 1008611 - 998525
demo.test = "block test"
print("index==========\(index)")
print("number==========\(number)")
print("demo.test==========\(demo.test)")
}
number = number + 1
demo.test = "test"
capturel()
// 打印結(jié)果
// index==========10086
// number==========10086
// demo.test==========block test
上面的代碼中,無論是常量呢撞、變量损姜、還是引用類型調(diào)用capturel()
后都可以正常打印數(shù)據(jù)。 無論是常量殊霞、變量摧阅、值類型還是引用類型,Closure
都可捕獲其值绷蹲。事實上在Swift中作為優(yōu)化當Closure
中并沒有修改或者在閉包的外面的值時棒卷,Swift可能會使用這個值的copy而不是捕獲顾孽。同時Swift也處理了變量的內(nèi)存管理操作,當變量不再需要時會被釋放比规。
在來看一個實現(xiàn)遞增的例子:
func makeIncrementer(_ amount: Int) -> () -> Int {
var total = 0
// 內(nèi)嵌函數(shù) 也是一種特殊的Closure
func incrementerClosure() -> Int {
total = total + amount
return total
}
return incrementerClosure
}
在上面的代碼中,incrementerClosure
中捕獲了total
值若厚,當我返回incrementerClosure
時,理論上包裹total
的函數(shù)就不存在了蜒什,但是incrementerClosure
仍然可以捕獲total
值测秸。可以得出結(jié)論:即使定義這些變量或常量的原作用域已經(jīng)不存在了吃谣,但closure
依舊能捕獲這個值乞封。
let incrementerTen = makeIncrementer(10) // () -> Int
incrementerTen() // 10
incrementerTen() // 20
incrementerTen() // 30
let incrementerSix = makeIncrementer(6) // () -> Int
incrementerSix() // 6
incrementerSix() // 12
incrementerTen() // 40
let alsoIncrementerTen = incrementerTen // () -> Int
alsoIncrementerTen() // 50
在上面的代碼中,調(diào)用了遞增閉包incrementerTen
每次+10岗憋,當我新建一個incrementerSix
閉包時就變成了+6遞增肃晚,也就說產(chǎn)生了一個新的變量引用。
當調(diào)用alsoIncrementerTen
后仔戈,返回的值是50关串,這里可以確定Closure
是引用類型,是因為alsoIncrementerTen
引用了incrementerTen
他們共享同一個內(nèi)存。如果是值類型监徘,alsoIncrementerTen
返回的結(jié)果會是10,而不是50晋修;
根據(jù)上面的代碼關(guān)于閉包捕獲值做出總結(jié):
-
closure
捕獲值本質(zhì)上是將存在棧(Stack)區(qū)的值存儲到堆(Heap)區(qū) - 當
Closure
中并沒有修改或者在閉包的外面的值時,Swift可能會使用這個值的copy而不是捕獲 -
Closure
捕獲值時即使定義這些變量或常量的原作用域已經(jīng)不存在了closure
依舊能捕獲這個值 - 如果建立了一個新的閉包調(diào)用凰盔,將會產(chǎn)生一個新的獨立的變量的引用
- 無論什么時候賦值一個函數(shù)或者閉包給常量或變量墓卦,實際上都是將常量和變量設(shè)置為對函數(shù)和閉包的引用
4、Closure循環(huán)引用
Swift中的closure
是引用類型,我們知道Swift中的引用類型是通過ARC
機制來管理其內(nèi)存的户敬。在Swift中落剪,兩個引用對象互相持有對方時回產(chǎn)生強引用環(huán),也就是常說的循環(huán)引用尿庐。雖然在默認情況下忠怖,Swift能夠處理所有關(guān)于捕獲的內(nèi)存的管理的操作,但這并不能讓開發(fā)者一勞永逸的不去關(guān)心內(nèi)存問題抄瑟,因為相對于對象產(chǎn)生的循環(huán)引用Closure
產(chǎn)生循環(huán)引用的情況更復(fù)雜凡泣,所以在使用Closure
時應(yīng)該更小心謹慎。那么在使用Closure
時一般哪些情況會產(chǎn)生循環(huán)引用問題呢皮假?
4.1鞋拟、Closure
捕獲對象產(chǎn)生的循環(huán)引用
當分配了一個Closure
給實例的屬性,并且Closure
通過引用該實例或者實例的成員來捕獲實例惹资,將會在Closure
和實例之間產(chǎn)生循環(huán)引用严卖。
這里我用學生Student
類來做演示,假設(shè)現(xiàn)在學生需要做一個單項選擇題,老師根據(jù)其返回的答案來判斷是否正確布轿。我將對照Objective-C
中的Block
來做一個對比,在Xcode中編寫如下代碼:
typedef NS_ENUM(NSInteger, AnswerEnum) {
A,
B,
C,
D,
};
@interface Student : NSObject
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) void (^replyClosure)(AnswerEnum answer);
@end
@implementation Student
- (instancetype)init {
self = [super init];
if (self) {
if (self.replyClosure) {
self.replyClosure(B);
}
}
return self;
}
@end
@interface Teacher : NSObject
@property (assign, nonatomic) BOOL isRight;
@property (strong, nonatomic) Student *student;
@end
@implementation Teacher
- (instancetype)init {
self = [super init];
if (self) {
self.student.replyClosure = ^(AnswerEnum answer) {
// Capturing 'self' strongly in this block is likely to lead to a retain cycle
NSLog(@"%@",self.student.name);
};
}
return self;
}
@end
其實上面的代碼,不用運行在Build的時候就會警告Capturing 'self' strongly in this block is likely to lead to a retain cycle
。
那么在Swift中使用closure
是否同樣也會產(chǎn)生循環(huán)引用呢汰扭?我把Objective-C
代碼轉(zhuǎn)換成Swift
:
enum Answer {
case A
case B
case C
case D
}
class Student: CustomStringConvertible {
var name: String = String()
var replyClosure: (Answer) -> Void = { _ in }
var description: String {
return "<Student: \(name)>"
}
init(name: String) {
self.name = name
print("==========Student init==========\(name)")
replyClosure(.B)
}
deinit {
print("==========Student deinit==========\(self.name)")
}
}
class Teacher {
var isRight: Bool = false
init() {
print("==========Teacher init==========")
let student = Student(name: "Kate Bell")
let judgeClosure = { (answer: Answer) in
print("\(student.name) is \(answer)")
}
student.replyClosure = judgeClosure
}
deinit {
print("==========Teacher deinit==========")
}
}
Student
類有兩個屬性:name
表示學生姓名稠肘,replyClosure
表示學生回答問題這一動作并返回答題結(jié)果。
// 調(diào)用并運行代碼
Teacher()
// 打印結(jié)果
==========Teacher init==========
==========Student init==========Kate Bell
==========Teacher deinit==========
運行上面的代碼萝毛,通過打印結(jié)果可以看到Student
類并沒有調(diào)用deinit
方法项阴,此處說明Student
在被初始化后內(nèi)存并沒有釋放。實際上在judgeClosure
內(nèi)部笆包,只要我調(diào)用(捕獲)了student
环揽,無論是任何操作,該部分內(nèi)存都不能有效釋放了。那么為什么會造成這種現(xiàn)象呢庵佣?下面做逐步分析:
- 當我調(diào)用了閉包之后歉胶,閉包才會捕獲值,在執(zhí)行
student.replyClosure = judgeClosure
之后巴粪,在內(nèi)存中他們的關(guān)系是這樣的:
closure001.png
在Swift中通今,class、func肛根、closure
都是引用類型辫塌,因此在上面的代碼中,student
和judgeClosure
都指向各種對象的strong reference
派哲。
同時由于在閉包中捕獲了student
,因此judgeClosure
閉包就有了一個指向student
的強引用臼氨。最后當執(zhí)行student.replyClosure = judgeClosure
之后,讓replyClosure
也成了judgeClosure
的強引用芭届。此時student
的引用計數(shù)為1储矩,judgeClosure
的引用計數(shù)是2。
- 當超過作用域后喉脖,
student
和judgeClosure
之間的引用關(guān)系是這樣的:
closure002.png
此時椰苟,只有Closure
對象的引用計數(shù)變成了1。于是Closure
繼續(xù)引用了student
树叽,student
繼續(xù)引用了他的對象replyClosure
舆蝴,而這個對象繼續(xù)引用著judgeClosure
。這樣就造成了一個引用循環(huán)题诵,所以就會出現(xiàn)內(nèi)存無法正常釋放的情況洁仗。
4.2、closure屬性的內(nèi)部實現(xiàn)捕獲self產(chǎn)生的循環(huán)引用
同樣的這里我先利用Objective-C
的代碼來舉例性锭,修改Student
類的代碼如下:
@implementation Student
- (instancetype)init {
self = [super init];
if (self) {
if (self.replyClosure) {
self.replyClosure = ^(AnswerEnum answer) {
// Capturing 'self' strongly in this block is likely to lead to a retain cycle
NSLog(@"%@",self);
};
}
}
return self;
}
@end
同樣的在Build時編譯器會警告赠潦,Capturing 'self' strongly in this block is likely to lead to a retain cycle
。
在Swift中雖然編譯器不會警告草冈,但也會產(chǎn)生同樣產(chǎn)生循環(huán)引用問題她奥。修改Student
中定義replyClosure
代碼如下:
lazy var replyClosure: (Answer) -> Void = { _ in
print("replyClosure self=============\(self)")
}
為了保證在replyClosure
內(nèi)部調(diào)用self
時replyClosure
閉包已經(jīng)正確初始化了瓮增,所以采用了lazy
懶加載的方式。修改調(diào)用的代碼為:
Student(name: "Tom").replyClosure(.B)
運行代碼哩俭,打印結(jié)果:
==========Student init==========Kate Bell
replyClosure self=============<Student: Tom>
由于Student
實例和replyClosure
是互相強持有關(guān)系绷跑,即使超出了作用域他們之間依然存在著引用,所以內(nèi)存不能有效釋放凡资。此時他們之間的引用關(guān)系是:
在
Objective-C
中一般采用弱引用的方式解決Block
和實例循環(huán)引用的問題砸捏,這里對Block
和類實例之間產(chǎn)生循環(huán)引用的原因不做贅述,關(guān)于Objective-C
中Block
的更多使用細節(jié)可查閱Objective-C高級編程: iOS與OS X多線程和內(nèi)存管理一書和蘋果官方文檔Getting Started with Blocks的內(nèi)容隙赁。那么在Swift中該如何處理循環(huán)引用呢垦藏?在Swift中需要根據(jù)closure
和class
對象生命周期的不同,而采用不同的方案來解決循環(huán)引用問題伞访。
5掂骏、無主引用(unowned)
5.1、使用unowned處理closure和類對象的引用循環(huán)
為了更易理解咐扭,我修改Teacher
類的代碼:
class Teacher {
var isRight: Bool = false
init() {
print("==========Teacher init==========")
let student = Student(name: "Kate Bell")
let judgeClosure = { [student] (answer: Answer) in
print("student===========\(student)")
}
student.replyClosure = judgeClosure
}
deinit {
print("==========Teacher deinit==========")
}
}
這里只考慮student
和teacher
之間的引用關(guān)系芭挽,此時student和
closure`之間存在著強引用關(guān)系,他們的引用計數(shù)都是2蝗肪,在內(nèi)存中他們之間的引用關(guān)系為:
超過作用域后袜爪,由于互相存在強引用
student
和clousre
的引用計數(shù)并不為0,所以內(nèi)存無法銷毀薛闪,此時他們之間的引用關(guān)系為:對于這種引用關(guān)系在前面的ARC就已經(jīng)說過辛馆,把循環(huán)的任意一方變成unowned
或weak
就好了。我將student
設(shè)置為無主引用豁延,代碼如下:
let judgeClosure = { [unowned student] (answer: Answer) in
print("\(student.name) is \(answer)")
}
使用無主引用后昙篙,他們之間的引用關(guān)系如下圖所示:
運行代碼并打印:
// 運行
Teacher()
// 打印結(jié)果
==========Teacher init==========
==========Student init==========Kate Bell
==========Student deinit==========Kate Bell
==========Teacher deinit==========
可以看到student
和teacher
都可以正常被回收了诱咏,說明closure
的內(nèi)存也被回收了苔可。當closure
為nil
時,student
對象就會被ARC
回收袋狞,而當student
為nil
時焚辅,teacher
也就失去了他的作用會被ARC回收其內(nèi)存。
5.2苟鸯、unowned
并不能解決所有的循環(huán)引用問題
雖然unowned
能解決循環(huán)引用問題同蜻,但并不意味著,遇到的所有closure
循環(huán)引用問題都可以用無主引用(unowned)來解決:
5.2.1早处、示例代碼一
同樣用Student
對象來舉例湾蔓,在"Kate Bell"學生回答完問題后,另一個Tom
又回答了問題砌梆,他選擇了C答案默责,代碼如下:
var student = Student(name: "Kate Bell")
let judgeClosure = { [unowned student] (answer: Answer) in
print("student===========\(student)")
}
student = Student(name: "Tom")
student.replyClosure = judgeClosure
student.replyClosure(.C)
// 打印結(jié)果
// ==========Student init==========Kate Bell
// ==========Student init==========Tom
// ==========Student deinit==========Kate Bell
運行代碼贬循,程序會Crash并報錯,error: Execution was interrupted, reason: signal SIGABRT.
來分析一下為什么會這樣:
- 代碼中首先創(chuàng)建了一個名為
Kate Bell
的學生對象傻丝,judgeClosure
捕獲了這個student
對象 - 當
student = Student(name: "Tom")
之后甘有,由于judgeClosure
是按照unowned
的方式捕獲的,此時judgeClosure
內(nèi)的student
對象實際上已經(jīng)不存了 - 名為
Tom
的student
對象引用了replyClosure
閉包 - 調(diào)用
student.replyClosure(.C)
的時候葡缰,replyClosure
之前捕獲的student
對象已經(jīng)不存在,此時就產(chǎn)生了Crash
5.2.1忱反、示例代碼二
那么泛释,如果我將student.replyClosure = judgeClosure
移動到最前面呢?修改代碼如下:
var student = Student(name: "Kate Bell")
let judgeClosure = { [unowned student] (answer: Answer) in
print("student===========\(student)")
}
student.replyClosure = judgeClosure
student = Student(name: "Tom")
student.replyClosure(.C)
// 打印結(jié)果
// ==========Student init==========Kate Bell
// ==========Student init==========Tom
// ==========Student deinit==========Kate Bell
可以看到温算,名為"Kate Bell"
的student
對象正常銷毀了怜校,但是Tom
學生對象并沒有正常銷毀,這是由于replyClosure
閉包在其內(nèi)部捕獲了self
造成的循環(huán)引用注竿。此時他們之間的引用關(guān)系為:
對于這種情況使用unowned
并不能解決循環(huán)引用問題茄茁,所以只能采用另一種解決循環(huán)引用的方案弱引用(weak),來告訴closure
當closure
所捕獲的對象已經(jīng)被釋放時,就不用在訪問這個對象了巩割。
6裙顽、弱引用(weak)
6.1、使用weak處理closure和類對象之間的引用循環(huán)
為了解決上面的的循環(huán)引用問題宣谈,我把replyClosure
的代碼修改為:
lazy var replyClosure: (Answer) -> Void = { [weak self] _ in
print("replyClosure self=============\(self)")
}
重新執(zhí)行代碼愈犹,可以看到Tom
學生對象可以正常釋放了:
// ==========Student init==========Kate Bell
// ==========Student init==========Tom
// ==========Student deinit==========Kate Bell
// ==========Student deinit==========Tom
讓self
為弱引用后,student
之間的引用關(guān)系是:
當我使用了weak
時闻丑,就意味這這個對象可能為nil
,而在closure
里捕獲和使用一個Optional
的值可能會發(fā)生一些不可預(yù)期的問題漩怎,此處需要做unwrap
操作:
lazy var replyClosure: (Answer) -> Void = { [weak self] _ in
guard let value = self else { return }
print("replyClosure self=============\(value)")
}
當離開作用域后,student
和closure
的引用計數(shù)都為0嗦嗡,他們的內(nèi)存就會合理的釋放勋锤,他們之間的引用關(guān)系如下圖所示:
關(guān)于closure
和類對象之間的循環(huán)問題,如何判斷兩者之間是否會產(chǎn)生循環(huán)引用侥祭,要根據(jù)一個類對象是否真的擁有正在使用的closure
叁执。如果類對象沒有持有這個closure
,那么就不必考慮循環(huán)引用問題卑硫。
6.2徒恋、 weak并不能解決所有的循環(huán)引用問題
雖然unowned
和weak
能夠解決Closure
和類實例之間的循環(huán)引用問題,但這并不表示在任何Closure
中都可以使用這種方案來解決問題欢伏。相反有時候濫用弱引用還會給帶來一些詭異的麻煩和內(nèi)存問題入挣。
6.2.1、濫用弱引用可能會造成一些不必要的麻煩
這里我同樣用Student
類來舉例硝拧,為學生添加一個寫作業(yè)的任務(wù):
func doHomeWork() {
// 全局隊列
let queue = DispatchQueue.global()
queue.async { [weak self] in
print("\(self?.name):開始寫作業(yè)")
sleep(2)
print("\(self?.name):完成作業(yè)")
}
}
// 模擬做家庭作業(yè)
Student(name: "Kate Bell").doHomeWork()
打印結(jié)果:
==========Student init==========
==========Student deinit==========
Optional("Kate Bell"):開始寫作業(yè)
nil:完成作業(yè)
為什么完成的作業(yè)是nil
呢径筏?實際上這里并不需要使用弱引用葛假,因為async
方法中使用的closure
并不歸student
對象持有,雖然closure
會捕獲student
對象滋恬,但這兩者之間并不會產(chǎn)生循環(huán)引用聊训,反而因為弱引用的問題學生對象被提前釋放了,但是如果這里我使用了強制拆包就又可能導致程序Crash恢氯。所以正確的理解closure
和類對象的引用關(guān)系并合理的使用weak
和unowned
才能從本質(zhì)上解決問題带斑。
6.2.2、使用withExtendedLifetime改進這個問題
那么有沒有方法可以避免在錯誤的使用了weak
之后造成的問題呢勋拟?這里可以使用Swift提供的withExtendedLifetime
函數(shù)勋磕,它有兩個參數(shù): 第一個參數(shù)是要延長生命的對象,第二個對象是clousre
敢靡,在這個closure
返回之前挂滓,第一個參數(shù)會一直存活在內(nèi)存中,修改async closure
里的代碼如下:
let queue = DispatchQueue.global()
queue.async { [weak self] in
withExtendedLifetime(self) {
print("\(self?.name):開始寫作業(yè)")
sleep(2)
print("\(self?.name):完成作業(yè)")
}
}
重新編譯代碼啸胧,打印結(jié)果:
==========Student init==========
Optional("Kate Bell"):開始寫作業(yè)
Optional("Kate Bell"):完成作業(yè)
==========Student deinit==========
6.2.3赶站、改進withExtendedLifetime語法
雖然withExtendedLifetime
能過解決弱引用問題,如果有很多地方有要這樣訪問對象這樣就很麻煩纺念。這里有一個解決方案是對withExtendedLifetime
做一個封裝贝椿,,給Optional
做一個擴展(extension)處理:
extension Optional {
func withExtendedLifetime(_ body: (Wrapped) -> Void) {
guard let value = self else { return }
body(value)
}
}
調(diào)用的代碼:
func doHomeWork() {
// 全局隊列
let queue = DispatchQueue.global()
queue.async { [weak self] in
self.withExtendedLifetime { _ in
print("\(self?.name):開始寫作業(yè)")
sleep(2)
print("\(self?.name):完成作業(yè)")
}
}
}
最終打印的結(jié)果和之前一樣,并且我還可以其他地方調(diào)用:
==========Student init==========
Optional("Kate Bell"):開始寫作業(yè)
Optional("Kate Bell"):完成作業(yè)
==========Student deinit==========
本文主要介紹了closure
基本概念柠辞、closure
的類型团秽、closure
和類對象之間的內(nèi)存問題及其解決方法,如果您發(fā)現(xiàn)我的理解有錯誤的地方叭首,請指出习勤。
本文參考:
The Swift Programming Language: Closures
The Swift Programming Language: Automatic Reference Counting