Trang chủ

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:

  1. 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.
  2. 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.
  3. Một subject đã bị kết thúc. Ai đó gửi .finished, và mọi send sau đó bị lờ đi.
  4. 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.