Quản lý self và cancellable trong Combine.
Bài đăng này đã không được cập nhật trong 3 năm
-
Công việc quản lý bộ nhớ
memory management
thường trở nên phức tạp khi chúng ta thực hiện các tác vụ bất đồng bộasynchronous
vì chúng ta thường phải lưu giữ một sốobject
nằm ngoàiscope
màobject
đượcdefine
trong khi vẫn phải đảm bảo được việc giải phóngobject
đó được thực hiện đúng quy trình. -
Mặc dù
Apple
đã giới thiệuframework
Combine
có thể tham chiếureference
đến cácobject
cũng như hỗ trợ việc quản lý vòng đời củaobject
đó một cách đơn giản hơn nhưng lại yêu cầu chúng ta triển khai cácasynchronous code base
của chúng ta dưới mô hìnhpipeline
thay vì triển khai dưới dạng cácclosure
lồng nhau. Tuy nhiên sử dụng mô hình này vẫn còn tiềm ẩn một số rủi ro mà chúng ta cần phải lưu ý đến, đó là công việc của chúng ta trong bài viết này:
1/ Quản lý vòng đời Subscription với Cancellable:
-
Apple
cóprotocol
Cancellable
giúp chúng ta công việc quản lýsubscription
sẽ tồn tại bao lâu cũng như khi nào nó đượcactive
(chúng ta thường làm việc thông quaAnyCancellable
). Lý do mà hầu hết cácAPI
subscription
củaApple
đềusink
vàreturn
mộtAnyCancellable
khi được gọi đến là ngay khicancellable
được giải phóngdeallocated
(tự động hay thủ công) thìsubscription
của nó sẽ được tự động vô hiệu hóa. -
Lấy ví dụ như
Clock
giữ mộtstrong reference
đến mộtinstance
AnyCancellable
và sẽ được gọi đến khisink
ở thời điểmTimer
publish
làm chosubscription
đượcactive
chỉ cầninstance
Clock
vẫn còn ở trongmemory
, trừ khicancellable
của nó đã được xóa bỏ thủ công.
class Clock: ObservableObject {
@Published private(set) var time = Date().timeIntervalSince1970
private var cancellable: AnyCancellable?
func start() {
cancellable = Timer.publish(
every: 1,
on: .main,
in: .default
)
.autoconnect()
.sink { date in
self.time = date.timeIntervalSince1970
}
}
func stop() {
cancellable = nil
}
}
- Cách triển khai trên có vẻ đã thực hiện tốt công việc quản lý
instance
AnyCancellable
vàsubscription
Timer
nhưng thực sự thì nó còn tồn tại một lỗ hổng lớn về việc quản lý bộ nhớmanagement memory
. Chúng ta đã giữ mộtstrong
self
củasink
closure
do đócancellable
của chúng ta sẽ giữ chosubscription
của nó tồn tại cho đến khi nó được giải phóng trong bộ nhớ. Chúng ta sẽ chấm dứt tình trạngretain cycle
này ngắn gọn thôi.
2/ Tránh tình trạng sử dụng self gây memory leak:
- Ý tưởng đầu tiên chúng ta có thể nghĩ đến là sử dụng
assign
trongCombine
. Chúng ta hoàn toàn có thểassign
thằng đếnproperty
củaClock
như sau:
class Clock: ObservableObject {
...
func start() {
cancellable = Timer.publish(
every: 1,
on: .main,
in: .default
)
.autoconnect()
.map(\.timeIntervalSince1970)
.assign(to: \.time, on: self)
}
...
}
- Cách làm trên vẫn sẽ sử dụng
self
để giữ lạiobject
vìassign
vẫn sẽ giữ mộtstrong reference
đến từngobject
truyền đến. Dễ nhận thấy một cách làm quen thuộc đó là sử dụngweak self
để giữ lại tham chiếu đến cácobject
này, và ở đây làinstance
củaClock
để tránh tình trạngretain cycle
:
class Clock: ObservableObject {
...
func start() {
cancellable = Timer.publish(
every: 1,
on: .main,
in: .default
)
.autoconnect()
.map(\.timeIntervalSince1970)
.sink { [weak self] time in
self?.time = time
}
}
...
}
- Như vậy thì mỗi
instance
củaClock
giờ có thể đượcdeallocated
mỗi khi nó không còn được tham chiếu đến bởi bất kìobject
nào ,AnyCancellable
cũng sẽ đượcdeallocated
và đó là cách mà mô hìnhpipeline
trongCombine
hoạt động.
3/ Assign trực tiếp value của output cho property của Published:
-
Một cách làm khác chúng ta có thể làm để khắc phục việc
retain cycle
là việc kết nối thẳng vớipublished property
(trong iOS 14). Tuy nhiên khi sử dụng cách làm có vẻ tiện lợi này thì chúng ta sẽ không có cách nào để có thểcancel
mộtsubscription
củaAnyCancellable
. -
Trong trường hợp
Clock
type
dưới đây, chúng ta vẫn có thể sử dụng cách làm trên nếu chúng ta bỏ đi 2method
làstart
vàstop
. Chúng ta sẽ tự động hóa việc mỗi khiclock
start
thay vì phảiimplement
method
. Đây là những đánh đổi chúng ta phải chấp nhận khi đồng ý sử dụng phương thức này, sau đây sẽ là phần triển khai:
class Clock: ObservableObject {
@Published private(set) var time = Date().timeIntervalSince1970
init() {
Timer.publish(
every: 1,
on: .main,
in: .default
)
.autoconnect()
.map(\.timeIntervalSince1970)
.assign(to: &$time)
}
}
- Điều tiện lợi ở đây là
Combine
sẽ tự động quản lýsubscription
cho chúng ta dựa trên vòng đời củaproperty
time
nghĩa là chúng ta vẫn sẽ tránh được cácreference cycle
trong khi không phải tốn công viết code để xử lý.
4/ Sử dụng weak property:
- Chúng ta sẽ tìm hiểu một ví dụ phức tạp hơn với việc
implement
ModelLoader
thực hiện công việcdecode
quaDecodeable
từURL
bằng cách dùngcancellable
. Cácloader
của chúng ta sẽ tự độngcancel
bất kìdata
đangloading
trước đó khi mộttrigger
mới được thực hiện cũng như bất kìAnyCancellable
nào trước đó sẽ đượcdeallocated
khivalue
củaproperty
được thay thế:
class ModelLoader<Model: Decodable>: ObservableObject {
enum State {
case idle
case loading
case loaded(Model)
case failed(Error)
}
@Published private(set) var state = State.idle
private let url: URL
private let session: URLSession
private let decoder: JSONDecoder
private var cancellable: AnyCancellable?
...
func load() {
state = .loading
cancellable = session
.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: Model.self, decoder: decoder)
.map(State.loaded)
.catch { error in
Just(.failed(error))
}
.receive(on: DispatchQueue.main)
.sink { [weak self] state in
self?.state = state
}
}
}
- Nếu như chúng ta vẫn muốn tránh việc phải sử dụng thủ công
self
đễ giữreference
mỗi khi chúng ta sử dụngpattern
trên thì chúng ta cần sử dụng thêmextension
củaPublisher
như việcadd
thêmweak
cho phương thứcassign
chúng ta đang sử dụng:
extension Publisher where Failure == Never {
func weakAssign<T: AnyObject>(
to keyPath: ReferenceWritableKeyPath<T, Output>,
on object: T
) -> AnyCancellable {
sink { [weak object] value in
object?[keyPath: keyPath] = value
}
}
}
- Bây giờ thì chúng ta đã có
weakAssign
mỗi khi chúng taassign
cácoutput
củapublisher
choproperty
củaobject
đang sử dụngweak self
như sau:
class ModelLoader<Model: Decodable>: ObservableObject {
...
func load() {
state = .loading
cancellable = session
.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: Model.self, decoder: decoder)
.map(State.loaded)
.catch { error in
Just(.failed(error))
}
.receive(on: DispatchQueue.main)
.weakAssign(to: \.state, on: self)
}
}
5/ Sử dụng store object thay vì self:
- Trong trường hợp chúng ta muốn mở rộng
ModelLoader
bằng việc sử dụngDatabase
để tự động việcstore
mỗimodel
đượcload
bằng việcwrap
cácmodel
này vớigeneric
làStored
. Để cho phép việcaccess
vàodatabase
instance
này với phương thức quen thuộcflatMap
chúng ta một lần nữa sử dụngweak reference
như sau:
class ModelLoader<Model: Decodable>: ObservableObject {
enum State {
case idle
case loading
case loaded(Stored<Model>)
case failed(Error)
}
...
private let database: Database
...
func load() {
state = .loading
cancellable = session
.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: Model.self, decoder: decoder)
.flatMap {
[weak self] model -> AnyPublisher<Stored<Model>, Error> in
guard let database = self?.database else {
return Empty(completeImmediately: true)
.eraseToAnyPublisher()
}
return database.store(model)
}
.map(State.loaded)
.catch { error in
Just(.failed(error))
}
.receive(on: DispatchQueue.main)
.weakAssign(to: \.state, on: self)
}
}
-
Việc sử dụng các phương thức như
map
hayflatMap
thì chúng ta đôi khi sẽ phải sử dụngguard
để đảm bảo cho cácoutput
được hợp lệ. Việc chúng ta sử dụngEmpty
ở trên đông nghĩa chúng ta sẽ phải thêm kha khá code cho công việc này: -
Để cải thiện vấn đề này chúng ta sẽ lưu trực tiếp
database property
này trực tiếp thay vì sử dụngself
như sau:
class ModelLoader<Model: Decodable>: ObservableObject {
...
func load() {
state = .loading
cancellable = session
.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: Model.self, decoder: decoder)
.flatMap { [database] model in
database.store(model)
}
.map(State.loaded)
.catch { error in
Just(.failed(error))
}
.receive(on: DispatchQueue.main)
.weakAssign(to: \.state, on: self)
}
}
- Có một điểm cộng nữa là chúng ta thậm chí có thể truyền trực tiếp
method
Database
bằng việc sử dụngflatMap
vì nó hoàn toàn phù hợp với bối cảnh này:
class ModelLoader<Model: Decodable>: ObservableObject {
...
func load() {
state = .loading
cancellable = session
.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: Model.self, decoder: decoder)
.flatMap(database.store)
.map(State.loaded)
.catch { error in
Just(.failed(error))
}
.receive(on: DispatchQueue.main)
.weakAssign(to: \.state, on: self)
}
}
All rights reserved