【问题标题】:Memory management with image saved from UIImagePickerController使用从 UIImagePickerController 保存的图像进行内存管理
【发布时间】:2012-05-20 18:32:53
【问题描述】:

我正在编写一个应用程序,在该应用程序中,用户为他们自己拍照,然后通过一系列视图使用导航控制器调整图像。如果用户使用前置摄像头拍摄照片(在支持它的设备上设置为默认设置),这非常有效,但是当我重复该过程时,我完成了大约一半,并在引发内存警告后崩溃。

在 Instruments 中进行分析后,我发现我的应用程序内存占用在使用较低分辨率的前置摄像头图像时保持在 20-25 MB 左右,但在使用后置摄像头时,每次视图更改都会再增加 33 MB 左右,直到它崩溃350 MB(在 4S 上)

下面是我用来处理将照片保存到文档目录的代码,然后读取该文件位置以将图像设置为UIImageView。这段代码的“读取”部分通过几个视图控制器(viewDidLoad)循环使用,以便将我保存的图像设置为每个视图中的背景图像。

我已经删除了我所有的图像修改代码,以将其剥离到最小限度,试图隔离问题,但我似乎找不到它。就目前而言,该应用所做的只是在第一个视图中拍摄一张照片,然后将该照片用作大约 10 个视图的背景图像,并在用户浏览视图堆栈时进行分配。

现在显然更高分辨率的照片会使用更多的内存,但我不明白为什么低分辨率的照片似乎没有使用越来越多的内存,而高分辨率的照片却持续使用越来越多,直到崩溃。

我如何保存和读取图像:

- (void) imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    UIImage *image = [info objectForKey:@"UIImagePickerControllerOriginalImage"];    
    jpgData = UIImageJPEGRepresentation(image, 1);

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);  
    NSString *documentsPath = [paths objectAtIndex:0];
    filePath = [documentsPath stringByAppendingPathComponent:@"image.jpeg"];
    [jpgData writeToFile:filePath atomically:YES];    

    [self dismissModalViewControllerAnimated:YES];
    [disableNextButton setEnabled:YES];

    jpgData = [NSData dataWithContentsOfFile:filePath];
    UIImage *image2 = [UIImage imageWithData:jpgData];
    [imageView setImage:image2];
}

现在我知道我可以在保存之前尝试缩放图像,我计划接下来研究它,但我不明白为什么这不起作用。也许我错误地认为 ARC 会在视图及其子视图离开堆栈顶部时自动释放它们。

谁能解释我为什么要囤积设备内存? (希望我完全忽略了一些简单的事情)我是否设法将 ARC 扔出窗外?

编辑:我如何在其他视图中调用图像

- (void)loadBackground
{
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);  
    NSString *documentsPath = [paths objectAtIndex:0];
    NSString *filePath = [documentsPath stringByAppendingPathComponent:@"image.jpeg"];
    UIImage *image = [UIImage imageWithContentsOfFile:filePath];
    [backgroundImageView setImage:image];

}

我的视图控制器之间的导航是如何建立的:

编辑 2:

我的基本声明是什么样的:

#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
@interface PhotoPickerViewController : UIViewController <UIImagePickerControllerDelegate, UINavigationControllerDelegate>
{
    IBOutlet UIImageView *imageView;
    NSData *jpgData;
    NSString *filePath;
    UIImagePickerController *imagePicker;
    IBOutlet UIBarButtonItem *disableNextButton;
}


@end

如果相关,我如何调用我的图像选择器:

