Swift 閉包(Closures)

我的博客

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都是引用類型辫塌,因此在上面的代碼中,studentjudgeClosure都指向各種對象的strong reference派哲。

同時由于在閉包中捕獲了student,因此judgeClosure閉包就有了一個指向student的強引用臼氨。最后當執(zhí)行student.replyClosure = judgeClosure之后,讓replyClosure也成了judgeClosure的強引用芭届。此時student的引用計數(shù)為1储矩,judgeClosure的引用計數(shù)是2。

  • 當超過作用域后喉脖,studentjudgeClosure之間的引用關(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)用selfreplyClosure閉包已經(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)系是:

closure004.png

Objective-C中一般采用弱引用的方式解決Block和實例循環(huán)引用的問題砸捏,這里對Block和類實例之間產(chǎn)生循環(huán)引用的原因不做贅述,關(guān)于Objective-CBlock的更多使用細節(jié)可查閱Objective-C高級編程: iOS與OS X多線程和內(nèi)存管理一書和蘋果官方文檔Getting Started with Blocks的內(nèi)容隙赁。那么在Swift中該如何處理循環(huán)引用呢垦藏?在Swift中需要根據(jù)closureclass對象生命周期的不同,而采用不同的方案來解決循環(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==========")
    }
}

這里只考慮studentteacher之間的引用關(guān)系芭挽,此時studentclosure`之間存在著強引用關(guān)系,他們的引用計數(shù)都是2蝗肪,在內(nèi)存中他們之間的引用關(guān)系為:

closure007.png

超過作用域后袜爪,由于互相存在強引用studentclousre的引用計數(shù)并不為0,所以內(nèi)存無法銷毀薛闪,此時他們之間的引用關(guān)系為:
closure008.png

對于這種引用關(guān)系在前面的ARC就已經(jīng)說過辛馆,把循環(huán)的任意一方變成unownedweak就好了。我將student設(shè)置為無主引用豁延,代碼如下:

let judgeClosure = { [unowned student] (answer: Answer) in
    print("\(student.name) is \(answer)")
}

使用無主引用后昙篙,他們之間的引用關(guān)系如下圖所示:


closure009.png

運行代碼并打印:

// 運行
Teacher()
// 打印結(jié)果
==========Teacher init==========
==========Student init==========Kate Bell
==========Student deinit==========Kate Bell
==========Teacher deinit==========

可以看到studentteacher都可以正常被回收了诱咏,說明closure的內(nèi)存也被回收了苔可。當closurenil時,student對象就會被ARC回收袋狞,而當studentnil時焚辅,teacher也就失去了他的作用會被ARC回收其內(nèi)存。

closure010.png

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)不存了
  • 名為Tomstudent對象引用了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)系為:

closure004.png

對于這種情況使用unowned并不能解決循環(huán)引用問題茄茁,所以只能采用另一種解決循環(huán)引用的方案弱引用(weak),來告訴closureclosure所捕獲的對象已經(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)系是:

closure005.png

當我使用了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)")
}

當離開作用域后,studentclosure的引用計數(shù)都為0嗦嗡,他們的內(nèi)存就會合理的釋放勋锤,他們之間的引用關(guān)系如下圖所示:

closure006.png

關(guān)于closure和類對象之間的循環(huán)問題,如何判斷兩者之間是否會產(chǎn)生循環(huán)引用侥祭,要根據(jù)一個類對象是否真的擁有正在使用的closure叁执。如果類對象沒有持有這個closure,那么就不必考慮循環(huán)引用問題卑硫。

6.2徒恋、 weak并不能解決所有的循環(huán)引用問題

雖然unownedweak能夠解決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)系并合理的使用weakunowned才能從本質(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

容易讓人犯錯的closure內(nèi)存管理

本文demo

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市焙格,隨后出現(xiàn)的幾起案子图毕,更是在濱河造成了極大的恐慌,老刑警劉巖眷唉,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件予颤,死亡現(xiàn)場離奇詭異,居然都是意外死亡冬阳,警方通過查閱死者的電腦和手機蛤虐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肝陪,“玉大人驳庭,你說我怎么就攤上這事。” “怎么了饲常?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵蹲堂,是天一觀的道長。 經(jīng)常有香客問我贝淤,道長柒竞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任播聪,我火速辦了婚禮朽基,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘离陶。我一直安慰自己踩晶,他們只是感情好,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布枕磁。 她就那樣靜靜地躺著,像睡著了一般术吝。 火紅的嫁衣襯著肌膚如雪计济。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天排苍,我揣著相機與錄音沦寂,去河邊找鬼。 笑死淘衙,一個胖子當著我的面吹牛传藏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播彤守,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼毯侦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了具垫?” 一聲冷哼從身側(cè)響起侈离,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎筝蚕,沒想到半個月后卦碾,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡起宽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年洲胖,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坯沪。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡绿映,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出屏箍,到底是詐尸還是另有隱情绘梦,我是刑警寧澤橘忱,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站卸奉,受9級特大地震影響钝诚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜榄棵,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一凝颇、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧疹鳄,春花似錦拧略、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至腺怯,卻和暖如春袱饭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背呛占。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工虑乖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人晾虑。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓疹味,卻偏偏與公主長得像,于是被迫代替她去往敵國和親帜篇。 傳聞我的和親對象是個殘疾皇子糙捺,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容