本文翻译自Brian's Brain的
在我的上一篇文章中,我描述了试图用图片平铺的方式来解决在ipad上展示“大型图片”的问题的第一次尝试。在这种方式中,你把图片拉伸成不同的尺寸,然后把每个图片分割成一张张正方形的片段。通过使用Cocoa框架提供的CATiledlayer类,你可以在不同的缩放层级下,绘制所需要的图片片段。
但是,在iPad 1上运行时,当我试图为大型图片计算分割的片段时,仍然偶尔会把内存用完。因此,在Pholio2.1版本中,选择了一个更加简单的方法。当用户提供了一个大的图像,我将其缩小到一个可管理的大小。我选择把图片长和宽的像素控制在1500像素以内。这样显示这张图片将需要9MB内存,用户仍然可以放大一点来看到更多的细节。
记住,平铺图片的技术需要你多次调整图片的尺寸,并且在每个尺寸,你都需要计算和保存图片的片段。这些都需要耗费时间和内存。现在这种方法,不但简单,而且更快。。。我只需要调整和保存图片一次,在UIImageViewController 中显示一个已经被调整大小的图片,而不是使用CATiledLayer显示一张图片的片段,同样也会防止用户在滚动到一个新的图片时产生闪烁。 这种方法的唯一缺点就是:用户不能放大来看清他们图片的实际像素。
尽管如此,仍然有些技巧来提高调整图片尺寸的效率,我用Pholio这个应用来告诉你这些技巧吧。
技巧一:有一个简单的调整图片尺寸的程序(方法)
在Pholio中,这个方法就是 - [IPPhoto optimize] 。 我将会更加详细地讲解里面的一些细节,但在一个更高的视角,这个方法有下面这些关键的属性:
同步。 这意味着可以很简单地进行单元测试。 也意味着慢。。。可以更多地关心多线程中的其他问题。
防止过多占用内存。这个问题就是在调整大尺寸图片时会消耗大量的内存,我的工作就是确保在这个程序返回时所有可能的内存都被释放了。尽可能少地把IPPhoto对象放到任何的自动释放池中。因为调整图片尺寸、释放内存等操作都会占用系统资源。
做了所有必要的准备工作,以使图片高效率地显示在iPad上。这意味着,所有调整大图片尺寸,为所有图片生成缩略图的工作都会在- [IPPhoto optimize ] 方法里面完成。
关键:我组织代码的其余部分,确保IPPhoto对象延迟加载,仅在我调用之后才在数据模型中构造。这意味着一切需要显示在屏幕上的数据模型都是公平的。
经过围绕在调整图片大小的一个同步程序的编写后,可以很容易推理出程序的正确行为。但是因为这个程序需要很长的运行时间,必须要在后台线程中执行,否则影响用户界面的响应。
技巧二:使用后台线程 - 但是不要太多
我的第一个错误是天真地让所有用GCD调度这个程序 - [IPPhoto optimize] 都在后台线程队列中响应。然后,一旦图片在后台完成优化,我会在主线程中把这个图片插入到模型中。
问题?太多的后台优化!因为每次调用 - [IPPhoto optimize] 都会消耗很多内存,这样同一时间超过一个优化都会把iPad 1 弄垮。而这就是我把不同的优化放到后台队列中发生的事情。不知道还好,原来GCD会同时调度安排工作运行。
为了解决这个问题,我引入了一个新的类, IPPhotoOptimizationManager(.h, .m )。这个优化图片的管理器可以完成下面的任务:
它创建一个单一的NSOperationQueue 来进行所有的内存密集型操作,如调整大小。然后使用 - [NSOperationQueue setMaxConcurrentOperationCount: ] 方法,确保同一时间最多只有一个后台优化。
它定义了一个对一张或多张图片进行优化的队列,并在主线程中调用上面的优化程序来简单地完成工作。
它维护一个所有正在进行和正在等待的优化的计数器。
每当正在进行的优化操作数量改变,都会通知委托,我使用这个提示用户优化正在进行。
通过使用 IPPhotoOptimizationManager 类,Pholio 可以控制后台的优化。
技巧三:ImageIO是你的朋友
我原始的调整图片尺寸的代码来自[ Trevor's Bike Shed ][2]的帮助。它运行良好,同时也可作为你开始编写调整图片尺寸代码的开始。
但是,因为每隔一段时间我的程序就会因为内存爆满而崩溃,我决定直接使用跟底层的源代码:Apple提供的多才多艺的ImageIO库。ImageIO是一个C语言写的程序库,而不是用Objective-C,因此会有一点难度,但它是读取和调整图片的最有效方式。
我主要在 - [IPPhoto optimize] 方法中使用ImageIO。下面是它如何工作的。首先,使用ImageIO,可以从一个图片源开始(CGImageSourceRef)。你可以从任何文件中通过生成一个文件URL来创建一个图片源。Pholio 代码是 :
NSURL *imageUrl = [NSURL fileURLWithPath:self.filename];CGImageSourceRef imageSource = CGImageSourceCreateWithURL((CFURLRef)imageUrl, NULL);
记住 CGImageSourceRef 是Core Foudation 中的一个对象,所有你需要调用 CFRelease() 来释放它。
有了图片源,Pholio接着确定是否需要调整图片尺寸。我需要调整最大像素为1500以上的所有图片(在代码中,为常量kIPPhotoMaxEdgeSize)。注意,ImageIO可以让你获得图片的元数据,而不用读取整个图片到内存中,这个可以用来确定图片的尺寸。
CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); CFNumberRef pixelWidthRef = CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelWidth); CFNumberRef pixelHeightRef = CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelHeight); CGFloat pixelWidth = [(NSNumber *)pixelWidthRef floatValue]; CGFloat pixelHeight = [(NSNumber *)pixelHeightRef floatValue]; CGFloat maxEdge = MAX(pixelWidth, pixelHeight); if (maxEdge > kIPPhotoMaxEdgeSize) { // Need to resize } CFRelease(imageProperties);
好了,如果我确定了我需要调整图片的大小,那是如何实现的?答案就是ImageIO 的CGImageSourceCreateThumbnailAtIndex() 方法。这个函数在使用时特别别扭,因为你需要通过字典来传递最有意义的参数(这才是ImageIO做的所有工作,但是到目前为止,我已经能够在其他的ImageIO调用中,忽略它)。我是有点懒惰的程序员,所有我把CFDictionaryRef和NSDictionary之间的转换封装到NSDictionary 的构造函数中。
NSDictionary *thumbnailOptions = [NSDictionary dictionaryWithObjectsAndKeys:(id)kCFBooleanTrue, kCGImageSourceCreateThumbnailWithTransform, kCFBooleanTrue, kCGImageSourceCreateThumbnailFromImageAlways, [NSNumber numberWithFloat:kIPPhotoMaxEdgeSize], kCGImageSourceThumbnailMaxPixelSize, nil]; CGImageRef thumbnail = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, (CFDictionaryRef)thumbnailOptions);
上面的代码说明:
调整大小后的图片应该包含原始图片中存在的所有旋转转换(kCGImageSourceCreateThumbnailWithTransform)。
调整大小后的图片应该至少在一边上是 kIPPhotoMaxEdgeSize 的像素(kCGImageSourceThumbnailMaxPixelSize)。
这个函数应该调整我所指定的图片,而不是重用文件中存在的小图片(kCGImageSourceCreateThumbnailFromImageAlways)。
这样,我就有了一个有效的CGImageRef 对象了。我把它压缩为一个JPEG图片,并保存它:
UIImage *resizedImage = [UIImage imageWithCGImage:thumbnail]; NSData *jpegData = UIImageJPEGRepresentation(resizedImage, 0.8); [jpegData writeToFile:self.filename atomically:YES]; CFRelease(thumbnail);
技巧四:不要依赖UIImage 会释放它的内存数据
前面的三个小技巧使Pholio对iPad的1可靠性产生了巨大的差异,但是,它仍然太容易了在大量的大图像时程序崩溃。
我花了一些时间在 Instruments 中,看看我能否找出发生了什么事。当你的应用收到内存警告时,最重要的事情是查看什么造成了大量脏内存。这个VM Tracker instruments 可以向你展示关于脏内存的信息。我在 Instruments 上发现,在虚拟机中进入多个页面以及导入图像后,我有超过140MB的脏内存,甚至在收到内存警告后! 这个 VM Tracker 告诉我,大部分的脏内存便签是 70. 如果你可以相信互联网, 这种内存来自于内存中加载的图片。。。。有道理,这就是我的程序所做的。
为什么在收到内存警告后会使用这么多图片数据?诚然,每当调用 - [IPPhoto image]时,我都会需要加载UIImage对象,确从来没有明确地卸载UIImage 对象。然而,根据UIImage 的说明文档,“在低内存下,图片数据可以从一个UIImage对象中被清除以释放系统内存。” 所以我预计大图片会从内存中自动清除。
我得出的结论是 UIImage 不能准确地清除图片,因此,我需要手动管理这些图片。我在IPPhoto 中 写了下面的简单方法:
- (void)unloadImage { [image_ release], image_ = nil;}
在Pholio, 只有一个类会以全分辨率显示图片: IPPhotoScrollView。 我做了以下两个小改动:
1、 当IPPhotoScrollView 被告知要显示一个新 IPPhoto 对象时,它会给 旧的 IPPhoto对象发送一个 unloadImage 消息。
2、 在 - [IPPhotoScrollView dealloc ]中方法中,我给当前的IPPhoto 对象 发送一个 unloadImage 消息。
这两个改动意味着当我不再需要显示给用户时,我明确地 卸载了 图片。
结果呢??在模拟器中,这个脏数据下降到了54MB - 这些简单的改动 减少了 60% 的 脏内存。
做了这些改动之后(直接使用Image IO, 确保同一时间不超过一个图片优化,当不再显示时卸载图片),我能够完美地同时在iPad 1 和 iPad 2上处理大图片了。