web-dev-qa-db-fra.com

Comment éviter les cycles de référence forts lors de l'utilisation du nouveau framework Combine d'Apple (.assign cause des problèmes)

Je ne comprends pas très bien comment stocker correctement les abonnés dans une classe afin qu'ils persistent mais n'empêchent pas l'objet d'être désinitialisé. Voici un exemple où l'objet ne désinitie pas:

import UIKit
import Combine

class Test {
    public var name: String = ""

    private var disposeBag: Set<AnyCancellable> = Set()

    deinit {
        print("deinit")
    }

    init(publisher: CurrentValueSubject<String, Never>) {
        publisher.assign(to: \.name, on: self).store(in: &disposeBag)
    }
}

let publisher = CurrentValueSubject<String, Never>("Test")

var test: Test? = Test(publisher: publisher)
test = nil

Lorsque je remplace le assign par un sink (dans lequel je déclare correctement [weak self]), il se désinitialise correctement (probablement parce que assign accède à self d'une manière qui cause des problèmes).

Comment puis-je empêcher de forts cycles de référence lors de l'utilisation de .assign par exemple?

Merci

11
Quantm

vous pouvez remplacer .asign (to :) par un évier où [self faible] dans sa fermeture freine le cycle de mémoire. Essayez-le dans Playground pour voir la différence

final class Bar: ObservableObject {
    @Published var input: String = ""
    @Published var output: String = ""

    private var subscription: AnyCancellable?

    init() {
        subscription = $input
            .filter { $0.count > 0 }
            .map { "\($0) World!" }
            //.assignNoRetain(to: \.output, on: self)
            .sink { [weak self] (value) in
                self?.output = value
        }

    }

    deinit {
        subscription?.cancel()
        print("\(self): \(#function)")
    }
}

// test it!!
var bar: Bar? = Bar()
let foo = bar?.$output.sink { print($0) }
bar?.input = "Hello"
bar?.input = "Goodby,"
bar = nil

il imprime

Hello World!
Goodby, World!
__lldb_expr_4.Bar: deinit

nous n'avons donc pas de fuite de mémoire!

enfin sur forums.Swift.org quelqu'un fait un joli petit

extension Publisher where Self.Failure == Never {
    public func assignNoRetain<Root>(to keyPath: ReferenceWritableKeyPath<Root, Self.Output>, on object: Root) -> AnyCancellable where Root: AnyObject {
        sink { [weak object] (value) in
        object?[keyPath: keyPath] = value
    }
  }
}
6
user3441734

Je ne sais pas ce que vous avez contre les fermetures mais la solution est de ne pas utiliser self dans l'attribution:

import Combine
import SwiftUI

class NameStore {
    var name: String
    init() { name = "" }
    deinit { print("deinit NameStore") }
}

class Test {
    private var nameStore = NameStore()
    public var name: String { get { return nameStore.name } }

    var subscriber: AnyCancellable? = nil

    deinit { print("deinit Test") }

    init(publisher: CurrentValueSubject<String, Never>) {
        subscriber = publisher.print().assign(to: \NameStore.name, on: nameStore)
    }
}

let publisher = CurrentValueSubject<String, Never>("Test")
var test: Test? = Test(publisher: publisher)

struct ContentView : View {
    var body: some View {
        Button(
            action: { test = nil },
            label: {Text("test = nil")}
        )
    }
}

Pour autant que je puisse voir, les références faibles ne sont autorisées que dans les fermetures, ce n'était donc pas la réponse. Mettre la référence dans un autre objet signifiait que les deux pouvaient être libérés.

J'ai ajouté un ContentView car il facilite la lecture et j'ai ajouté une impression au pipeline pour voir ce qui se passait. Le nom calculé n'est probablement pas nécessaire, il l'a simplement fait ressembler à ce que vous aviez. J'ai également supprimé le Set, c'est probablement utile mais je n'ai pas compris quand.

1
Michael Salmon

Vous devez supprimer AnyCancellable stocké de disposeBag pour libérer l'instance Test.

import UIKit
import Combine

private var disposeBag: Set<AnyCancellable> = Set()

class Test {
    public var name: String = ""


    deinit {
        print("deinit")
    }

    init(publisher: CurrentValueSubject<String, Never>) {
        publisher.assign(to: \.name, on: self).store(in: &disposeBag)
    }
}

let publisher = CurrentValueSubject<String, Never>("Test")

var test: Test? = Test(publisher: publisher)
disposeBag.removeAll()
test = nil

ou utilisez facultatif disposeBag

import UIKit
import Combine

class Test {
    public var name: String = ""
    private var disposeBag: Set<AnyCancellable>? = Set()

    deinit {
        print("deinit")
    }

    init(publisher: CurrentValueSubject<String, Never>) {
        guard var disposeBag = disposeBag else { return }
        publisher.assign(to: \.name, on: self).store(in: &disposeBag)
    }
}

let publisher = CurrentValueSubject<String, Never>("Test")

var test: Test? = Test(publisher: publisher)
test = nil
0
Victor Kushnerov