UIImage 記憶體優化
這篇文章是筆者在開發App過程中發現的一些記憶體問題, 然後學習了YYKit框架時候也發現了圖片的快取處理的<del>不夠得當</del> (YYKit 作者聯絡了我, 說明了YYKit重寫imageNamed:的目的不是為了記憶體管理, 而是增加相容性, 同時也是為了YYKit中的動畫服務). 以下內容是筆者在開發中做了一些實驗以及總結. 如有錯誤望即時提出, 筆者會第一時間改正.
文章的前篇主要是對兩種不同的UIImage
工廠方法的分析, <del>以及對 YYKit 中的 YYImage 的分析</del>. 羅列出這些工廠方法的記憶體管理的優缺點.
文章的後篇是本文要說明的重點, 如何結合兩種工廠方法的優點做更進一步的節約記憶體的管理.
PS
本文所說的 Resource 是指使用
imageWithContentsOfFile:
建立圖片的圖片管理方式.ImageAssets 是指使用
imageNamed:
建立圖片的圖片管理方式.如果你對這兩個方法已經瞭如指掌, 可以直接看UIImage 與 YYImage 的記憶體問題和後面的內容
UIImage 的記憶體處理
在實際的蘋果App開發中, 將圖片檔案匯入到工程中無非使用兩種方式. 一種是 Resource (我也不知道應該稱呼什麼,就這麼叫吧)
,還有一種是 ImageAssets 形式儲存在一個圖片資源管理檔案中. 這兩種方式都可以儲存任何形式的圖片檔案, 但是都有各自的優缺點在內. 接下來我們就來談談這兩種圖片資料管理方式的優缺點.
Resource 與 "imageWithContentsOfFile:"
Resource 的使用方式
將檔案直接拖入到工程目錄下, 並告訴Xcode打包專案時候把這些圖片檔案打包進去. 這樣在應用的".app"資料夾中就有這些圖片. 在專案中, 讀取這些圖片可以通過以下方式來獲取圖片檔案並封裝成UIImge
物件:
NSString *path = [NSBundle.mainBundle pathForResource:@"image@2x" type:@"png"];
UIImage *image = [UIImage imageWithContentsOfFile:path];
而底層的實現原理近似是:
+ (instancetype)imageWithContentsOfFile:(NSString *)fileName {
NSUInteger scale = 0;
{
scale = 2;//這一部分是取 fileName 中"@"符號後面那個數字, 如果不存在則為1, 這一部分的邏輯省略
}
return [[self alloc] initWithData:[NSData dataWithContentsOfFile:fileName scale:scale];
}
這種方式有一個侷限性, 就是圖片檔案必須在.ipa
的根目錄下或者在沙盒中. 在.ipa
的根目錄下建立圖片檔案僅僅只有一種方式, 就是通過 Xcode 把圖片檔案直接拖入工程中. 還有一種情況也會建立圖片檔案, 就是當工程支援低版本的 iOS 系統時, 低版本的iOS系統並不支援 ImageAssets 打包檔案的圖片讀取, 所以 Xcode 在編譯時候會自動地將 ImageAssets 中的圖片複製一份到根目錄中. 此時也可以使用這個方法建立圖片.
Resource 的特性
在 Resource 的圖片管理方式中, 所有的圖片建立都是通過讀取檔案資料得到的, 讀取一次檔案資料就會產生一次NSData
以及產生一個UIImage
, 當圖片建立好後銷燬對應的NSData
, 當UIImage
的引用計數器變為0的時候自動銷燬UIImage
. 這樣的話就可以保證圖片不會長期地存在在記憶體中.
Resource 的常用情景
由於這種方法的特性, 所以 Resource 的方法一般用在圖片資料很大, 圖片一般不需要多次使用的情況. 比如說引導頁背景(圖片全屏, 有時候執行APP會顯示, 有時候根本就用不到).
Resource 的優點
圖片的生命週期可以得到管理無疑是 Resource 最大的優點, 當我們需要圖片的時候就建立一個, 當我們不需要這個圖片的時候就讓他銷燬. 圖片不會長期的儲存在記憶體當中, 所以不會有很多的記憶體浪費. 同時, 大圖一般不會長期使用, 而且大圖佔用記憶體一般比小圖多了好多倍, 所以在減少大圖的記憶體佔用中, Resource 做的非常好.
ImageAssets 與 "imageNamed:"
ImageAssets 的設計初衷主要是為了自動適配 Retina 螢幕和非 Retina 螢幕, 也就是解決 iPhone 4 和 iPhone 3GS 以及以前機型的螢幕適配問題. 現在 iPhone 3GS 以及之前的機型都已被淘汰, 非 Retina 螢幕已不再是開發考慮的範圍. 但是 plus 機型的推出將 Retina 螢幕又提高了一個水平, ImageAssets 現在的主要功能則是區分 plus 螢幕和非 plus 螢幕, 也就是解決 2 倍 Retina 螢幕和 3 倍 Retina 螢幕的視屏問題.
ImageAssets 的使用方式
iOS 開發中一般在工程內匯入兩個到三個同內容不同畫素的圖片檔案, 一般如下:
- image.png (30 x 30)
- image@2x.png (60 x 60)
- image@3x.png (90 x 90)
這三張圖片都是相同內容, 而且圖片名稱的字首相同, 區別在與圖片名以及圖片的解析度. 開發者將這三張圖片拉入 ImageAssets 後, Xcode 會以圖片字首建立一個圖片組(這裡也就是 "image"). 然後在程式碼中寫:
UIImage *image = [UIImage imageNamed:@"image"];
就會根據不同螢幕來獲取對應不同的圖片資料來建立圖片. 如果是 3GS 之前的機型就會讀取 "image.png", 普通 Retina 會讀取 "image@2x.png", plus Retina 會讀取 "image@3x.png", 如果某一個檔案不存在, 就會用另一個解析度的圖片代替之.
ImageAssets 的特性
與 Resources 相似, ImageAssets 也是從圖片檔案中讀取圖片資料轉為 UIImage, 只不過這些圖片資料都打包在 ImageAssets 中. 還有一個最大的區別就是圖片快取. 相當於有一個字典, key 是圖片名, value是圖片物件. 呼叫imageNamed:
方法時候先從這個字典裡取, 如果取到就直接返回, 如果取不到再去檔案中建立, 然後儲存到這個字典後再返回. 由於字典的key
和value
都是強引用, 所以一旦建立後的圖片永不銷燬.
其內部程式碼相似於:
+ (NSMutableDictionary *)imageBuff {
static NSMutableDictionary *_imageBuff;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_imageBuff = [[NSMutableDictionary alloc] init];
});
return _imageBuff;
}
+ (instancetype)imageNamed:(NSString *)imageName {
if (!imageName) {
return nil;
}
UIImage *image = self.imageBuff[imageName];
if (image) {
return image;
}
NSString *path = @"this is the image path"//這段邏輯忽略
image = [self imageWithContentsOfFile:path];
if (image) {
self.imageBuff[imageName] = image;
}
return image;
}
ImageAssets 的使用場景
ImageAssets 最主要的使用場景就是 icon 類的圖片, 一般 icon 類的圖片大小在 3kb 到 20 kb 不等, 都是一些小檔案.
ImageAssets 的優點
當一個 icon 在多個地方需要被顯示的時候, 其對應的UIImage
物件只會被建立一次, 而且多個地方的 icon 都將會共用一個 UIImage
物件. 減少沙盒的讀取操作.
<del>YYImage 的記憶體處理</del>
由於YYImage的目的並不是為了關閉快取, 所以此段沒有分析的意義, 現已刪除.
<del>YYImage 的核心就是學習imageWithContentsOfFile:
的方法原理去實現imageNamed:
方法. 達到imageNamed:
方法中沒有快取功能, 最終使得不需要圖片的時候即可銷燬圖片物件. </del>
<del>imageWithContentsOfFile 代替 imageNamed</del>
<del>首先看 YYImage 的程式碼:</del>
+ (YYImage *)imageNamed:(NSString *)name {
if (name.length == 0) return nil;
if ([name hasSuffix:@"/"]) return nil;
NSString *res = name.stringByDeletingPathExtension;
NSString *ext = name.pathExtension;
NSString *path = nil;
CGFloat scale = 1;
// If no extension, guess by system supported (same as UIImage).
NSArray *exts = ext.length > 0 ? @[ext] : @[@"", @"png", @"jpeg", @"jpg", @"gif", @"webp", @"apng"];
NSArray *scales = [NSBundle preferredScales];
for (int s = 0; s < scales.count; s++) {
scale = ((NSNumber *)scales[s]).floatValue;
NSString *scaledName = [res stringByAppendingNameScale:scale];
for (NSString *e in exts) {
path = [[NSBundle mainBundle] pathForResource:scaledName ofType:e];
if (path) break;
}
if (path) break;
}
if (path.length == 0) return nil;
NSData *data = [NSData dataWithContentsOfFile:path];
if (data.length == 0) return nil;
return [[self alloc] initWithData:data scale:scale];
}
<del>從程式碼可以看出 [YYImage imageNamed:]
這個方法底層是利用通過一定的計算獲取到最佳尺寸, 然後列舉圖片匹配圖片檔名, 拼接成路徑後利用NSData
建立出UIImage
. 本質上和imageWithContentsOfFile:
沒有啥區別.</del>
UIImage <del>與 YYImage</del> 的記憶體問題
Resource 的缺點
當我們需要圖片的時候就會去沙盒中讀取這個圖片檔案, 轉換成UIImage
物件來使用. 現在假設一種場景:
- image@2x.png 圖片佔用 5kb 的記憶體
- image@2x.png 在多個介面都用到, 且有7處會同時顯示這個圖片
通過程式碼分析就可以知道 Resource 這個方式在這個情景下會佔用 5kb/個 X 7個 = 35kb 記憶體. 然而, 在 ImageAssets 方式下, 全部取自字典快取中的UIImage
, 無論有幾處顯示圖片, 都只會佔用 5kb/個 X 1個 = 5kb 記憶體. 此時 Resource 佔用記憶體將會更大.
<del>由於 YYImage 的核心就是利用imageWithContentsOfFile:
代替imageNamed:
, 所以這也是 YYImage 的缺陷之處</del>
ImageAssets 的缺點
第一次讀取的圖片儲存到緩衝區, 然後永不銷燬. 如果這個圖片過大, 佔用幾百 kb, 這一塊的記憶體將不會釋放, 必然導致記憶體的浪費, 而且這個浪費的週期與APP的生命週期同步.
解決方案
為了解決 Resource 的多圖共存問題, 可以學習 ImageAssets 中的字典來形成鍵值對, 當字典中name
對應的image
存在就不建立, 如果不存在就建立. 字典的存在必然導致 UIImage 永不銷燬, 所以還要考慮字典不會影響到 UIImage 的自動銷燬問題. 由此可以做出如下總結:
- 需要一個字典儲存已經建立的 Image 的 name-image 對映
- 當除了這個字典外, 沒有別的物件持有 image, 則從這個字典中刪除對應 name-image 對映
第一個要求的實現方式很簡單, 接下來探討第二個要求.
首先可以考慮如何判斷除了字典外沒有別的物件持有 image? 字典是強引用 key 和 value 的, 當 image 放入字典的時候, image 的引用計數器就會 + 1. 我們可以判斷字典中的 image 的引用計數器是否為 1, 如果為 1 則可以判斷出目前只有字典持有這個 image, 因此可以從這個字典裡刪除這個 image.
這樣即可提出一個方案 MRC+字典
我們還可以換一種思想, 字典是強引用容器, 字典存在必然導致內部value的引用計數器大於等於1. 如果字典是一個弱引用容器, 字典的存在並不會影響到內部value的引用計數器, 那麼 image 的銷燬就不會因為字典而受到影響.
於是又有一個方案 弱引用字典
接下來對這兩個方案作深入的分析和實現:
方案一之 MRC+字典
該方案具體思路是: 找到一個合適的時機, 遍歷所有 value 的 引用計數器, 當某個 value 的引用計數器為 1 時候(說明只有字典持有這個image), 則刪除這個key-value對.
第一步, 在ARC下獲取某個物件的引用計數器:
首先 ARC 下是不允許使用retainCount
這個屬性的, 但是由於 ARC 的原理是編譯器自動為我們管理引用計數器, 所以就算是 ARC 環境下, 引用計數器也是 Enable 狀態, 並且仍然是利用引用計數器來管理記憶體. 所以我們可以使用 KVC 來獲取引用計數器:
@implementation NSObject (MRC)
// 無法直接重寫 retainCount 的方法, 所以加了一個字首
- (NSUInteger)obj_retainCount {
return [[self valueForKey:@"retainCount"] unsignedLongValue];
}
@end
第二步 遍歷 value的引用計數器
// 由於遍歷鍵值對時候不能做新增和刪除操作, 所以把要刪除的key放到一個陣列中
NSMutableArray *keyArr = [NSMutableArray array];
[self.imageDic enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, NSObject * _Nonnull obj, BOOL * _Nonnull stop) {
NSInteger count = obj.obj_retainCount;
if(count == 2) {// 字典持有 + obj引數持有 = 2
[keyArr addObject:key];
}
}];
[keyArr enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[self.imageDic removeObjectForKey:obj];
}];
然後處理遍歷時機. 選擇遍歷時機是一個很困難的, 不能因為遍歷而大量佔有系統資源. 可以在每一次通過 name 建立(或者從字典中獲取)時候遍歷一次, 但這個方法有可能會長時間不呼叫(比如一個使用者在某一個介面上呆很久). 所以我們可以在每一次 runloop 到來時候來做一次遍歷, 同時我們還需要標記遍歷狀態, 防止第二次 runloop 到來時候第一次的遍歷還沒結束就開始新的遍歷了(此時應該直接放棄第二次遍歷).程式碼如下:
CFRunLoopObserverRef oberver= CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
if (activity == kCFRunLoopBeforeWaiting) {
static enuming = NO;
if (!enuming) {
enuming = YES;
// 這裡是遍歷程式碼
enuming = NO;
}
}
});
CFRunLoopAddObserver(CFRunLoopGetMain(), oberver, kCFRunLoopCommonModes);
具體實現請看程式碼.
方案二之 弱引用字典
在上面那個方案中, 會在每一次 runloop 到來之時開闢一個執行緒去遍歷鍵值對. 通常來說, 每一個 APP 建立的圖片個數很大, 所以遍歷鍵值對雖然不會阻塞主執行緒, 但仍然是一個非常耗時耗資源的工作.
弱引用容器是指基於NSArray
, NSDictionary
, NSSet
的容器類, 該容器與這些類最大的區別在於, 將物件放入容器中並不會改變物件的引用計數器, 同時容器是以一個弱引用指標指向這個物件, 當物件銷燬時自動從容器中刪除, 無需額外的操作.
目前常用的弱引用容器的實現方式是block封裝解封
利用block封裝一個物件, 且block中物件的持有操作是一個弱引用指標. 而後將block當做物件放入容器中. 容器直接持有block, 而不直接持有物件. 取物件時解包block即可得到對應物件.
第一步 封裝與解封
typedef id (^WeakReference)(void);
WeakReference makeWeakReference(id object) {
__weak id weakref = object;
return ^{
return weakref;
};
}
id weakReferenceNonretainedObjectValue(WeakReference ref) {
return ref ? ref() : nil;
}
第二步 改造原容器
- (void)weak_setObject:(id)anObject forKey:(NSString *)aKey {
[self setObject:makeWeakReference(anObject) forKey:aKey];
}
- (void)weak_setObjectWithDictionary:(NSDictionary *)dic {
for (NSString *key in dic.allKeys) {
[self setObject:makeWeakReference(dic[key]) forKey:key];
}
}
- (id)weak_getObjectForKey:(NSString *)key {
return weakReferenceNonretainedObjectValue(self[key]);
}
這樣就實現了一個弱引用字典, 之後用弱引用字典代替imageNamed:
中的強引用字典即可.
相關文章
- Android效能優化篇之記憶體優化--記憶體洩漏Android優化記憶體
- Android記憶體優化Android記憶體優化
- 關於redis記憶體分析,記憶體優化Redis記憶體優化
- Android 效能優化之記憶體優化Android優化記憶體
- 記憶體優化相關記憶體優化
- Android Note - 記憶體優化Android記憶體優化
- 1.記憶體優化(一)記憶體洩漏記憶體優化
- 實踐App記憶體優化:如何有序地做記憶體分析與優化APP記憶體優化
- Android記憶體優化之圖片優化Android記憶體優化
- JNI記憶體管理及優化記憶體優化
- mariadb 記憶體佔用優化記憶體優化
- iOS圖片記憶體優化iOS記憶體優化
- App記憶體優化-實踐APP記憶體優化
- 淺談Android記憶體優化Android記憶體優化
- Android記憶體優化全解析Android記憶體優化
- Redis-記憶體優化(一)Redis記憶體優化
- win10怎麼優化記憶體 win10系統記憶體優化的方法Win10優化記憶體
- 2.記憶體優化(二)優化分析記憶體優化
- Redis 記憶體優化神技,小記憶體儲存大資料Redis記憶體優化大資料
- iOS 使用Instruments優化記憶體效能iOS優化記憶體
- Linux 效能優化之 記憶體 篇Linux優化記憶體
- MongoDB記憶體使用分析和優化MongoDB記憶體優化
- HBase記憶體配置及JVM優化記憶體JVM優化
- win10虛擬記憶體如何優化_win10怎麼優化虛擬記憶體Win10記憶體優化
- win10系統如何優化記憶體_win10優化記憶體佔用率怎麼操作Win10優化記憶體
- android效能評測與優化-記憶體Android優化記憶體
- [BASIS]SAP記憶體優化配置學習記憶體優化
- android記憶體管理機制與優化Android記憶體優化
- 讀書筆記2-記憶體優化篇筆記記憶體優化
- Android 是如何管理 App 記憶體的 — Android 記憶體優化第二彈AndroidAPP記憶體優化
- Android深度效能優化--記憶體優化(一篇就夠)Android優化記憶體
- 從記憶體洩露、記憶體溢位和堆外記憶體,JVM優化引數配置引數記憶體洩露記憶體溢位JVM優化
- Spark效能優化:診斷記憶體的消耗Spark優化記憶體
- C# 記憶體管理優化暢想----前言C#記憶體優化
- Linux效能優化實戰記憶體篇(五)Linux優化記憶體
- Linux效能優化:記憶體使用情況分析Linux優化記憶體
- Android效能優化,Startalk會話頁GIF記憶體優化實踐Android優化會話記憶體
- Android效能優化:手把手帶你全面實現記憶體優化Android優化記憶體
- 效能優化——記憶體洩漏(1)入門篇優化記憶體