6

Im seeing some memory leaks when using the memory graph debugger in xcode. The Backtrace is not linking directly to any of my code, but guessing by the trace its save to assume that its related to combine and some DataTaskPublisher.

enter image description here

Next I checked inside Instruments, where I also see some memory leaks. All leaks mention "specialized static UIApplicationDelegate.main()" inside the stack trace, but its not really linking to something that can cause a memory leak.

enter image description here

Removing the the ViewModel, that is responsible for loading Data from an API, gets rid of the leaks. The memory graph debugger was showing a dataTaskPublisher, so this kinda makes sense.

import Foundation
import Combine

enum API {
    static func games() -> AnyPublisher<[GameResult], Error> {
        let requestHeaderGames = gamesRequest()
        
        return URLSession.shared.dataTaskPublisher(for: requestHeaderGames)
            .map(\.data)
            .decode(type: [GameResult].self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
    
    private static func gamesRequest() -> URLRequest {
        let url = URL(string: "http://localhost:8080/api/games")!
        var requestHeader = URLRequest.init(url: url)
        requestHeader.httpBody =
            "filter ..."
            .data(using: .utf8, allowLossyConversion: false)

        requestHeader.httpMethod = "POST"
        requestHeader.setValue("application/json", forHTTPHeaderField: "Accept")

        return requestHeader
    }
}

struct GameResult: Decodable, Identifiable, Equatable, Hashable {
    let id: Int
    // ...
}

final class ViewModel: ObservableObject {
    @Published private(set) var games: [GameResult] = []
    
    private var subscriptions = Set<AnyCancellable>()
    
    public func unsubscribe() -> Void {
        subscriptions.forEach {
            $0.cancel()
        }
        subscriptions.removeAll()
    }
    
    func load() -> Void {
        API.games()
            .sink(receiveCompletion: { _ in }, receiveValue: { [weak self] results in
                self?.games = results
            })
            .store(in: &subscriptions)
    }
}

Im already put much time into figuring out whats causing this leak, but im more confused than anything else. CFString is nothing that im using. Also I cant find out why my code is seemingly causing this leak. Is there something that im just simply missing, or could someone help me by giving me some advise how to go about this problem?

KevinP
  • 2,562
  • 1
  • 14
  • 27
  • Seeing many unspecific leaks with iOS 14.x and SwiftUI/Combine, too. Especially linked to CFString, even from basic methods like `String(format:)`. I compared it to iOS 13.x in Instruments where I don't have any leaks. Have you tried profiling in iOS 13? – Frederik Winkelsdorf Nov 14 '20 at 09:09

1 Answers1

1

The first image you posted seems to indicate it's an issue with the Decoder publisher. I'd try a couple different scenarios to see if we can isolate the problem with the decoding:

enum API {

    static var fakeGameData: Data {
        let encoder = JSONEncoder()
        return try! encoder.encode(fakeGames)
    }

    static var fakeGames: [GameResult] {
        return [GameResult(id: 0), GameResult(id: 2), GameResult(id: 3)]
    }

    static func gamesFromData() -> AnyPublisher<[GameResult], Error> {
        return Just(fakeGameData)
            .decode(type: [GameResult].self, decoder: JSONDecoder())
            .mapError({ $0 as Error })
            .eraseToAnyPublisher()
    }

    static func gamesFromArray() -> AnyPublisher<[GameResult], Error> {
        return Just(fakeGames)
            .mapError({ $0 as Error })
            .eraseToAnyPublisher()
    }
}

See what happens if you subscribe to API.gamesFromData(). If you still see a leak, then it may be an issue with the decoder. If not, then it's more likely that you have an issue with the dataTaskPublisher.

Then, see what happens if you subscribe to API.gamesFromArray(). If you still see a leak, then you know the issue isn't the Decoder publisher.

Rob C
  • 4,877
  • 1
  • 11
  • 24
  • Hey, thx for this approach and your help! I tried what you suggested, but subscribing to both examples still would sometimes result in a Memory leak inside of the memory graph. This time it was only a single 48 Byte Malloc Block. The Data that is being fatched is displayed inside a LazyVGrid, changing the Grid to a ForEach inside a VStack removes the leak from the memory graph. The leak also only appeared inside of the LazyVGrid if not all of the elements where displayed directly (content area is not big enough) Am I save to assume that thats the problem and im forced to live with the leak? – KevinP Oct 31 '20 at 18:59
  • Ive created a new question for what I have found: https://stackoverflow.com/questions/64627309/swiftui-hiding-lazyvgrid-causes-memory-leak – KevinP Nov 01 '20 at 10:25