【问题标题】:swift combine sink receiveValue memory leakswift combine sink receiveValue 内存泄漏
【发布时间】:2020-08-07 19:10:45
【问题描述】:

我很难与Combine 打交道。发布者完成后,我想更新一个值,但每当我更新该值时,内存就会被分配并且永远不会消失。

每当我尝试分配图像时,都会出现泄漏。如果我不指定不泄漏。

编辑:此处可重现的示例:https://github.com/peterwarbo/MemoryAllocation

这是我的代码的样子:

final class CameraController: ObservableObject {

    private var storage = Set<AnyCancellable>()    
    var image: UIImage?

    func capture(_ image: UIImage) {

        PhotoLibrary.saveImageToTemporaryDirectory(image) // AnyPublisher<URL, Error>
            .zip(PhotoLibrary.saveImage(image, location: self.locationObserver.location) // AnyPublisher<UIImage, Error>)
            .sink(receiveCompletion: { [weak self] (completion) in
                switch completion {
                case let .failure(error):
                    Log.error(error)
                    self?.handleCaptureError(error)
                case .finished: break
                }
            }) { [weak self] (value) in
                print(value.1) // no leak
                self.image = value.1 // leak

            }
            .store(in: &self.storage)
     }
}

我也尝试过使用sink

.receive(
    subscriber: Subscribers.Sink(
        receiveCompletion: { [weak self] completion in
            switch completion {
            case let .failure(error):
                Log.error(error)
                self?.handleCaptureError(error)
            case .finished: break
            }
        },
        receiveValue: { value in
            print(value.1) // no leak
            self.image = value.1 // leak            
        }
    )
)

【问题讨论】:

    标签: swift memory-leaks combine


    【解决方案1】:

    只是通过阅读代码...使用弱自我,而不是直接自我,

    }) { [weak self] (value) in
        print(value.1) // no leak
        self?.image = value.1     // << here !!
        self?.storage.removeAll() // just in case
    }
    

    我也会在主队列中添加传递,因为

    PhotoLibrary.saveImageToTemporaryDirectory(image)
        .zip(PhotoLibrary.saveImage(image, location: self.locationObserver.location)
        .receive(on: DispatchQueue.main)          // << here !!
        .sink(receiveCompletion: { [weak self] (completion) in
        // ... other code here
    

    【讨论】:

    • 不幸的是,这没有帮助:(
    • @PeterWarbo,好的,靠猜测是不可能解决的。它需要一些可重现的例子。
    • 这是一个可重现的例子:github.com/peterwarbo/MemoryAllocation
    • @PeterWarbo 你的测试是错误的。你说:进入调试导航器。点击内存。在模拟器中单击“选择图像”并选择一个图像。每次选择图像时,观察内存增长。 — 调试版本中的模拟器内存​​管理不会告诉您任何信息。在设备上测试并使用仪器;这样,测量是正确的,而且如果你有泄漏,你会知道它是什么以及为什么
    • @PeterWarbo 你的线程看起来也有问题。您的管道在后台线程上运行,并且在设置 self 属性之前您永远不会切换到主线程。
    【解决方案2】:

    您的代码的一个明显问题是每次调用capture 时都会创建并存储一个新管道。这与使用 Combine 的方式相反;您可能根本不使用 Combine。使用 Combine 的方法是创建一个管道一次,然后让信息从管道异步传下来。

    您发布了一个示例项目,在该项目中,您使用 Future 来引入图像沿管道传递的延迟。在您的项目中,用户从照片库中反复选择一张图像。再一次,在您的项目中,每次选择图像时,您都会创建并存储一个新的管道。我将示例改写如下:

    import UIKit
    import Combine
    
    class ViewController: UIViewController, UINavigationControllerDelegate {
        let queue = DispatchQueue(label: "Queue", qos: .userInitiated, attributes: [], autoreleaseFrequency: .workItem)
        var image: UIImage?
        var storage: Set<AnyCancellable> = []
        let publisher = PassthroughSubject<UIImage, Never>()
        override func viewDidLoad() {
            super.viewDidLoad()
            self.publisher
                .flatMap {image in
                    self.futureMaker(image: image)
                }
                .receive(on: DispatchQueue.main)
                .sink(receiveCompletion: { (completion) in
                }) { (value) in
                    print("finished processing image")
                    self.image = value
                }
                .store(in: &self.storage)
        }
        @IBAction func didTapPickImage(_ sender: UIButton) {
            let picker = UIImagePickerController()
            picker.delegate = self
            present(picker, animated: true)
        }
        func futureMaker(image: UIImage) -> AnyPublisher<UIImage, Never> {
            Future<UIImage, Never> { promise in
                self.queue.asyncAfter(deadline: .now() + 0.5) {
                    promise(.success(image))
                }
            }.eraseToAnyPublisher()
        }
    }
    extension ViewController: UIImagePickerControllerDelegate {
        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            dismiss(animated: true)
            guard let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else { return }
            print("got image")
            self.publisher.send(image)
        }
    }
    

    注意架构:我在viewDidLoad 中创建了一次管道,每当图像到达时,我将其传递到相同 管道。可以肯定的是,使用了一些内存,因为我们正在存储 UIImage;但它不会以任何不受控制的方式增长,而是以最佳方式趋于平稳。

    在反复选取库中的所有图像后,我们使用了 8.4 MB。没问题!

    此外,没有多余的大图像持续存在。查看来自图像选择器中选择的内存,一个图像仍然存在;这是我们 8.4 MB 中的 2.7 MB:

    这正是我们所期望的。

    【讨论】:

    • 您可能想阅读我关于合并的在线教程:apeth.com/UnderstandingCombine
    • 谢谢马特,我会试试你的解决方案。有没有办法在不涉及发布者和管道变量的情况下获得更紧凑的语法?我正在尝试使用 combine 和没有 3rd 方库来实现“类似承诺”的语法。
    • 这取决于你真正想要完成什么。但发布者必须住在某处。照片库不出售其图像的发布者,因此某人必须出售它。但这并不是每次都制作一个。恰好相反。这就是我的观点。
    • 尝试实现与此线程 (stackoverflow.com/q/59428026/294661) 中类似的功能。基本上我有一些异步方法,有些可以并行执行,在它们执行之后我想链接另一个异步方法。我之前使用的是 PromiseKit,但我想尝试通过结合 Future 来实现它。也许 combine 不适合我的需求?
    • 我敢肯定。这是保存原生 Promise 的地方(对此感到抱歉)。而Combine正是关于“一些可以并行执行,然后......我想链接另一个”。这是完美的。问题是什么?对不起,我有点迷路了。在我看来,我回答了原来的问题......?你读过我的教程吗?
    【解决方案3】:

    你正在使用 .store(in: &self.storage)

    需要取消这个私有 var storage = Set()

    storage.cancel() storage.removeAll()

    自己也需要软弱

    【讨论】:

      猜你喜欢
      • 2020-04-29
      • 2021-02-13
      • 2020-08-02
      • 1970-01-01
      • 2020-03-19
      • 1970-01-01
      • 2016-03-28
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多