+2

Bài 3. RxSwift – DisposeBag

1.DisposeBag và vấn đề

  • Nếu Observable không kết thúc thì sao nào?
  • Không muốn nhận nữa mà Observable vẫn cứ phát dữ liệu đi?
  • Chã lẻ lúc nào cũng phải mang theo 1 closure để đi subscribe Observable.
  • Có nên huỷ nó hay không huỷ nó, khi phải làm việc với bất đồng bộ. Như đang chờ dữ liệu từ API trả về
  • ... Vấn đề quản lý bộ nhớ luôn là một vấn đề lớn trong việc lập trình.

DisposeBag, Dispose & Disposable thì chỉ có trong nền tảng RxSwift và nó không tương đồng với các nền tảng khác trong Rx. Đây là cơ chế giúp cho RxSwift có thể quản lý bộ nhớ của mình được tốt nhất.

Đây cũng là cách nhanh nhất đơn phương kết thúc một đăng ký (subscription) từ phía người đăng ký (subscriber). Mà không cần đợi nguồn phát phát đi error hay completed.

2. Dispose Subscription

Khi một Observable được tạo ra, nó sẽ không hoạt động hay hành động gì cho tới khi có 1 subscriber đăng ký tới. Việc subscriber khi đăng ký tới thì gọi là subscription. Lúc đó, sẽ kích hoạt Observable (hay gọi là trigger) bắn đi các giá trị của mình. Việc này cứ lặp đi lặp lại, cho đến khi phát ra .error hoặc .completed.

Vấn đề chính bắt đầu từ đây. Bạn không bao giờ biết lúc nào nó kết thúc, khi đó các đối tượng trong chương trình của bạn không bao giờ bị giải phóng. Và bạn không biết khi nào dữ liệu tới hoặc de-bugs để biết lỗi từ đâu mà ra. Cùng xử lý theo ví dụ sau:

let observable = Observable<String>.of("A", "B", "C", "D", "E", "F")
    
let subscription = observable.subscribe { event in
    print(event)
}

Ta có một đoạn code với việc khai báo 1 Observable với kiểu dữ liệu Output là String. Tiếp sau đó, tạo thêm 1 subscription bằng việc sử dụng toán tử .subscribe cho Observable. Cung cấp thêm 1 closure để handle các dữ liệu nhận được.

Để dừng việc phát của Observable thì bạn sử dụng hàm .dispose()

subscription.dispose()

Lúc này, kết nối của subscription sẽ chấm dứt và Observable sẽ dừng phát.

3. DisposeBag

Bạn xem tiếp ví dụ code sau:

Observable<String>.of("A", "B", "C", "D", "E", "F")
        .subscribe { event in
            print(event)
        }

Nếu như bạn tạo ra 1 subscription để quản lý các đăng ký. Thì mọi thứ đơn giản rồi. Tuy nhiên, thường trong code Rx thì:

  • Các đối tượng subscriber hầu như không tồn tại hoặc không tạo ra.
  • Các subscription sẽ được tạo ra nhằm giữ kết nối. Nó sẽ phục vụ cho tới khi nào class chứa nó bị giải phóng.
  • Có quá nhiều subscription trong một class.
  • Nhiều trường hợp muốn subscribe nhanh tới 1 Observable nên các subscription sẽ không tạo ra. Do đó, người ta sinh ra khái niệm mới DisposeBag Cùng thử trong ví dụ sau:
  let bag = DisposeBag()
    
    Observable<String>.of("A", "B", "C", "D", "E", "F")
        .subscribe { event in
            print(event)
        }
        .disposed(by: bag)

Lúc này, tất cả Observable sẽ được disposeBag quản lý và thủ tiêu.

Nếu bạn quên việc thêm dispose() hay disposeBag thì trình biên dịch sẽ báo cho bạn biết, nên đừng quá bận tâm.

4. Dispose vs DisposeBag

Hai thằng Dispose và DisposeBag có gì khác nhau? Chúng ta cùng tìm hiểu vào bài toán thực tế.

Ví dụ, ta có 1 ViewController với nhiều subscription. Công việc giờ quản lý và tránh đi retain-cycle khi các biến trong class ViewController không được tự động giải phóng.

Sử dụng Dispose

  • Với 1 subscription đơn lẻ
final class ViewController: UIViewController {
    var subscription: Disposable?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        subscription = theObservable().subscribe(onNext: {
            // handle subscription
        })
    }
    
    deinit {
        subscription?.dispose()
    }
}
  • Với nhiều subscription
final class ViewController: UIViewController {
    var subscriptions = [Disposable]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        subscriptions.append(theObservable().subscribe(onNext: {
            // handle subscription
        }))
    }
    
    deinit {
        subscriptions.forEach { $0.dispose() }
    }
}

Bạn hãy chú ý tới function deinit. Nó là nơi các subscription gọi việc tự huỷ .dispose().

Sử dụng DisposeBag

final class ViewController: UIViewController {
    let disposeBag = DisposeBag()
    override func viewDidLoad() {
        super.viewDidLoad()
        theObservable().subscribe(onNext: {
                // handle subscription
        })
        .disposed(by: disposeBag)
    }
}

DisposeBag sẽ chịu trách nhiệm cho việc gọi dispose trên mỗi Disposable bên trong nó. Việc này sẽ được gọi khi bản thân DisposeBag bị deinit.

Retain cycle

Khi bạn đã cài đặt đầy đủ DisposeBag rồi, nhưng nguy cơ retain-cycle vẫn còn. Nhất là với với thao tác bất đồng bộ.

