Linh Tạ

Deinit observer

Gần đây mình gặp trường hợp khá đặc biệt khi một object cần chạy code tại đúng thời điểm deinit của object khác.


class Foo {
    func doSomething() { //... }
}

class Bar {
    //...

    deinit {
        //...
        //chạy function doSomething của Foo
    }
}


Closure

Cách đầu tiên và đơn giản nhất mình nghĩ đến là truyền closure vào Bar và gọi nó trong thân hàm deinit.


class Foo {
    func doSomething() { ... }
}

class Bar {
    var deinitHandler: (() -> Void)?

    deinit {
        deinitHandler?()
    }
}

let foo = Foo()
let bar = Bar()

bar.deinitHandler = { [weak foo] in
    foo?.doSomething()
}

Phương pháp này chạy tốt trong ví dụ trên nhưng tồn tại 2 nhược điểm lớn:

  1. Cồng kềnh và thủ công bởi để chạy deinitHandler ta phải khai báo closure ở object cần quan sát, gọi closure trong deinit, và gán closure tại thời điểm cần thiết. Không chỉ vậy, ta sẽ phải lặp lại các bước trên cho từng class khác nhau.
  2. Không thể áp dụng cho code ta không có quyền thay đổi ví dụ như class hệ thống hoặc thư viện bên thứ ba.

Objective-C Runtime

Để tìm giải pháp tổng quát hơn, ta sẽ cần sự trợ giúp từ Objective-C Runtime. Thư viện này bộc lộ toàn bộ tính dynamic của Objective-C và cho phép chúng ta làm những điều không thể với Swift, ví dụ như kiểm tra xem class có function với tên gọi nhất định, hay thay thế implementation của một function với phiên bản khác (swizzling). Tuyệt hơn nữa, ta có thể làm tất cả những điều trên mà không phải thay đổi bất kỳ dòng code nào ở class mục tiêu. Đây cũng là cách các framework như Google Analytics và Facebook SDK hoạt động.

Quay lại bài toán ban đầu, function quan trọng nhất ta cần sử dụng là objc_setAssociatedObject để gán property từ bên ngoài object tại runtime (thời điểm app đang chạy). Các property này có thể được liên kết với object dưới dạng unowned hoặc strong. Với liên kết strong, ta không cần lưu lại property được gán bởi nó sẽ tự deinit khi object mục tiêu deinit.

DeinitObserver

Lợi dụng tính chất này, ta sẽ tạo một custom object với closure muốn chạy khi deinit và gán object đó làm strong property của object cần quan sát.


final class DeinitObserver {
    //Key độc nhất - không lặp của liên kết
    private let key: String

    //Object để gán DeinitObserver
    private weak var target: AnyObject?

    //Closure để chạy khi DeinitObserver deinit sau khi target deinit
    private let deinitHandler: () -> Void

    deinit {
        deinitHandler()
    }

    init(for target: AnyObject, deinitHandler: @escaping () -> Void) {
        self.target = target
        self.key = UUID().uuidString
        self.deinitHandler = deinitHandler

        //gán DeinitObserver làm strong property của target
        objc_setAssociatedObject(target, key, self, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }

    func cancel() {
        guard let target = target else { return }
        objc_setAssociatedObject(target, key, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }
}

DeinitObserver khắc phục toàn bộ điểm yếu của closure khi giờ đây việc sử dụng trở nên tiện lợi và ngắn gọn hơn. Ta chỉ cần khởi tạo nó với object mục tiêu mà không phải lưu nó ở bất kì đâu ngoại trừ trường hợp muốn huỷ liên kết.


let foo = Foo()
let bar = Bar()

DeinitObserver(for: bar) { [weak foo] in
    foo?.doSomething()
}


Tổng kết

Mình mong rằng đã bộc lộ được phần nào khả năng của Objective-C Runtime qua mẹo nhỏ trên. Tuy nhiên bạn hãy cân nhắc thật kĩ trước khi sử dụng các API của thư viện này bởi

With great power comes great responsibility - Người nhện 🕸️🕸️🕸️

Source code: DeinitObserver

  1. https://developer.apple.com/documentation/objectivec
  2. https://nshipster.com/associated-objects/