- (void)callCameraPicker
{


    if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] == YES)
    {
        NSLog(@"Camera is available and ready");

        imagePicker.sourceType =  UIImagePickerControllerSourceTypeCamera;
        imagePicker.delegate = self;
        imagePicker.allowsEditing = NO;
        imagePicker.cameraCaptureMode = UIImagePickerControllerCameraCaptureModePhoto;

        NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; for (AVCaptureDevice *device in devices) 
        {
            if([[UIScreen mainScreen] respondsToSelector:@selector(scale)] && [[UIScreen mainScreen] scale] == 2.0) 
            {
                imagePicker.cameraDevice = UIImagePickerControllerCameraDeviceFront;


            }
        }


        imagePicker.modalTransitionStyle = UIModalTransitionStyleCoverVertical;

        [self presentModalViewController:imagePicker animated:YES];

    }


    else
    {
        NSLog(@"Camera is not available");
        UIAlertView *cameraAlert = [[UIAlertView alloc] initWithTitle:@"Error" 
                                                              message:@"Your device doesn't seem to have a camera!" 
                                                             delegate:self cancelButtonTitle:@"Dismiss" 
                                                    otherButtonTitles:nil];
        [cameraAlert show];

    }
}

编辑 3: 我记录了 viewDidUnload,实际上它没有被调用,所以我现在在 viewWillAppear 中调用 loadBackground 并在 viewDidDisappear 中设置我的 backgroundImageView nil。我希望这会有所帮助,但没有任何区别。

- (void)viewWillAppear:(BOOL)animated
{
    [self loadBackground];
}


- (void)viewDidDisappear:(BOOL)animated
{
    NSLog(@"ViewDidDisappear");
    backgroundImageView = nil;
}

【问题讨论】:

  • 你不应该使用imageWithData,因为你可以使用imageWithContentsOfFileimageWithData 无法缓存,因此您将为每个图像创建一个新的字节缓冲区。
  • 我可以问一下,当您已经从信息字典中的 UIImagePickerControllerOriginalImage 键中将 image2 作为 UIImage 对象时,为什么要从数据中创建 image2 作为 UIImage 对象?完成所有任务以释放内存后,您也可以取消图像和 jpgdata...
  • @JonathanCichon 不幸的是,imageWithDataimageWithContentsOfFile 都没有缓存数据。
  • 你绝对不应该通过从文件中加载来创建image2 - 只需执行[imageView setImage:image]。通过第二次加载,您的内存需求增加了一倍。但这只能解释这么多-我认为我们需要在导航堆栈中查看您的 10 个视图的一些示例代码,以了解您在其中所做的事情-听起来您正在加载每个视图中的图像并以某种方式阻止 iOS释放导航堆栈中较低的那些。
  • 好点,我已经对那段代码进行了更正,实际上并没有什么可显示的。我上面列出的代码(阅读部分)在我的其他视图控制器的 viewDidLoad 方法中被回收,它们只是链接到故事板中的视图控制器,设置为从一个推送到另一个。

标签: objective-c ios cocoa-touch memory uiimagepickercontroller


【解决方案1】:

UIImageUIImageView 之间的关系不一定对每个人都直观。

UIImage 是图像数据的高级表示 - 就显示数据而言,它本身没有任何作用。

UIImageViewUIImage 一起使用,让您可以显示图像。

UIImageView 的多个实例没有理由不能显示相同的UIImage。这是很好且高效的,因为图像数据在内存中只有一个表示,由多个视图共享。

您似乎正在做的是通过从磁盘加载它来为您的每个视图创建一个新的UIImage。所以从两个方面来看,这是一个糟糕的通用设计:一遍又一遍地实例化实际上相同的UIImage,并反复从磁盘重新加载相同的图像数据。

您的内存问题实际上是一个单独的问题,您没有正确释放一直加载到UIImage 对象和UIImageViews 中的图像数据。

理论上,您应该能够获取您从 UIImagePickerController 获得的第一个 UIImage 并将该引用传递给您的视图,而无需从磁盘重新加载。

如果由于更高级别的功能要求(例如,因为图像正在被用户更改并且您想继续保存),您需要从磁盘保存和重新加载,您需要确保完全撕裂通过从它的视图层次结构中删除之前的UIView。在视图的 dealloc 方法中设置断点以确认它被删除和解除分配是有帮助的,并确保您设置了对子视图的任何 iVar 引用(看起来您的 backgroundImageView 是一个 iVar)已设置为零。如果您没有正确拆除该backgroundImageView,它会继续持有对您设置为其图像属性的UIImage 的引用。