Ví dụ, bạn gọi kết nối tới 1 API. Lúc này, bạn phải chờ response trả về. Nhưng thời gian chờ quá lâu, bạn thoát khỏi màn hình đó. Và sau đó, response về. Đây là lúc nhiều câu chuyện bắt đầu

  • Về mặt quản lý biến và đối tượng thì chúng bằng nil khi chúng ta thoát khỏi màn hình đó.
  • Tuy nhiên vì để chờ response thì đâu đó, ta cần phải sử dụng chính đối tượng self để xử lý & update UI. Việc này tạo ra sự tham chiếu tới self.

Mà cái closure handle response đó lại được lưu trữ trong 1 Subscription. Và subscription đó lại ở trong 1 DisposeBag. DisposeBag lại ở trong ViewController.

Khi thoát khỏi màn hình, hàm deinit sẽ chạy, nhưng chúng sẽ không deinit được vì trong closure trên vẫn còn được trỏ tới.

image.png

Mô tả code như sau:

final class MyViewController: UIViewController {
    private let disposeBag = DisposeBag()
    private let parser = MyModelParser()
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let parsedObject = theObservable
            .map { json in
                return self.parser.parse(json)
            }
            
        parsedObject.subscribe(onNext:{ _ in 
            //do something
        })
        .disposed(by: disposeBag)
    }
}

Vậy chúng ta có giải pháp gì?

weak self

Cách để giải quyết nó là bạn hãy nhớ về [weak self]. Bạn hãy xem đoạn code:

let parsedObject = theObservable
    .map { [weak self] json in
        return self?.parser.parse(json) //compile-time error. What should be returned if `self` is nil?
    }

Tuy nhiên, nếu sử dụng [weak self], ta cần báo cho compiler phải trả về giá trị gì nếu self bị nil. Trong trường hợp này, cách tốt nhất là truyền parser trong capture list thay vì dùng self:

let parsedObject = theObservable
    .map { [parser] json in
        return parser.parse(json)
    }

Swift cho phép ta truyền một biến vào capture list mà không cần bất cứ thuộc tính nào như weak hoặc unowned. Nếu làm như thế, compiler sẽ biết phải giữ reference cho duy nhất parse (với strong reference), chứ không phải cho self.

Một cách nữa là sử dụng[unowned self] thay cho [weak self].

5. Disposable

Create

Trước tiên, bạn cần tìm hiểu về toán tử create. Nó sẽ giúp cho bạn tạo được một Observable. Ví dụ được thực hiện với kiểu Output là String.

Observable<String>.create(subscribe: (AnyObserver<String>) -> Disposable##(AnyObserver<String>) -> Disposable>)

Tham số cần truyền vào chính là 1 observer. Tới đây nhiều bạn sẽ loạn não 1 chút, mình xin tóm gọn lại thế này:

  • Observable là nguồn phát
  • Observer là nguồn nhận

Công việc của nó chính là:

  • Thiết lập đăng kí từ Observable tới Subscriber
  • Xác định các dữ liệu được phát ra bởi Observable tới Subscriber

Disposable là một protocol với một phương thức dispose() Khi subscribe một Observable, Disposable giữ một tham chiếu đến Observable và Observable này giữ strong reference đến Disposable (Rx tạo ra một loại retain cycle). Nhờ vậy, khi user quay lại màn hình trước đó, Observable sẽ không bị giải phóng cho đến khi ta muốn giải phóng nó. Chúng ta làm một ví dụ như sau:

let bag = DisposeBag()

Observable<String>.create { observer -> Disposable in
        observer.onNext("1")
        
        observer.onNext("2")
        
        observer.onNext("3")
        
        observer.onNext("4")
        
        observer.onNext("5")
        
        observer.onCompleted()
        
        observer.onNext("6")
        
        return Disposables.create()
    }.subscribe(
        onNext: { print($0) },
        onError: { print($0) },
        onCompleted: { print("Completed") },
        onDisposed: { print("Disposed") }
        )
    .disposed(by: bag)

Handle full các sự kiện với các dữ liệu mà Observable có thể phát ra. Các hàm handle là onNext, onError, onCompletedonDisposed sẽ tương ứng với từng kiểu dữ liệu mà Observable phát ra. Kết quả khi thực thi như sau:

1
2
3
4
5
Completed
Disposed

Bạn để ý kĩ thì sẽ không thấy 6. Đơn giản vì nó được phát ra sau .completed. Nên sẽ không bao giờ phát đi được và cũng không bao giờ nhận được. Áp dụng tương tự cho error.

Vậy nếu chúng ta bỏ:

disposed(by: bag)
observer.onError(MyError.anError)
observer.onCompleted()

Thì chuyện gì sẽ xảy ra, cùng test thử với ví dụ dưới đây nhé:

  let bag = DisposeBag()
    
    Observable<String>.create { observer -> Disposable in
        observer.onNext("1")
        
        observer.onNext("2")
        
        observer.onNext("3")
        
        observer.onNext("4")
        
        //observer.onError(MyError.anError)
        
        observer.onNext("5")
        
        //observer.onCompleted()
        
        observer.onNext("6")
        
        return Disposables.create()
    }
    .subscribe(
        onNext: { print($0) },
        onError: { print($0) },
        onCompleted: { print("Completed") },
        onDisposed: { print("Disposed") }
        )
    //.disposed(by: bag)

Như vậy, nếu không có disposeBag và không có việc phát ra error hay completed, thì subscribe sẽ vẫn đứng đó chờ Observable. Và subscription sẽ không kết thúc được.

Tham khảo:

https://fxstudio.dev/rxswift-disposebag/


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.