#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