IOS開發-APP的內存優化



本文所說的 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:中的強引用字典便可.web

 

PS:轉載自公衆號,做者:ISO大全,公衆號:iOShub。數組

相關文章
相關標籤/搜索