本文翻譯自Brian's Brain的Resize Images on Your iPad
在我的上一篇文章中,我描述了試圖用圖片平鋪的方式來解決在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上處理大圖片了。