【讨论】:

  • 我想知道这件事。这是否意味着将一堆 UIImage 存储在一个类中的数组中(比如说一个单例)并设置 myImageView.image = [singletonarray objectAtIndex:k]; 会更有效在整个应用程序中,与调用 myImageView.image = [[UIImage imageNamed:someNSStringWithTheImageName]; 相比?这两个选项有很大不同吗? @Rob Reuss
  • 显然,如果您的内存中只有您正在显示的图像(或即将显示的图像,因为它们在滚动视图上),这是最好的。如果您的应用程序提供对用户可能随机决定显示的图像集合的访问,并且您希望通过预加载它们或在加载后保留它们来最大化用户的性能,那么 NSCache 可能是一个很好的解决方案。它的功能类似于 NSMutableDictionary,但如果存在内存限制,它将动态删除对象。您可以使用它们的文件路径作为键将图像存储到 NSCache。很酷。
  • @0x7fffffff - 对解决此问题的路线感兴趣。您是否决定使用原始 UIImage 中的引用并将其传递给其他 VC,或者您是否遵循第二种方法来确保正确删除/释放视图?
【解决方案2】:

您发布的代码有几件事好奇

  1. 您的视图回调实现都没有调用super太糟糕了!请特别确保您在 viewDidUnload 和(如果您实现了)didReceiveMemoryWarning 中调用了 super。
  2. 确保以有意义的方式实现didReceiveMemoryWarning
  3. 真的不应该一遍又一遍地重新创建该图像!我假设您没有编辑实际图像,因为您在其上使用了 JPEG 压缩——即使是 100% 质量——每次保存都会使您的图像质量下降……
  4. 检查viewDidUnload 的实现,确保将每个IBOutlets 设置为nil

ARC 不是 Pixie Dust™!它只是为您节省了一点打字时间,它不会让您免于设计和维护对象图!

根据您的问题,我至少看到这些图表引用了您的图片:

image 1 <- image-view 1 <- view-controller 1 <- navigation-controller <- key window <- application

image 1 <- image-view 1 <- view 1 <- view-controller 1 <- navigation-controller <- key window <- application

对于每个视图控制器重复此操作,并在视图控制器、视图、图像视图和图像上进行索引移位。虽然您必须为您的视图控制器提供单独的视图和图像视图,但我想不出为什么您需要同一图像的多个副本。

因此,内存消耗的首要问题显然是不再创建相同图像数据的所有副本 — 我估计这将为您节省一半的内存。

接下来的事情是,ARC 只有在不再被引用时才能释放对象消耗的内存。

在内存方面,视图并不是完全轻量级的对象,当您构建一个深度导航堆栈时,您最终会得到大量的视图。

因此,您还需要取消对这些视图的任何不需要的强引用。

级别,这必须发生在视图控制器。这个应该发生的最晚时间是在视图控制器的viewDidUnload实现中。

为什么是视图控制器?

根据您的描述,图像本身仅由 UIImageView 引用 - 这是一个糟糕的选择,恕我直言,但我离题了...
UIViewController 旨在“知道”何时需要其视图以及何时需要其视图处理它是安全的——这就是它实现 didReceiveMemoryWarningviewDidUnload 的原因:

如果内存压力变大并且视图控制器的视图不在“屏幕上”,didReceiveMemoryWorning 的根实现将放开它的视图并随后调用viewDidUnload

这就是为什么您必须在实现这两种方法时调用super

另外,这就是为什么如果你有强大的 IBOutlets 引用视图控制器视图的子视图,你必须viewDidUnload 中将它们设为 nil,否则系统无法回收它们占用的内存。

UIViewController 的核心是一个大型有限状态机。所有这些“something-will/did-whatever”回调都用于在这些状态之间进行转换,并且大多数默认实现都会做一些非常重要的簿记以保持所有状态井然有序。

如果你没有在你的覆盖中调用它们,你最终会处于不一致的状态和坏事——比如这种内存不足的崩溃——发生。

【讨论】:

    【解决方案3】:

    只需创建单独的文件夹并将您的 Capture 图像保存在其中。成功操作后,使用 nsfilemager 清除文件夹数据(或)文件夹。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-07-24
      • 2012-02-04
      • 2011-08-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多