本文翻译自Brian's Brain的Resize Images on Your iPad程序员
在个人上一篇文章中,我描述了试图用图片平铺的方式来解决在ipad上展现“大型图片”的问题的第一次尝试。在这种方式中,你把图片拉伸成不一样的尺寸,而后把每一个图片分割成一张张正方形的片断。经过使用Cocoa框架提供的CATiledlayer类,你能够在不一样的缩放层级下,绘制所须要的图片片断。多线程
可是,在iPad 1上运行时,当我试图为大型图片计算分割的片断时,仍然偶尔会把内存用完。所以,在Pholio2.1版本中,选择了一个更加简单的方法。当用户提供了一个大的图像,我将其缩小到一个可管理的大小。我选择把图片长和宽的像素控制在1500像素之内。这样显示这张图片将须要9MB内存,用户仍然能够放大一点来看到更多的细节。框架
记住,平铺图片的技术须要你屡次调整图片的尺寸,而且在每一个尺寸,你都须要计算和保存图片的片断。这些都须要耗费时间和内存。如今这种方法,不但简单,并且更快。。。我只须要调整和保存图片一次,在UIImageViewController 中显示一个已经被调整大小的图片,而不是使用CATiledLayer显示一张图片的片断,一样也会防止用户在滚动到一个新的图片时产生闪烁。 这种方法的惟一缺点就是:用户不能放大来看清他们图片的实际像素。ide
尽管如此,仍然有些技巧来提升调整图片尺寸的效率,我用Pholio这个应用来告诉你这些技巧吧。函数
在Pholio中,这个方法就是 - [IPPhoto optimize] 。 我将会更加详细地讲解里面的一些细节,但在一个更高的视角,这个方法有下面这些关键的属性:单元测试
同步。 这意味着能够很简单地进行单元测试。 也意味着慢。。。能够更多地关心多线程中的其余问题。测试
防止过多占用内存。这个问题就是在调整大尺寸图片时会消耗大量的内存,个人工做就是确保在这个程序返回时全部可能的内存都被释放了。尽量少地把IPPhoto对象放到任何的自动释放池中。由于调整图片尺寸、释放内存等操做都会占用系统资源。优化
作了全部必要的准备工做,以使图片高效率地显示在iPad上。这意味着,全部调整大图片尺寸,为全部图片生成缩略图的工做都会在- [IPPhoto optimize ] 方法里面完成。ui
关键:我组织代码的其他部分,确保IPPhoto对象延迟加载,仅在我调用以后才在数据模型中构造。这意味着一切须要显示在屏幕上的数据模型都是公平的。atom
通过围绕在调整图片大小的一个同步程序的编写后,能够很容易推理出程序的正确行为。可是由于这个程序须要很长的运行时间,必需要在后台线程中执行,不然影响用户界面的响应。
个人第一个错误是天真地让全部用GCD调度这个程序 - [IPPhoto optimize] 都在后台线程队列中响应。而后,一旦图片在后台完成优化,我会在主线程中把这个图片插入到模型中。
问题?太多的后台优化!由于每次调用 - [IPPhoto optimize] 都会消耗不少内存,这样同一时间超过一个优化都会把iPad 1 弄垮。而这就是我把不一样的优化放到后台队列中发生的事情。不知道还好,原来GCD会同时调度安排工做运行。
为了解决这个问题,我引入了一个新的类, IPPhotoOptimizationManager(.h, .m )。这个优化图片的管理器能够完成下面的任务:
它建立一个单一的NSOperationQueue 来进行全部的内存密集型操做,如调整大小。而后使用 - [NSOperationQueue setMaxConcurrentOperationCount: ] 方法,确保同一时间最多只有一个后台优化。
它定义了一个对一张或多张图片进行优化的队列,并在主线程中调用上面的优化程序来简单地完成工做。
它维护一个全部正在进行和正在等待的优化的计数器。
每当正在进行的优化操做数量改变,都会通知委托,我使用这个提示用户优化正在进行。
经过使用 IPPhotoOptimizationManager 类,Pholio 能够控制后台的优化。
我原始的调整图片尺寸的代码来自[ 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);
前面的三个小技巧使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。 我作了如下两个小改动:
一、 当IPPhotoScrollView 被告知要显示一个新 IPPhoto 对象时,它会给 旧的 IPPhoto对象发送一个 unloadImage 消息。
二、 在 - [IPPhotoScrollView dealloc ]中方法中,我给当前的IPPhoto 对象 发送一个 unloadImage 消息。
这两个改动意味着当我再也不须要显示给用户时,我明确地 卸载了 图片。
结果呢??在模拟器中,这个脏数据降低到了54MB - 这些简单的改动 减小了 60% 的 脏内存。
作了这些改动以后(直接使用Image IO, 确保同一时间不超过一个图片优化,当再也不显示时卸载图片),我可以完美地同时在iPad 1 和 iPad 2上处理大图片了。