Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

GPUImage and GPUImageView : App terminated due to memory error

I am using a GPUImage and many GPUImageView instances. The purpose is to display the original image, layer several slices of filtered images on top, and finally animate the slice filters slowly across the original image. Imagine an image with some Sepia bars rolling across to show the normal image and sepia image in sections.

I wrapped this functionality in a subclass of UIView as seen below:

import Foundation
import QuartzCore

class FilteredImageMaskView : UIView {

init(frame: CGRect, image: UIImage){
    super.init(frame: frame);

    let imageViewFrame = CGRectMake(frame.origin.x, 0.0, frame.size.width, frame.size.height);

    let origImage = GPUImagePicture(image: image);
    origImage.forceProcessingAtSizeRespectingAspectRatio(imageViewFrame.size);

    // Display the original image without a filter
    let imageView = GPUImageView(frame: imageViewFrame);
    origImage.addTarget(imageView);
    origImage.processImageWithCompletionHandler(){
        origImage.removeAllTargets();

        var contentMode = UIViewContentMode.ScaleAspectFit;
        imageView.contentMode = contentMode;

        // Width of the unfiltered region
        let regularWidth: CGFloat = 30.0;
        // Width of filtered region
        let filterWidth: CGFloat = 30.0;

        // How much we are moving each bar
        let totalXMovement = (regularWidth + filterWidth) * 2;

        // The start X position
        var currentXForFilter: CGFloat = -totalXMovement;

        // The filter being applied to an image
        let filter = GPUImageSepiaFilter();
        filter.intensity = 0.5;
        // Add the filter to the originalImage
        origImage.addTarget(filter);

        let filteredViewCollection = FilteredViewCollection(filteredViews: [GPUImageView]());

        // Iterate over the X positions until the whole image is covered
        while(currentXForFilter < imageView.frame.width + totalXMovement){
            let frame = CGRectMake(currentXForFilter, imageViewFrame.origin.y, imageViewFrame.width, imageViewFrame.height);
            var filteredView = GPUImageView(frame: frame);
            filteredView.clipsToBounds = true;
            filteredView.layer.contentsGravity = kCAGravityTopLeft;

            // This is the slice of the overall image that we are going to display as filtered
            filteredView.layer.contentsRect = CGRectMake(currentXForFilter / imageViewFrame.width, 0.0, filterWidth / imageViewFrame.width, 1.0);
            filteredView.fillMode = kGPUImageFillModePreserveAspectRatio;

            filter.addTarget(filteredView);

            // Add the filteredView to the super view
            self.addSubview(filteredView);

            // Add the filteredView to the collection so we can animate it later
            filteredViewCollection.filteredViews.append(filteredView);

            // Increment the X position           
            currentXForFilter += regularWidth + filterWidth;
        }

        origImage.processImageWithCompletionHandler(){
            filter.removeAllTargets();

            // Move to the UI thread
            ThreadUtility.runOnMainThread(){
                // Add the unfiltered image
                self.addSubview(imageView);
                // And move it behind the filtered slices
                self.sendSubviewToBack(imageView);

                // Animate the slices slowly across the image
                UIView.animateWithDuration(20.0, delay: 0.0, options: UIViewAnimationOptions.Repeat, animations: { [weak filteredViewCollection] in
                    if let strongfilteredViewCollection = filteredViewCollection {
                        if(strongfilteredViewCollection.filteredViews != nil){
                            for(var i = 0; i < strongfilteredViewCollection.filteredViews.count; i++){
                                strongfilteredViewCollection.filteredViews[i].frame.origin.x += totalXMovement;
                                strongfilteredViewCollection.filteredViews[i].layer.contentsRect.origin.x += (totalXMovement / imageView.frame.width);
                            }
                        }
                    }
                }, completion: nil);
            }
        }
    }
}

required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder);
}

}

class FilteredViewCollection {
    var filteredViews: [GPUImageView]! = [GPUImageView]();

    init(filteredViews: [GPUImageView]!){
        self.filteredViews = filteredViews;
    }
}

