Contents
Chào bạn đến với Fx Studio. Bài viết này với chủ đề là DisposeBag, là phần tiếp nối trong series RxSwift.
Chúng ta đã lang thang qua được phần Observables và đây cũng là phần tiếp sau nó. Và nếu bạn chưa biết về Observables thì có thể ghé sang link này để đọc lại.
Còn mọi thứ đã ổn rồi thì …
Bắt đầu thôi!
Chuẩn bị
- Xcode 11
- Swift 5
- Playground
Cũng không phải chuẩn gì nhiều ở đây cả. Vẫn là project lần trước và bạn tạo thêm một file Playground là ổn thôi. Nếu bạn quên link checkout source code thì có thể checkout tại đây.
1.DisposeBag và vấn đề
Điểm nguy hiểm nhất khi sử dụng RxSwift (hay Reactive Programming) là việc quản lý bộ nhớ. Nó không hoàn toàn tự động và không được tối ưu. Khi có quá nhiều nguồn phát và quá nhiều đối tượng lắng nghe tới các nguồn phát đó, thì bạn có dám chắc rằng bạn đã giải phóng tất cả các đối tượng hay không.
Một đối tượng Observable sẽ phát đi dữ liệu khi nó được một đối tượng khác (hay cái gì đó) đăng ký tới.
1.1. Vấn đề
Bạn chỉ cần tò mò một chút thì sẽ có nhiều vấn đề trong cơ chế hoạt động trên. Như:
- 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ề
- Lỡ quên huỷ mất tiêu … thì có toang không ta?
- …
Quá nhiều, chắc cũng phải tới 1001 lý do ở đây rồi. Nhưng ta chỉ cần quan tâm là mối liên kết giữa chúng không bị giải phóng. Dù ít hay nhiều thì bộ nhớ của thiết bị sẽ bị chiếm dụng. Nguy hiểm hơn là chúng ta sẽ không handle được sự thay đổi tới các thành phần khác mà cùng đăng ký tới Observable.
Với Mobile cho dù ram chip mạnh tới đâu đi nữa. Thì đó, vẫn là các thiết bị bị giới hạn bộ nhớ và tài nguyên. Nên việc lập trình với nó cần phải coi trong việc quản lý bộ nhớ.
1.2. DisposeBag
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 đa vũ trụ 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.
Để hiển cách hoạt động của nó, bạn tiếp tục theo dõi các phần sau.
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 hoặc bạn phó mặc nó với số phận. Khi bạn chấp nhận buông tay thì hậu quả để lại là 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.
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.
Tiếp theo, để 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. Và hoà bình sẽ trở lại với thế giới!
3. DisposeBag
Câu chuyện vẫn còn vui, 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 vẫn là câu nói cũ Đời không như là mơ
. 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.
Không giải quyết riêng lẻ được thì gom lại mà xử lý thằng cầm đầu.
Vậy là, vấn đề bộ nhớ vẫn nhức nhói. Do đó, người ta sinh ra khái niệm mới DisposeBag
(túi rác quốc dân). Bạn xem tiếp code ví dụ trên với việc thêm DisposeBag vào.
let bag = DisposeBag()
Observable<String>.of("A", "B", "C", "D", "E", "F")
.subscribe { event in
print(event)
}
.disposed(by: bag)
Lúc này, bạn không cần bận tâm lắm về Observable mình tự do làm càng. Tất cả sẽ được disposeBag
quản lý và thủ tiêu.
Áp dụng vào trong project, khi bạn khai báo 1 disposeBag
là biến toàn cục. Khi đó, bạn chỉ cần ném tất cả các subscription hoặc các Observable vào đó. Và yên tâm về vấn đề bộ nhớ sẽ không bị ảnh hưởng.
Tất nhiên, nếu bạn quên việc thêm dispose()
hay disposeBag
thì trình biên dịch sẽ nhắn lời yêu thương cho bạn biết.
4. Dispose vs. DisposeBag
Hai thằng Dispose và DisposeBag có gì khác nhau. Vì tất cả cũng đều phục vụ cho việc giải phóng subscription. Để hiểu hơn thì bạn phải đưa 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.
4.1. 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()
.
4.2. Sử dụng DisposeBag
final class ViewController: UIViewController {
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
theObservable().subscribe(onNext: {
// handle subscription
})
.disposed(by: disposeBag)
}
}
Vậy thì function deinit
đã đi đâu?
Một điều tuyệt vời của DisposeBag, là nó 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
.
4.3. 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ớiself
.
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.
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)
}
}
4.4. 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
5.1. 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
5.2 Disposable
Disposable là một protocol với một phương thức dispose()
public protocol Disposable { func 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ó.
Lý thuyết thì nó như vậy, giờ ta tiến hành implement đầy đủ cho Observable nào:
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()
}
Bạn sẽ thấy trong closure đó, chúng ta tuỳ ý phát ra các dữ liệu. Quan trọng là return về Disposables
, để có thể kết thúc việc đăng ký từ bên ngoài. Và nó cũng là đại diện cho mỗi lần đăng ký tới Observable.
5.3. Subscribe
Bạn khai báo thêm 1 túi rác quốc dân nữa.
let bag = DisposeBag()
Tiếp theo là .subscribe
tới Observable nào
.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
, onCompleted
và onDisposed
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
.
5.4. onError
Khai báo 1 Error cho dễ quản lý
enum MyError: Error {
case anError
}
Và phát error như sau:
observer.onError(MyError.anError)
Kết quả thực thi như sau với việc phát ra error
sau khi phát ra 4
1
2
3
4
anError
Disposed
À, bạn tuỳ ý custom về phát Error rồi nha.
5.5. non terminate
Để nhìn rõ hơn việc DisposeBag
ảnh hưởng như thế nào, chúng ta sẽ comment lại các lệnh sau:
- disposed(by: bag)
- observer.onError(MyError.anError)
- observer.onCompleted()
disposed(by: bag)
observer.onError(MyError.anError)
observer.onCompleted()
enum MyError: Error {
case anError
}
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)
Bạn xem kết quả như sau:
1
2
3
4
5
6
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.
Tạm kết
- Huỷ một subscription bằng
dispose()
- Quản lý nhiều subscription cho nó tự huỷ bằng
DisposeBag
.create
tạo ra một Observable và cài đặt các hành vi cho nó.
Okay! Tới đây, mình xin kết thúc bài viết về DisposeBag trong RxSwift. Và nếu có gì thắc mắc hay góp ý cho mình thì bạn có thể để lại bình luận hoặc gởi email theo trang Contact.
- Bài viết tiếp theo tại đây.
- Tham khảo : http://adamborek.com/memory-managment-rxswift/
Cảm ơn bạn đã đọc bài viết này!
Related Posts:
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
- Automatic Reference Counting (ARC) trong 10 phút
- Autoresizing Masks trong 10 phút
- Regular Expression (Regex) trong Swift
You may also like:
Archives
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)