Почему нет цикла сохранения в объединении блоков ReceiveValue

#swiftui #combine #retain-cycle

#swiftui #объединить #сохранить-цикл

Вопрос:

Я полон решимости полностью понять, почему это не вызывает ссылочный цикл. И вообще, что здесь происходит на каждом этапе управления памятью.

У меня следующая настройка:

 struct PresenterView: View {
    @State private var isPresented = false
    var body: some View {
        Text("Show")
            .sheet(isPresented: $isPresented) {
                DataList()
            }
            .onTapGesture {
                isPresented = true
            }
    }
}

struct DataList: View {

    @StateObject private var viewModel = DataListViewModel()
    
    var body: some View {
        NavigationView {
            List(viewModel.itemViewModels, id: .self) { itemViewModel in
                Text(itemViewModel.displayText)
            }.onAppear {
                viewModel.fetchData()
            }.navigationBarTitle("Items")
        }
    }
}

class DataListViewModel: ObservableObject {
    
    private let webService = WebService()

    @Published var itemViewModels = [ItemViewModel]()
    
    private var cancellable: AnyCancellable?
    
    func fetchData() {
        cancellable = webService.fetchData().sink(receiveCompletion: { _ in
            //...
        }, receiveValue: { dataContainer in
            self.itemViewModels = dataContainer.data.items.map { ItemViewModel($0) }
        })
    }
    
    deinit {
        print("deinit")
    }
    
}

final class WebService {
    
    var components: URLComponents {
        //...
        return components
    }

    func fetchData() -> AnyPublisher<DataContainer, Error> {
        return URLSession.shared.dataTaskPublisher(for: components.url!)
            .map { $0.data }
            .decode(type: DataContainer.self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
}
  

Поэтому, когда я создаю PresenterView, а затем отклоняю его, я получаю успешную печать deinit.

Однако я не понимаю, почему здесь нет ссылочного цикла. DataListViewModel имеет cancellables подписку, которая фиксирует self. Итак DataListViewModel -> подписка и подписка -> DataListViewModel . Как это может deinit быть вызвано? В целом, существует ли хороший подход к пониманию того, существует ли цикл сохранения в подобных ситуациях?

Комментарии:

1. Когда происходит отключение? Я предполагаю, что это происходит после того, как данные были получены (или иным образом dataTaskPublisher завершены. Итак, приемник освобождает свои ресурсы (например, замыкания), поэтому больше нет ссылок, которые хранят экземпляр DataListViewModel в памяти

2. Да, когда данные были получены. Когда вы говорите, что освобождает ресурсы, он будет сохранять закрытия слабо и устанавливать их равными нулю? Возможно, это выходит за рамки объединения и более общего управления памятью. Не могли бы вы подробнее рассказать об этом? Может быть, у приемника есть ссылка на закрытие (ы), которые захватывают DataListViewModel — когда закрытие установлено на ноль / отпущено, оно больше не указывает DataListViewModel , и это позволяет его деинициализировать? Я пытаюсь представить себе каждый этап этого процесса. Спасибо!

3. Да, .sink подписчик хранит ссылки на закрытия до тех пор, пока не получит отмену или завершение. Затем он устанавливает их в nil . В качестве тестирования используйте [weak self] закрытие приемника и что вы должны увидеть, что deinit произойдет до получения данных

Ответ №1:

Закрытие, как вы и ожидали, сохраняет сильную ссылку на self . Само закрытие поддерживается Sink подписчиком.

Если больше ничего не происходит, это утечка памяти, потому что подписчик никогда не отменяется, потому AnyCancellable что никогда не освобождается, потому self что никогда не де-инициализируется и self никогда не де-инициализируется, потому что подписчик содержит ссылку на него.

Однако в вашем случае издатель завершает, и это еще один способ для подписчика освободить свои закрытия. Итак, self освобождается только после завершения конвейера.

Чтобы проиллюстрировать, мы можем использовать a PassthroughSubject для явной отправки завершения:

 class Foo {
   var c: AnyCancellable? = nil

   func fetch() {
      let subject = PassthroughSubject<String, Never>()

      c = subject.sink {
         self.c // capture self
         print($0)
      }

      subject.send("sync")

      DispatchQueue.main.async { subject.send("async") }

      DispatchQueue.main.asyncAfter(deadline: .now()   2) { 
         subject.send("async 2 sec")
         subject.send(completion: .finished)
      }
   }

   deinit { print("deinit") }
}


do {
   Foo().fetch()
}
  

Поскольку self он захвачен, он не освобождается до тех пор, пока через 2 секунды не будет отправлено завершение:

 sync
async
async 2 sec
deinit 
  

Если вы закомментируете строку subject.send(completion: .finished) , не будет deinit :

 sync
async
async 2 sec
  

Если вы используете [weak self] в закрытии, конвейер будет отменен:

 sync
deinit