Quản lý self và cancellable trong Combine.
Bài đăng này đã không được cập nhật trong 4 năm
-
Công việc quản lý bộ nhớ
memory managementthườ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ộasynchronousvì chúng ta thường phải lưu giữ một sốobjectnằm ngoàiscopemàobjectđượcdefinetrong 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ệuframeworkCombinecó thể tham chiếureferenceđến cácobjectcũ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 basecủa chúng ta dưới mô hìnhpipelinethay vì triển khai dưới dạng cácclosurelồ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:
-
ApplecóprotocolCancellablegiúp chúng ta công việc quản lýsubscriptionsẽ 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ácAPIsubscriptioncủaAppleđềusinkvàreturnmộtAnyCancellablekhi được gọi đến là ngay khicancellableđược giải phóngdeallocated(tự động hay thủ công) thìsubscriptioncủa nó sẽ được tự động vô hiệu hóa. -
Lấy ví dụ như
Clockgiữ mộtstrong referenceđến mộtinstanceAnyCancellablevà sẽ được gọi đến khisinkở thời điểmTimerpublishlàm chosubscriptionđượcactivechỉ cầninstanceClockvẫn còn ở trongmemory, trừ khicancellablecủ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ý
instanceAnyCancellablevàsubscriptionTimernhư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ộtstrongselfcủasinkclosuredo đócancellablecủa chúng ta sẽ giữ chosubscriptioncủ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 cyclenà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
assigntrongCombine. Chúng ta hoàn toàn có thểassignthằng đếnpropertycủaClocknhư 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ạiobjectvìassignvẫn sẽ giữ mộtstrong referenceđến từngobjecttruyề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ácobjectnày, và ở đây làinstancecủ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
instancecủaClockgiờ có thể đượcdeallocatedmỗi khi nó không còn được tham chiếu đến bởi bất kìobjectnào ,AnyCancellablecũng sẽ đượcdeallocatedvà đó là cách mà mô hìnhpipelinetrongCombinehoạ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 cyclelà 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ểcancelmộtsubscriptioncủaAnyCancellable. -
Trong trường hợp
Clocktypedướ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 2methodlàstartvàstop. Chúng ta sẽ tự động hóa việc mỗi khiclockstartthay vì phảiimplementmethod. Đâ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à
Combinesẽ tự động quản lýsubscriptioncho chúng ta dựa trên vòng đời củapropertytimenghĩa là chúng ta vẫn sẽ tránh được cácreference cycletrong 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
implementModelLoaderthực hiện công việcdecodequaDecodeabletừURLbằng cách dùngcancellable. Cácloadercủa chúng ta sẽ tự độngcancelbất kìdatađangloadingtrước đó khi mộttriggermới được thực hiện cũng như bất kìAnyCancellablenào trước đó sẽ đượcdeallocatedkhivaluecủ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ữreferencemỗi khi chúng ta sử dụngpatterntrên thì chúng ta cần sử dụng thêmextensioncủaPublishernhư việcaddthêmweakcho phương thứcassignchú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ó
weakAssignmỗi khi chúng taassigncácoutputcủapublisherchopropertycủaobjectđang sử dụngweak selfnhư 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
ModelLoaderbằng việc sử dụngDatabaseđể tự động việcstoremỗimodelđượcloadbằng việcwrapcácmodelnày vớigenericlàStored. Để cho phép việcaccessvàodatabaseinstancenày với phương thức quen thuộcflatMapchúng ta một lần nữa sử dụngweak referencenhư 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ư
maphayflatMapthì 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 propertynày trực tiếp thay vì sử dụngselfnhư 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
methodDatabasebằng việc sử dụngflatMapvì 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