为什么ReceiveValue块合并订阅没有Retain Cycle

jeh*_*jeh 3 retain-cycle swiftui combine

我决心完全理解为什么这不会导致引用循环。一般来说,内存管理的每个阶段都发生了什么。

我有以下设置:

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()
    }
}
Run Code Online (Sandbox Code Playgroud)

因此,当我创建 PresenterView 然后关闭它时,我会得到成功的 deinit 打印。

但是我不明白为什么它们在这里没有参考周期。DataListViewModelhascancellables其中有一个捕获 self 的订阅。所以DataListViewModel-> 订阅和订阅 -> DataListViewModel. 怎样才能deinit触发呢?一般来说,有没有一个好的方法来理解在这种情况下是否存在保留周期?

New*_*Dev 7

正如您所期望的,该闭包确实保留了对 的强烈引用self。关闭本身由Sink订户维护。

如果没有发生其他情况,这就是内存泄漏,因为订阅者永远不会被取消,因为永远AnyCancellable不会被释放,因为 self永远不会取消初始化,并且self永远不会取消初始化,因为订阅者持有它的引用。

但是,在您的情况下,发布者完成,这是订阅者释放其关闭的另一种方式。因此,self仅在管道完成后才释放。

为了说明这一点,我们可以使用 aPassthroughSubject显式发送完成:

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()
}
Run Code Online (Sandbox Code Playgroud)

因为self被捕获,所以直到 2 秒后发送完成后才被释放:

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()
}
Run Code Online (Sandbox Code Playgroud)

如果您注释掉该行subject.send(completion: .finished),则不会有deinit

sync
async
async 2 sec
deinit 
Run Code Online (Sandbox Code Playgroud)

如果在闭包中使用[weak self],管道将取消:

sync
async
async 2 sec
Run Code Online (Sandbox Code Playgroud)