我遇到一个问题,我的应用程序在连续加载图像文件时会大量消耗内存到“崩溃点”。例如,考虑以下代码,该代码重复加载和释放 15MB JPEG 文件(用于测试目的的大文件大小):
NSURL *inputUrl = [NSURL URLWithString:@"file:///Users/me/Desktop/15MBjpeg.jpg"];
for(int i=0; i<1000; i++) {
NSImage *image = [[NSImage alloc] initWithContentsOfURL:inputUrl];
[image release];
}
由于有足够的可用系统内存,它在前几次中执行得很快,但最终系统在我所说的“崩溃点”处崩溃了。在这里,我相信系统释放了足够的内存来加载下一个图像,因此性能最终会变慢。此外,现在其他应用程序运行缓慢,因为系统必须释放这些占用但现在未使用的内存。
对我来说有意义的是,如果它分配内存,然后让系统释放它,以便活动监视器中的“真实内存”统计数据保持较小,而不是通过连续的加载/释放迭代进入千兆字节。实际上,我可能永远不会在这一点上结束,但奇怪的是,当任何时候所需的实际驻留内存通常都很小时,我的活动监视器“Real Mem”统计数据最终超过了所有其他应用程序。我最初认为这是某种内存泄漏或缓存问题,但它似乎与应用程序中的积极内存分配和系统上的惰性内存释放更相关(如果操作系统有该策略,则不是有任何问题 - 如果是这样)事实上它的工作方式)。也许我完全错过了一些东西。
有没有更好的方法来重复加载图像,而不会出现这种占用而不主动释放内存使用行为?也许有一种方法可以强制减少应用程序内存占用,或者有一种方法可以更智能地了解如何将图像加载到相同的对象或内存位置?我的目标是加载图像,处理它(获取缩略图,更改图像格式等),在内存中删除它,然后再次执行 - 所有这些都不会观察到内存增长。
--
跟进:
巴伐利亚,谢谢。包装 NSAutoreleasePool 确实可以解决加载同一文件时迭代内存增长的问题:
NSURL *inputUrl = [NSURL URLWithString:@"file:///Users/me/Desktop/15MBjpeg.jpg"];
for(int i=0; i<1000; i++) {
NSAutoreleasePool *apool = [[NSAutoreleasePool alloc] init];
NSImage *image = [[NSImage alloc] initWithContentsOfURL:inputUrl];
[image release];
[apool drain];
}
但是,它并没有解决图像释放后(并且 NSAutoreleasePool 耗尽)内存仍然增加的问题。例如,当加载我的 15MB JPEG 图像时,“Real Mem”内存从 8MB 稳定状态跳到大约 25MB,然后停留在那里。 (我的应用程序只有一个 Interface Builder 按钮,该按钮具有连接到仅调用我复制的 for 循环的方法的 IBAction )。我希望在 for 循环完成后(或者如果只加载和释放一张图像),“Real Mem”统计数据将减少回名义应用程序级别的内存使用量。
调用 NSImage 功能时也可以加载后台的其他内容,这似乎是合理的,这可以增加内存。然而,不同大小的图像(15MB、30MB、50MB 等)会按比例增加应用程序中的内存,这让我相信它不仅仅是这样的分配。
此外,如果我尝试连续加载单独的图像(例如 15MBjpeg-1.jpg、15MBjpeg-2.jpg 等),则内存有时加载每个新图像的化合物。例如,如果连续加载两个图像,则加载/释放后应用程序的“Real Mem”内存使用量现在约为 50MB,并且根据我的观察,它永远不会减少。加载后续图像时,此行为会继续存在,因此应用程序在加载多个大图像后可能会占用数百 MB 的“Real Mem”内存。
有趣的是,如果我一遍又一遍地重新加载相同的图像,稳态内存不会增加。这是否表明正在进行某种缓存?同样,我的目标是在不增加内存的情况下对几个不同的图像文件进行批处理。提前致谢。
哦,我正在研究 Heapshot Analysis 文章 ATM,但至少想发布我的进度并看看是否有其他输入。
--
后续#2
bbum,感谢这篇精彩的文章。我用我的测试程序运行了 Instruments Allocations,但没有发现任何堆增长。正如所引用的博客文章中所述,我的方法是,1)单击 Interface Builder 界面上的“加载和释放图像”按钮(调用加载/释放行为),2)每隔几秒单击几次“标记堆”在“工具分配”中,然后 3) 重复 1) 和 2)。
使用这种方法,随着时间的推移,Heapshots 在堆增长列中始终报告 0 字节(每 5 秒点击 3 次,持续 15 秒),这意味着没有从基线 Heapshot 分配额外的内存。另外,在“统计”窗格中,每次单击“加载并释放映像”按钮时都会有 13.25MB 的 Malloc,但 Live Bytes 为 0 字节,这意味着它已完全释放。如果我单击“加载和释放映像”按钮三次,则映像的总字节数报告为 39.75MB (3 * 13.25MB),这意味着已分配 39.75MB,但由于实时字节数为 0,因此已完全释放。分配图快速上升并立即下降,因为这是一个相当快的操作。内存的稳态使用没有泄漏也没有增长,这一切似乎都是有道理的。
但是,现在我该怎么办,我的“Real Mem”统计数据仍然很高?我知道活动监视器不是调试内存问题的标准。但是,“Real Mem”仍然保持高位,直到我关闭程序,然后“Real Mem”全部回到“免费”类别,这对我来说很奇怪。
我通过在相同的方法中复制代码,使用两个图像(15MBjpeg-1.jpg、15MBjpeg-2.jpg)测试了相同的方法,并且我再次观察到没有堆增长。显然,已经进行和释放了更多的分配。然而,现在“Real Mem”的增量大约是仅加载和释放一张图像的情况的两倍。同样,在测试程序的稳定状态下它不会减少。
我还能做些什么吗?以下是单个图像加载/释放的测试代码,供任何想要尝试的人使用(只需将 IB 按钮连接到 openFiles):
#import "TestMemory.h" // only declares -(IBAction) openFiles:(id)sender;
@implementation TestMemory
-(IBAction) openFiles:(id)sender {
NSURL *inputUrl = [NSURL URLWithString:@"file:///Users/me/Desktop/15MBjpeg.jpg"];
NSAutoreleasePool *apool = [[NSAutoreleasePool alloc] init];
NSImage *image = [[NSImage alloc] initWithContentsOfURL:inputUrl];
[image release];
[apool drain];
}
@end
谢谢阅读。 :)
--
后续#3
bbum,不,我没有启用垃圾收集(GC 在项目设置中设置为不支持)。我使用 vmmap 和仪器分配中的 VM Tracker 栏研究了内存。当您提到 VM Instruments 时,我假设您指的是 VM Tracker 数据,因为它报告的信息与 vmmap 相同。使用“加载和释放映像”按钮打开单个映像后,一些重要的内存数字包括以下内容(来自 VM Tracker):
Type %ofRes ResSize VirtSize Res% %AllDirty DirtySize
__TEXT 38% 33.84MB 80.45MB 42% 0% 0Bytes
*Dirty*
32% 28.23MB 114.99MB 24% 100% 17.11MB
MALLOC_LARGE 14% 13.25MB 13.25MB 100% 0% 4KB
Carbon 11% 9.86MB 9.86MB 100% 20% 3.46MB
VM_ALLOCATE 9% 8.43MB 48.17MB 18% 49% 8.43MB
...
有趣的是,单个图像的后续加载/释放会增加该图像的“驻留大小”Dirty和 VM_ALLOCATE 类型约 0.3MB,并且这些类型的“脏大小”也会随着时间的推移而增加。 (VM_ALLOCATE 似乎是一个子集Dirty)。其他类型似乎不会随着后续加载/发布而改变。
我不确定要从这些数据中删除什么,或者如何使用它来使程序释放内存。看起来 VM_ALLOCATE 类型可能是未释放的块,但这只是猜测。底层 NSImage init 实现的某些部分是否可能保存图像文件的缓存并且不会释放它?再次,如前所述,令我感兴趣的是,与第一次加载/释放相比,同一图像文件的每次后续加载/释放几乎不消耗资源(CPU、磁盘研磨等)和挂钟时间。提前致谢。