最近在做涂鸦小程序的时候,发现几个内存问题。
涂鸦Demo这个程序打开后是进入到相册选择图片,接着载入一个UIScrollView,然后在UIScrollView上添加一个UIImageView,再将选择图片设置为ImageView的Image。涂鸦的时候,将一个UIView加在UIImageView上,绘图将在这个UIView上进行。
第一个问题
现在的问题是,只要开始涂鸦,内存就会从暴涨,涨幅跟图片的大小有关,见下图。
图片大小 | 涂鸦前内存 | 涂鸦后内存 | 内存涨幅 |
21K | 4.9M | 8.1M | 3.2M |
104K | 4.5M | 29.5M | 25M |
563K | 6.9M | 35.1M | 28M |
为什么小图片涂鸦前所占内存更大?大概是因为图片较小时,对内存影响更大的因素可能是颜色等。104K的图片偏黑,而21K的图片偏亮。
经过不断注释代码,发现内存暴涨的原因是,我在UIView的touchesMove函数中调用了[self setNeedsDisplay],即手指移动的每一个过程,都会让界面重绘。那么就可以解释上图为什么内存的涨幅与图片的大小相关,因为重绘的界面与图片的大小相关。
目前绘图的原理是,由于重绘的函数会清空上次的绘画内容,所以每次重绘时需要将以前画的轨迹重新绘一遍。举个例子,假如画一条线有3个点,则手指移动到第一点的时候,画第一个点,手指移动到第二点的时候,将第一第二个点画一下,手指移动到第三个点的时候,将第一第二第三个点都画一下;同理,画第二条线的时候,第一条线的每个点每时每刻都被重画。
这种方法有个好处,就是撤销功能很简单,只要将最后一条线从记录中删除,再重绘一下,就好像撤销了最新的那条线,其实是以前的所有线条被重画了一次。
坏处显而易见,耗时,耗CPU,耗内存。
所以现在,只能寻找一种可以保存上一次的绘画内容又能完成撤销功能的绘图方法就应该能解决问题,待续……
第二个问题
在研究第一个问题的时候,发现第二个问题,就是从相册中选择图片,进入预览界面后内存会上升(正常,因为载入了图片),再取消,回到相册。此时内存不会变,这就有问题了,按理说预览界面已经不存在,为什么内存不会降到跟选图前一样呢?
用XCode的Profile的Leak工具检查了一下,发现内存泄漏的原因是UIStatusBarHideAnimationParameters和UIImage。
先看UIImage,检查了一下代码,发现一个隐藏得比较深的内存泄漏问题。下面是打开相册的ViewController中的代码,该代码是在选择照片后被调用。选择照片后跳转到预览模式的ViewController。
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { /*选择图片后,获得图片*/ UIImage *image = [info objectForKey:UIImagePickerControllerOriginalImage]; /*跳转*/ QLMomentMainViewController *viewController = [[QLMomentMainViewController alloc] initWithNibName:nil bundle:nil]; viewController.image = image; [picker dismissViewControllerAnimated:YES completion:^(void){}]; [self.navigationController pushViewController:viewController animated:YES]; [viewController release]; }
注意红色语句,首先该image不是第一个ViewController创建的(没有一个包含alloc/new/copy/mutableCopy的方法),所以不需要它来释放image;然后第二个ViewController也没有一个包含alloc/new/copy/mutableCopy的方法来创建image,所以,我天真的以为,第二个ViewController不需要对这个image的释放负责。所以没有在第二个ViewController的dealloc函数中释放该image。
其实这是错误的,因为viewController.image = image;这行代码会让image的引用计数+1,这个image是我们自己创建的property,所以在其隐含的setImage方法中对image retain了一次。所以第二个ViewController有责任在dealloc中release该image属性。
OK,那么现在只要在第二个ViewController中的dealloc中release该image,问题解决。
第三个问题
剩下就是这个神秘的UIStatusBarHideAnimationParameters。
经不断测试,发现出现这种情况的原因是,进入相册,不选图,取消之后出来,那么就会发生一次内存泄漏。
刚开始还以为是我在工程的Info.plist文件中添加的用于隐藏状态栏的原因
可是删掉之后还是出现内存泄漏。于是只能上网求助。
后来发现,我重写了完成选择照片的函数
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
却没有重写取消选择照片的函数
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
该函数的描述中有这么一句:
Your delegate’s implementation of this method should dismiss the picker view by calling the dismissModalViewControllerAnimated: method of the parent view controller.
Implementation of this method is optional, but expected.
ok,那么重写这个函数,并在里面调用[picker dismissViewControllerAnimated:YES completion:^(void){}];来让相册界面消失。
……
但是,问题依然存在!
StackOverFlow上有人遇到这个问题,有人建议:
Maybe you need to clear the delegate for your UIImagePickerController? Delegates can prevent objects from being properly deallocated.
于是我在完成选择照片或取消选择照片的回调函数中添加一句pick.delegate = nil,
……
但是,问题依然存在!
然后,StackOverFlow有人说,
It's a bug in the SDK.
然后,下了一下苹果的官方源码——一个打开相册选择图片的demo,发现竟然也会出现这个神秘的UIStatusBarHideAnimationParameters引发的内存泄漏!
There is a know issue with the uiimagepickercontroller
with memory leaks.
Apple recommend that you only allocate and instantiate only one instance and store it somewhere for the life of the application (whilst running that is).
所以目前只能把picker声明为全局变量,避免多次alloc和release。
目前问题只能解决到这里了。
总结
1.记得必要时释放你自己创建的property,不能一味依赖alloc,new,copy等字眼来决定是否release.
2.打开系统相册再退出会发生内存泄漏,这是苹果的Bug?