An instance of FilteredImageMaskView is added programmatically to a view in a viewController. When that viewController is dismissed, the assumption is that the resources will be disposed - I was careful to avoid retain cycles. When I watch the memory consumption in the debugger on a real device, the memory does drop appropriately when the viewController is dismissed. However, if I repeatedly load that viewController to look at the image, then dismiss it, then reload it again, I will eventually encounter "App terminated due to memory error"

If I wait a while after dismissing the viewController, the memory errors seem less frequent which leads me to believe the memory is still being released after the viewController is dismissed...? But I have also seen the error after only a couple times of not so rapid opening and closing of the viewController.

I must be using the GPUImage and/or GPUImageView inefficiently and I am looking for guidance.

Thanks!

EDIT: See below for the view controller implementation.

import UIKit

class ViewImageViewController: UIViewController, FetchImageDelegate {

    var imageManager = ImageManager();

    @IBOutlet var mainView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()

        imageManager.fetchImageAsync(delegate: self);
    }

    // This callback is dispatched on the UI thread
    func imageFetchCompleted(imageData: [UInt8]) {
        let imageView = FilteredImageMaskView(frame: self.mainView.frame, image: UIImage(data: imageData));
        mainView.addSubview(imageView);

        var timer = NSTimer.scheduledTimerWithTimeInterval(NSTimeInterval(10.0), target: self, selector: Selector("displayReminder"), userInfo: nil, repeats: false);
    }

    func displayReminder(){
        // Show an alert or message here
    }

}

class ImageManager {

    func fetchImageAsync(delegate: FetchImageDelegate) {
        // This dispatches a high priority background thread
        ThreadUtility.runOnHighPriorityBackgroundThread() { [weak delegate] in
            // Get the image (This part could take a while in the real implementation)
            var imageData = [UInt8]();

            // Move to the UI thread
            ThreadUtility.runOnMainThread({
                if let strongDelegate = delegate {
                    strongDelegate.imageFetchCompleted(imageData);
                }
            });
        }
    }
}

Now that I am looking through this stripped down version, does passing self to the ImageManager create a retain cycle even though I reference it weakly to the background thread? Can I pass that as a weak reference right from the ViewImageViewController? It is certainly possible that the ViewImageViewController is dismissed before the fetchImageAsync method completes and the callback is called.

EDIT: I think I found the issue. If you look at the ViewImageViewController in the callback, I create an NSTimer and pass self. My suspicion is that is creating a retain cycle if the viewController is dismissed before the timer executes. That would explain why if I wait a few extra seconds, I don't get the memory error - because the timer fires and the viewController disposes properly. Here is the fix (I think).

// This is on the ViewImageViewController
var timer: NSTimer!;

// Then instead of creating a new variable, assign the timer to the class variable
self.timer = NSTimer.scheduledTimerWithTimeInterval(NSTimeInterval(10.0), target: self, selector: Selector("displayReminder"), userInfo: nil, repeats: false);

// And finally, on dismiss of the viewcontroller (viewWillDisappear or back button click event, or both)
func cancelTimer() {
    if(self.timer != nil){
        self.timer.invalidate();
        self.timer = nil;
    }
}
like image 798
a432511 Avatar asked May 01 '15 02:05

a432511


1 Answers

I think I found the issue. If you look at the ViewImageViewController in the callback, I create an NSTimer and pass self. My suspicion is that is creating a retain cycle if the viewController is dismissed before the timer executes. That would explain why if I wait a few extra seconds, I don't get the memory error - because the timer fires and the viewController disposes properly. Here is the fix (I think).

// This is on the ViewImageViewController
var timer: NSTimer!;

// Then instead of creating a new variable, assign the timer to the class variable
self.timer = NSTimer.scheduledTimerWithTimeInterval(NSTimeInterval(10.0), target: self, selector: Selector("displayReminder"), userInfo: nil, repeats: false);

// And finally, on dismiss of the viewcontroller (viewWillDisappear or back button click event, or both)
func cancelTimer() {
    if(self.timer != nil){
        self.timer.invalidate();
        self.timer = nil;
    }
}
like image 168
a432511 Avatar answered Oct 18 '22 08:10

a432511