Capture đối tượng trong Swift closure

Kể từ khi block được giới thiệu trong iOS4 thì cho đến nay nó đã đóng một vai trò quan trọng trong hầu hết các API của Apple. Lên đến Swift thì cú pháp của block đã tiến hóa thành một bản thể khác là closures, một tính năng rất phổ biến mà bất kỳ lập trình viên iOS nào cũng biết. Mặc dù phổ biến như vậy thế nhưng chúng ta vẫn có nhiều điểm cần lưu ý khi sử dụng closure, bài viết ngày hôm nay chúng ta sẽ đi sâu hơn vào cơ chế capture đối tượng của closure và một vài kĩ thuật giúp cho việc sử dụng chúng dễ dàng hơn.

Escape và Non-escape

Closure được chia làm hai kiểu - escaping and non-escaping. Escape ở đây được hiểu là "trốn thoát", một closure được định nghĩa theo kiểu escape tức là nó được lưu dưới dạng một property, hoặc capture bởi một closure khác. Một function sau khi return xong thì sẽ giải phóng hết mọi đối tượng trong đó, bao gồm cả closure, do vậy nếu ở một function khác bạn capture đến closure đó thì sẽ xảy ra lỗi, vì vậy chúng ta thêm từ khóa @escape ở trước mỗi closure để đảm bảo closure được capture khi cần thiết. Chúng ta thường sử dụng escape closure trong những func có liên quan đến request tới server, hoặc những func bất đồng bộ (asynchoronous).

DispatchQueue.main.async {
    ...
}

Ngược lại thì non-escape closure không thể lưu được và bắt buộc phải thực thi ngay khi được gọi. Ví dụ trong hàm forEach:

[1, 2, 3].forEach { number in
    ...
}

Capturing & retain cycles

Vì escaping closure tự động capture bất kì giá trị hoặc object nào được sử dụng bên trong chúng, nên sẽ gây ra tình trạng retain cycle. Ví dụ chúng ta có một view controller sẽ bị capture trong một closure được lưu trong view model của nó:

class ListViewController: UITableViewController {
    private let viewModel: ListViewModel

    init(viewModel: ListViewModel) {
        self.viewModel = viewModel

        super.init(nibName: nil, bundle: nil)

        viewModel.observeNumberOfItemsChanged {
            // Ở đây view controller đang trỏ tới view model
            // đồng thời closure của view model cũng đang capture lại
            // view controller => retain cycle
            self.tableView.reloadData()
        }
    }
}

Và cách phổ biến để fix lỗi này đó là chúng ta thay thuộc tính của self thành weak để phá vòng lặp retain cycle:

viewModel.observeNumberOfItemsChanged { [weak self] in
    self?.tableView.reloadData()
}

Capturing context thay vì self

Cách sử dụng [weak self] ở trên rất phổ biến tuy nhiên nó cũng có mặt trái. Đó là chúng ta sẽ phải viết thêm code để chuyển từ weak reference về strong reference khi cần:

dataLoader.loadData(from: url) { [weak self] data in
    guard let strongSelf = self else {
        return
    }

    let model = try strongSelf.parser.parse(data, using: strongSelf.schema)
    strongSelf.titleLabel.text = model.title
    strongSelf.textLabel.text = model.text
}

Một phương án thay thế capture self là capture những object đơn lẻ mà bạn cần bên trong closure, những đối tượng này không sở hữu closure. Điều này sẽ giúp chúng ta tránh được retain cycle:

 

//Thay vì capture cả self, thì chúng ta chỉ capture những đối tượng cần thiết:
let context = (
    parser: parser,
    schema: schema,
    titleLabel: titleLabel,
    textLabel: textLabel
)

dataLoader.loadData(from: url) { data in
    // We can now use the context instead of having to capture 'self'
    let model = try context.parser.parse(data, using: context.schema)
    context.titleLabel.text = model.title
    context.textLabel.text = model.text
}

Dùng arguments thay vì capturing

Một phương án thay thế capture object đó là sử dụng chúng như là một argument. Đây là kỹ thuật mà tôi sử dụng để thiết kế  event API cho con game của tôi, cho phép bạn truyền một observer khi đang theo dõi một event sử dụng closure. Cách này cho phép self được truyền vào đồng thời cũng đc truyền qua closure mà không phải capture một cách thủ công nữa:

actor.events.moved.addObserver(self) { scene in
    ...
}

Quay trở lại với ví dụ đầu tiên, và cùng xem làm cách nào để chúng ta có thể thiết lập một API tương tự khi đang quan sát view model. Bằng cách đó chúng ta cũng có thể truyền vào table view như một đối tượng để quan sát.

viewModel.numberOfItemsChanged.addObserver(tableView) { tableView in
    tableView.reloadData()
}

Đầu tiên chúng ta bắt đầu bằng việc khai báo một kiểu Event chứa một observation closure:

class Event {
    private var observers = [() -> Void]()
}

Sau đó, chúng ta khai báo một method cho phép thêm một observer của một kiểu bất kỳ, cùng với closure để gọi khi mà observation được trigger. Thủ thuật ở đây là chúng ta sẽ wrap closure vào trong closure thứ hai:

func addObserver<T: AnyObject>(_ observer: T, using closure: @escaping (T) -> Void) {
    observers.append { [weak observer] in
        observer.map(closure)
    }
}

Việc này giúp chúng ta chỉ phải thực hiện chuyển đổi weak/strong một lần mà không ảnh hưởng đến phía gọi. Cuối cùng là thêm method trigger để trigger event:

func trigger() {
    for observer in observers {
        observer()
    }
}

Bây giờ thì chúng ta có thể quay lại ListViewModel và thêm event cho numberOfItemsChanged:

class ListViewModel {
    let numberOfItemsChanged = Event()
    var items: [Item] { didSet { itemsDidChange(from: oldValue) } }

    private func itemsDidChange(from previousItems: [Item]) {
        if previousItems.count != items.count {
            numberOfItemsChanged.trigger()
        }
    }
}

Lợi ích lớn nhất của phương pháp event-based trên là rất khó xảy ra lỗi retain cycle, và chúng ta có thể tái sử dụng cho mọi loại event observation trong project của mình.

 

Search API trong iOS 9 của Apple là rất hữu ích đối với các nhà phát triển ứng dụng Search API trong iOS 9 của Apple là rất hữu ích đối với các nhà phát triển ứng... Hồ Sỹ Hùng Blog Home iOS 6 sẽ tích hợp toàn diện với Facebook iOS 6 sẽ tích hợp toàn diện với Facebook Techmaster team
Nguyễn Duy Khánh

iOS Developer, Former Student and Content Editor of TechMaster