本文所說的 Resource 是指使用imageWithContentsOfFile:創建圖片的圖片管理方式.
ImageAssets 是指使用imageNamed:創建圖片的圖片管理方式.
如果你對這兩個方法已經了如指掌, 可以直接看UIImage 與 YYImage 的內存問題和后面的內容
[TOC]
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 對象. 減少沙盒的讀取操作.
+ (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 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];
}
UIImage 的內存問題
Resource 的缺點
當我們需要圖片的時候就會去沙盒中讀取這個圖片文件, 轉換成UIImage對象來使用. 現在假設一種場景:
image@2x.png 圖片占用 5kb 的內存
image@2x.png 在多個界面都用到, 且有7處會同時顯示這個圖片
通過代碼分析就可以知道 Resource 這個方式在這個情景下會占用 5kb/個 X 7個 = 35kb 內存. 然而, 在 ImageAssets 方式下, 全部取自字典緩存中的UIImage, 無論有幾處顯示圖片, 都只會占用 5kb/個 X 1個 = 5kb 內存. 此時 Resource 占用內存將會更大.
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:中的強引用字典即可.
PS:轉載自公眾號,作者:ISO大全,公眾號:iOShub。