Search code examples
swiftuiimagegif

Memory leak issue trying to combine [UIImage] to single GIF image


I am trying to combine multiple UIImage as 1 gif animated image. I have a problem if the number of frames (UIImages) is more than 20. The app is closing because of memory leaks. When createGIF starts to make the gif, the memory jumps from 200MB to 2GB and keeps increasing until the app crashes due to the memory leak.

enter image description here

Here is my code:

func createGIF(from images: [UIImage], loopCount: Int = 0, frameDuration: TimeInterval = 0.1, completion: @escaping (URL?) -> Void) {
    guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
        print("Error: Unable to create documentsDirectory.")
        completion(nil)
        return
    }

    let gifURL = documentsDirectory.appendingPathComponent("\(UUID().uuidString).gif")

    guard let destination = CGImageDestinationCreateWithURL(gifURL as CFURL, kUTTypeGIF, images.count, nil) else {
        print("Error: Unable to create CGImageDestination.")
        completion(nil)
        return
    }

    let gifProperties = [
        kCGImagePropertyGIFDictionary as String: [
                    kCGImagePropertyGIFLoopCount as String: loopCount
                ]
    ]
    CGImageDestinationSetProperties(destination, gifProperties as CFDictionary)

    // Process frames in batches to reduce memory usage
    let batchSize = 10
    let totalFrames = images.count

    for startIndex in stride(from: 0, to: totalFrames, by: batchSize) {
        let endIndex = min(startIndex + batchSize, totalFrames)
        let batchImages = Array(images[startIndex..<endIndex])

        for image in batchImages {
            if let cgImage = image.cgImage {
                let frameProperties = [
                    kCGImagePropertyGIFDictionary as String: [
                        kCGImagePropertyGIFDelayTime as String: frameDuration
                    ]
                ]
                CGImageDestinationAddImage(destination, cgImage, frameProperties as CFDictionary)
            } else {
                print("Error: Unable to retrieve CGImage from UIImage.")
                completion(nil)
                return
            }
        }
    }

    guard CGImageDestinationFinalize(destination) else {
        print("Error: Failed to finalize CGImageDestination.")
        completion(nil)
        return
    }

    completion(gifURL)
}

I am trying to achieve a sticker animation gif from UIImage array. What did I forget here or what is the problem that makes the memory increase to over 2GB when I use more than 20 FPS.


Solution

  • Well, using DispatchGroup and resizing the images and converting them to CGImage before generating the actual gif image fixed my problem , the memory max peek between 150 and 200mb on generating gif .

    here is the final code could help someone :

    public func generateGifFromImages(images: [UIImage], colorSpace: colorSpace, delayTime: Double, loopCount: Int,completion: @escaping (URL?) -> Void) {
            
            // create empty file to hold gif
            let destinationFilename = String(NSUUID().uuidString + ".gif")
            let fileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(destinationFilename)
    
            let gifGroup = DispatchGroup()
            var tempImages: [UIImage] = []
            for image in images {
                gifGroup.enter()
                tempImages.append(image.resized(to: CGSize(width: 512,height: 512)))
                gifGroup.leave()
            }
            
            gifGroup.notify(queue: .main) {
                let gifFileProperties: CFDictionary = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFLoopCount as String: loopCount]]  as CFDictionary
                let gifFrameProperties: CFDictionary = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFDelayTime as String: delayTime]] as CFDictionary
                if let url = fileURL as CFURL? {
                    if let destination = CGImageDestinationCreateWithURL(url, UTType.gif.identifier as CFString, images.count, nil) {
                        CGImageDestinationSetProperties(destination, gifFileProperties)
                        for image in tempImages {
                            if let cgImage = image.cgImage {
                                CGImageDestinationAddImage(destination, cgImage, gifFrameProperties)
                            }
                        }
                        if !CGImageDestinationFinalize(destination) {
                            print("Failed to finalize the image destination")
                        }else{
                            completion(fileURL)
                        }
                    }
                }
            }
        }
    

    this is the resize extension :

    extension UIImage {
      /**
        Resizes the image.
    
        - Parameters:
          - scale: If this is 1, `newSize` is the size in pixels.
      */
      @nonobjc public func resized(to newSize: CGSize, scale: CGFloat = 1) -> UIImage {
        let format = UIGraphicsImageRendererFormat.default()
        format.scale = scale
        let renderer = UIGraphicsImageRenderer(size: newSize, format: format)
        let image = renderer.image { _ in
          draw(in: CGRect(origin: .zero, size: newSize))
        }
        return image
      }
    }