web-dev-qa-db-ja.com

Appleの新しいCombineフレームワークを使用するときに強い参照サイクルを防ぐ方法(.assignは問題の原因です)

サブスクライバーをクラス内に適切に格納して永続化する方法はよくわかりませんが、オブジェクトが非初期化されるのを防ぎません。次に、オブジェクトが初期化されない例を示します。

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

assignsinkに置き換えると([weak self]を適切に宣言します)、実際には正しく初期化を解除します(おそらくassignself問題を引き起こす方法で)。

たとえば.assignを使用する場合、どのようにして強い参照サイクルを防ぐことができますか?

ありがとう

11
Quantm

.asign(to :)をシンクに置き換えると、クロージャの[weak self]がメモリサイクルをブレーキします。プレイグラウンドで試して、違いを確認してください

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

印刷する

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

メモリリークはありません。

最終的にforums.Swift.orgで誰かが素敵な小さな

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

私はあなたがクロージャーに対して何を持っているのかわかりませんが、解決策は代入で自分自身を使用しないことです:

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")}
        )
    }
}

私が見る限り、弱い参照はクロージャーでのみ許可されているので、それは答えではありませんでした。参照を別のオブジェクトに入れると、両方が解放される可能性があります。

簡単に操作できるようにContentViewを追加し、何が起こっているのかを確認するためにパイプラインに印刷を追加しました。計算された名前はおそらく必要ではなく、単にあなたが持っていたのと同じように見えただけです。また、セットも削除しました。おそらく便利ですが、まだ機能していません。

1
Michael Salmon

保存されているAnyCancellabledisposeBagから削除して、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

またはオプションの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