本文翻譯自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上處理大圖片了。