Hủy và vòng đời
AnyCancellable, những vòng giữ tham chiếu nó mời gọi, và những luồng dừng lặng lẽ
Một subscription trong Combine không có vòng đời của riêng nó. Nó sống đúng bằng thời gian có thứ gì
đó giữ cái AnyCancellable mà sink hay assign đã trả về, và nó chết ngay khi tham chiếu ấy mất
đi.
func brokenFetch() {
api.fetchItems()
.sink { print($0) } // AnyCancellable bị vứt ngay cuối dòng này
}
Đoạn đó không in ra gì, bao giờ cũng vậy. Cái cancellable bị hủy ngay lập tức, cancel() chạy, và
request bị tháo dỡ trước khi nó kịp xong. Không cảnh báo, không lỗi — Swift sẽ phát một cảnh báo về
kết quả không dùng đến cho sink, và đó là manh mối duy nhất bạn có.
Cách chữa tiêu chuẩn là một tập hợp được lưu lại:
final class ItemsModel {
private var cancellables: Set<AnyCancellable> = []
func load() {
api.fetchItems()
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.items = $0 }
.store(in: &cancellables)
}
}
Việc hủy thật sự làm gì
cancel() lan ngược lên trên. Từng toán tử lần lượt tháo dỡ, và publisher nguồn được dọn dẹp —
một request URLSession thật sự bị hủy, một đồng hồ bị vô hiệu hóa, một file handle được đóng lại.
Nó không giao một tín hiệu kết thúc. sink của một luồng bị hủy không nhận .finished cũng không
nhận .failure; nó đơn giản là thôi được gọi. Điều này là cố ý, và đó là lý do không quan sát được
việc hủy từ closure nhận tín hiệu kết thúc — hãy dùng handleEvents(receiveCancel:) nếu bạn cần
biết.
publisher
.handleEvents(
receiveSubscription: { _ in print("đã đăng ký") },
receiveOutput: { print("giá trị: \($0)") },
receiveCompletion: { print("kết thúc: \($0)") },
receiveCancel: { print("đã hủy") }
)
.sink { … }
handleEvents là công cụ gỡ lỗi của framework này. Khi một pipeline chẳng làm gì mà bạn không thấy
vì sao, chèn nó vào hai điểm sẽ cho bạn biết giá trị có đang đến không, luồng đã kết thúc chưa, hay
nó đã bị hủy ngay dưới chân bạn.
Những vòng giữ tham chiếu
Hai hình dạng gây ra gần như tất cả.
self bị bắt trong một sink được lưu trên chính self:
// vòng: self → cancellables → subscription → closure → self
.sink { self.items = $0 }
// đã sửa
.sink { [weak self] in self?.items = $0 }
assign(to:on:) nhắm vào self:
// vòng: assign giữ `on:` một cách mạnh
.assign(to: \.items, on: self)
// đã sửa — bản nạp chồng này tự quản vòng đời và không giữ tham chiếu
.assign(to: &$items)
Dạng assign(to: &$published) thật sự khác với assign(to:on:), không phải một biến thể phong cách.
Nó đòi một thuộc tính @Published và nhận nó theo kiểu inout, và nó không tạo ra cancellable nào
cả — vòng đời gắn với chính thuộc tính được publish.
Cảnh báo
[weak self] trên closure là chưa đủ nếu pipeline còn có một assign(to:on: self) ở phía sau,
hoặc bắt self bên trong một map. Cái vòng được tạo ra bởi bất kỳ lần bắt mạnh nào ở bất cứ
đâu trong chuỗi, không riêng gì closure cuối cùng.
Những luồng dừng lặng lẽ
Kiểu hỏng tệ nhất của Combine là sự im lặng, và có đúng bốn nguyên nhân. Khi một pipeline ngừng chạy, hãy kiểm tra theo đúng thứ tự này:
- Cancellable đã bị giải phóng. Đối tượng sở hữu đã bị hủy, hoặc
.store(in:)bị quên. Phổ biến hơn hẳn các nguyên nhân còn lại. - Một lỗi đã kết liễu luồng. Thứ gì đó ở trên hỏng, một
catchđặt sai chỗ, và subscription kết thúc. Đã nói ở chương về lỗi. - Một subject đã bị kết thúc. Ai đó gửi
.finished, và mọisendsau đó bị lờ đi. - Demand đã cạn. Chỉ xảy ra với subscriber tự viết, và hiếm — nhưng nó trông hệt như các trường hợp kia.
handleEvents(receiveCompletion:receiveCancel:) phân biệt được 2 và 3 với 1 và 4 trong khoảng ba
mươi giây, và đó là lý do đáng với tới nó trước khi ngồi đọc lại code.
Thu hẹp phạm vi subscription theo vòng đời
Lưu tất cả vào một Set<AnyCancellable> sống đúng bằng đời của đối tượng là cách làm phổ biến, và nó
thô. Có hai cách tinh chỉnh đáng biết.
Hủy cả một nhóm bằng cách thay cái tập hợp. Gán một Set mới sẽ giải phóng mọi cancellable trong
cái cũ, tháo dỡ cả nhóm subscription đó một lượt:
func reset() {
cancellables = [] // mọi subscription bị hủy
}
Giữ một cancellable duy nhất cho một subscription thay thế được. Khi một subscription mới nên thay cho cái cũ — một màn hình chi tiết đi theo lựa chọn — hãy giữ một thuộc tính optional thay vì một tập hợp:
private var currentRequest: AnyCancellable?
func select(_ id: Item.ID) {
currentRequest = api.fetchItem(id) // phép gán hủy cái trước đó
.sink { … }
}
Phép gán giải phóng cái AnyCancellable trước, thứ hủy luôn request đang bay. Đó là ngữ nghĩa của
switchToLatest đạt được thông qua quyền sở hữu chứ không qua một toán tử, và nó đọc hay hơn khi thứ
kích hoạt là một lời gọi hàm chứ không phải một luồng.