ARC and Memory Management in Swift Part 3

Tiếp theo từ phần trước

Link tutorial gốc

Reference Cycles with Closures

Reference cycles của các objects xuất hiện khi các property reference lẫn nhau. Giống như object, closures cũng là reference types nên cũng có thể xảy ra cycles. Closures giữ lại đối tượng chỗ nó khai báo để thực hiện khối lệnh ở trong (Closures capture (or close over) the objects that they operate on.) ví dụ: nếu 1 closure được assigned như 1 property của 1 class, và closure sử dụng instance property của class đó, bạn sẽ có 1 reference cycle. Nói cách khác, object giữ 1 reference tới closure thông qua 1 stored property, closure lại giữ 1 reference tới object thông qua captured value of self:

Thêm đoạn code sau vào CarrierSubscription, ngay sau property user:

lazy var completePhoneNumber: () -> String = {
  self.countryCode + " " + self.number
}

Closure này tính toán và trả về 1 số điện thoại đầy đủ (gồm số điện thoại và mã quốc gia). Closure này được khai báo là lazy nghĩa là nó sẽ không bị assigned cho đến khi sử dụng lần đầu. Bắt buộc phải dùng lazy bởi vì bên trong closure, ta sử dụng self.countryCodeself.number, mà những thứ này không thể available trừ khi hàm init chạy xong. Thêm dòng sau vào bên trong do block:

print(subscription1.completePhoneNumber())

Bạn có thể thấy ngay ở dưới: user1iPhone được deallocate nhưng CarrierSubscription thì không, do strong reference cycle giữa object và closure. Swift có 1 cách vô cùng thanh lịch và đơn giản để phá vỡ strong reference cycles trong closures. Bạn khai báo 1 capture list trong đó bạn định nghĩa rõ mỗi quan hệ giữa closure và objects mà nó captures. Để minh hoạ rõ ràng capture list hoạt động, bạn hãy xem xét đoạn code sau đây:

var x = 5
var y = 5

let someClosure = { [x] in
  print("\(x), \(y)")
}

x = 6
y = 6

someClosure()      // Prints 5,6
print("\(x), \(y)")    // Prints 6,6

Biến x được đặt trong capture list, nên 1 copy của x được tạo ra tại thời điểm closure được defined. Tức là x được capture bởi value, ngược lại, y không có trong capture list nên nó được capture bởi reference. Có nghĩa là khi closure runs, y sẽ có giá trị ở thời điểm đó chứ ko phải thời điểm bị capture. Capture list rất hữu ích cho việc xác định 1 weak relationship hoặc unowned relationship giưã objects và closure. Trong trường hợp này, unowned là rất phù hợp bởi closure không tồn tại khi mà instance của CarrierSubscription đã bị giải phóng. Thay đổi completePhoneNumber closure trong CarrierSubscription như sau:

    lazy var completePhoneNumber: () -> String = {
        [unowned self] in
        self.countryCode + " " + self.number
    }

Ta đã add [unowned self] vào capture list của closure: tức là self bị captured như unowned` reference chứ không phải strong reference. Syntax chúng ta dùng ở trên thực chất là viết tắt của 1 form dài hơn như sau:

var closure = {
  [unowned newID = self] in
  // Use unowned newID here...
}

Ở đây, newID là 1 unowned copy của self. Bên ngoài scope của closure, self vẫn giữ ý nghĩa nguyên của nó. Khi viết tắt, 1 biến self mới được tạo ra mà thực chất là shadows của biến self đang tồn tại, và chỉ dùng trong closure's scope. Trong code của bạn, mối quan hệ giữa selfcompletePhoneNumber closure là unowned. Nếu bạn chắc chắn rằng 1 reference object từ closure không bao giờ deallocate, bạn có thể dùng unowned. Nếu nó deallocate, bạn sẽ gặp rắc rối =)). Thêm đoạn code sau vào cuối playground:

class WWDCGreeting {
    let who: String
    
    init(who: String) {
        self.who = who
    }
    
    lazy var greetingMaker: () -> String = {
        [unowned self] in
        return "Hello \(self.who)."
    }
}

let greetingMaker: () -> String

do {
    let mermaid = WWDCGreeting(who: "Caffinated Mermaid")
    greetingMaker = mermaid.greetingMaker
}
greetingMaker() // TRAP!

Playground sẽ báo 1 runtime exception bởi vì closure hy vọng rằng self.who vẫn còn valid, nhưng nó đã bị deallocated bởi mermaid đã out of scope. Ví dụ này có vẻ giả tạo và đần độn =)) nhưng điều này rất dễ xảy ra trong thực tế ví dụ như: khi bạn sử dụng closure để chạy 1 cái gì đó sau này, như sau khi 1 asynchronous network call đã hoàn tất. Thay đổi greetingMaker trong WWDGreeting như sau:

    lazy var greetingMaker: () -> String = {
        [weak self] in
        return "Hello \(self?.who)."
    }

Có 2 điều thay đổi: thay unowned bằng weak và bởi vì selfweak nên bạn sẽ phải access vào property who với self?.who Playground sẽ không bị crashes, nhưng bạn sẽ nhận được kết quả bên sidebar: "Hello nil.". Có lẽ xử lý ntn là chấp nhận được, nhưng nếu bạn thường muốn làm gì đó hoàn toàn khác khi mà object đã bị giải phóng, thì Swift's guard let sẽ rất dễ dàng. Viết lại closure lần cuối cùng như sau:

    lazy var greetingMaker: () -> String = {
        [weak self] in
        guard let strongSelf = self else {
            return "No greeting available."
        }
        return "Hello \(strongSelf.who)."
    }

guard statement binds a new variable strongSelf từ weak self. Nếu selfnil, closure sẽ trả về "No greeting available". Ngược lại nếu self không nil, strongSelf sẽ tạo ra 1 strong reference, để object vẫn có thể "sống" đến khi closure kết thúc. Cách làm này thường được gọi là strong-weak dance, vô cùng quen thuộc từ hồi objective-C.

All Rights Reserved