5

I'm having a hard time with dealing with Combine. After the publisher is complete I want to update a value but whenever I update that value the memory is allocated and never goes away.

Whenever I try to assign image there is a leak. If I don't assign no leak.

EDIT: Reproducible example here: https://github.com/peterwarbo/MemoryAllocation

This is what my code looks like:

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

I've also tried instead of using 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            
        }
    )
)
Peter Warbo
  • 11,136
  • 14
  • 98
  • 193

3 Answers3

11

An obvious problem with your code is that you create and store a new pipeline every time capture is called. That is the opposite of how to use Combine; you might as well not be using Combine at all. The way to use Combine is to create a pipeline once and let information come down the pipeline asynchronously from then on.

You posted an example project in which you use a Future to introduce a delay in the passing of an image down a pipeline. In your project, the user chooses an image from the photo library, repeatedly. Once again, in your project you create and store a new pipeline every time an image is chosen. I rewrote the example as follows:

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

Note the architecture: I create the pipeline once, in viewDidLoad, and whenever an image arrives I pass it down the same pipeline. Some memory is used, to be sure, because we are storing a UIImage; but it does not grow in any uncontrolled way, but levels off in an optimal manner.

enter image description here

We're using 8.4 MB after picking all the images in the library repeatedly. No problem there!

enter image description here

Also, no surplus large images are persisting. Looking at memory that comes from choosing in the image picker, one image persists; that is 2.7 MB of our 8.4 MB:

enter image description here

That is exactly what we expect.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 1
    You might want to read my online tutorial on Combine: http://www.apeth.com/UnderstandingCombine/ – matt Apr 28 '20 at 12:22
  • Thanks Matt, I'll try out your solution. Is there a way to get more compact syntax without involving variables for the publisher and pipeline? I'm trying to achieve "promise like" syntax with using combine and no 3rd party libs. – Peter Warbo Apr 28 '20 at 18:57
  • It depends on what you're really trying to accomplish. But the publisher must live _somewhere_. The photo library does not vend a publisher of its image, so _someone_ must vend it. But that doesn't _make_ one every time. Just the opposite. That's my point. – matt Apr 28 '20 at 19:15
  • Trying to achieve similar functionality as in this thread (https://stackoverflow.com/q/59428026/294661). Basically I have a few async methods, some can be performed in parallel and after they performed I want to chain another async method. What I've used before is PromiseKit but I wanted to try to achieve it with combines Future. Maybe combine is not the right fit for my needs? – Peter Warbo Apr 28 '20 at 22:00
  • I'm sure it is. It is where native Promises are kept (sorry about that). And Combine is exactly about "some can be performed in parallel and after.. I want to chain another". It's perfect for that. What's the question? Sorry, I got a bit lost. It seems to me I answered the original question...? Did you read my tutorial? – matt Apr 28 '20 at 22:55
  • I updated my pull request to reorganize more like what you seem to have in mind. – matt Apr 29 '20 at 12:48
  • Much obliged Matt. Thanks for your solution(s)! – Peter Warbo Apr 29 '20 at 22:46
1

Just by code reading... use weak self, not direct self,

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

also I would add delivery on main queue, as

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
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 1
    That didn't help unfortunately :( – Peter Warbo Apr 27 '20 at 09:38
  • @PeterWarbo, ok, it is impossible to solve by guessing.. it needs some reproducible example. – Asperi Apr 27 '20 at 10:19
  • here's a reproducible example: https://github.com/peterwarbo/MemoryAllocation – Peter Warbo Apr 28 '20 at 10:21
  • @PeterWarbo Your test is wrong. You say: Go into Debug navigator. Click on memory. In simulator click on "Pick image" and choose an image. Watch memory grow every time an image is picked. — Simulator memory management in a debug build tells you nothing. Test on a device and use Instruments; that way, measurement is correct, plus if you have a leak you will know what it is and why – matt Apr 28 '20 at 10:30
  • @PeterWarbo also your threading looks wrong. Your pipeline runs on background threads and you never switch to the main thread before setting properties of self. – matt Apr 28 '20 at 10:36
  • @PeterWarbo and your use of Combine is wrong. You are creating and storing a whole new pipeline every time you fetch an image. – matt Apr 28 '20 at 10:39
  • @matt If you have a device then of course it's better to test on device, great point. About threading I don't think it matters in this case since I'm not updating any UI I'm only setting a variable. How do you suggest I use combine for doing a one-off async operation? I'm following this guide: https://www.donnywals.com/using-promises-and-futures-in-combine/ – Peter Warbo Apr 28 '20 at 10:59
  • Well the point is that it isn't one-off. You say "Watch memory grow every time an image is picked." But every time an image is picked, you are making a whole new pipeline. That's the opposite of one-off, it's every-off. :) – matt Apr 28 '20 at 11:24
  • @PeterWarbo I forked your repo and pushed a pull request that shows one way to use combine cleanly here. Memory does not grow and grow; the needle in the gauge stays pegged at the bottom. Sure, some memory is used, but it levels off. There are some _tiny_ leaks in Apple's code but that's not your problem. – matt Apr 28 '20 at 11:59
-1

You are using .store(in: &self.storage)

need to cancel this private var storage = Set()

storage.cancel() storage.removeAll()

and also self need to be weak

zdravko zdravkin
  • 2,090
  • 19
  • 21