11、緩存在AFNetworking中是如何工作的?AFImageCache和NSUrlCache給你答案

如果你是一名使用Mattt Thompson網絡框架AFNetworking的iOS開發者(如果你不是,那還等什麼呢?),也許你對這個框架中的緩存機制很好奇或者疑惑,並想學習如何在自己的app中充分利用這種機制。

AFNetworking實際上使用了兩個獨立的緩存機制:

  • AFImagecache:一個提供圖片內存緩存的類,繼承自NSCache
  • NSURLCache:NSURLConnection's默認的URL緩存機制,用於存儲NSURLResponse對象:一個默認緩存在內存,通過配置可以緩存到磁盤的類。

爲了理解每個緩存系統是如何工作的,我們看一下他們是如何定義的。

AFImageCache是如何工作的

AFImageCacheUIImageView+AFNetworking分類的一部分。它繼承自NSCache,通過一個URL字符串作爲它的key(從NSURLRequest中獲取)來存儲UIImage對象。

AFImageCache定義:

@interface AFImageCache : NSCache <AFImageCache>

// singleton instantiation :

+ (id <AFImageCache>)sharedImageCache {
    static AFImageCache *_af_defaultImageCache = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _af_defaultImageCache = [[AFImageCache alloc] init];

// clears out cache on memory warning :

    [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * __unused notification) {
        [_af_defaultImageCache removeAllObjects];
    }];
});

// key from [[NSURLRequest URL] absoluteString] :

static inline NSString * AFImageCacheKeyFromURLRequest(NSURLRequest *request) {
    return [[request URL] absoluteString];
}

@implementation AFImageCache

// write to cache if proper policy on NSURLRequest :

- (UIImage *)cachedImageForRequest:(NSURLRequest *)request {
    switch ([request cachePolicy]) {
        case NSURLRequestReloadIgnoringCacheData:
        case NSURLRequestReloadIgnoringLocalAndRemoteCacheData:
            return nil;
        default:
            break;
    }

    return [self objectForKey:AFImageCacheKeyFromURLRequest(request)];
}

// read from cache :

- (void)cacheImage:(UIImage *)image
        forRequest:(NSURLRequest *)request {
    if (image && request) {
        [self setObject:image forKey:AFImageCacheKeyFromURLRequest(request)];
    }
}

AFImageCache 從 AFNetworking 2.1開始可以進行配置了。有一個公共方法setSharedImageCache。詳細文檔可以看這裏 。它把所有可訪問的UIImage對象存到了NSCache。當UIImage對象釋放之後NSCache會進行處理。如果你想觀察images什麼時候釋放,可以實現NSCacheDelegatecache:willEvictObject方法

NSURLCache如何工作

默認是可以的,但最好還是手動配置一下

既然AFNetworking使用NSURLConnection,它利用了原生的緩存機制NSURLCacheNSURLCache緩存了從服務器返回的NSURLResponse對象。

NSURLCacheshareCache方法默認是可以使用的,緩存獲取的內容。不幸的是,它的默認配置只是緩存在內存並沒有寫到硬盤。爲了解決這個問題,你可以聲明一個 sharedCache,像這樣:

1
2
3
4
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:2 * 1024 * 1024
                                              diskCapacity:100 * 1024 * 1024
                                              diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];

這樣,我們聲明瞭一個2M內存,100M磁盤空間的NSURLCache

對NSURLRequest對象設置緩存策略

NSURLCache對每個NSURLRequest對象都會遵守緩存策略(NSURLRequestCachePolicy)。策略定義如下:

  • NSURLRequestUseProtocolCachePolicy:指定定義在協議實現裏的緩存邏輯被用於URL請求。這是URL請求的默認策略
  • NSURLRequestReloadIgnoringLocalCacheData:忽略本地緩存,從源加載
  • NSURLRequestReloadIgnoringLocalAndRemoteCacheData:忽略本地&服務器緩存,從源加載
  • NSURLRequestReturnCacheDataElseLoad:先從緩存加載,如果沒有緩存,從源加載
  • NSURLRequestReturnCacheDataDontLoad離線模式,加載緩存數據(無論是否過期),不從源加載
  • NSURLRequestReloadRevalidatingCacheData存在的緩存數據先確認有效性,無效的話從源加載
用NSURLCache緩存到磁盤
Cache-Control HTTP Header

Cache-Control或者Expires header 必須在從服務器返回的 HTTP response header 中,用於客戶端的緩存(Cache-Control header 優先權高於 Expires header)。這裏邊有很多需要注意的地方,Cache Control可以有被定義爲 max-age的參數(在更新響應之前緩存多長時間),public/private 訪問,或者 no-cache(不緩存響應數據),這裏有一個關於HTTP cache headers的文章。

Subclass NSURLCache for Ultimate Control

如果你想繞過 Cache-Control 需求,定義你自己的規則來讀寫一個帶有 NSURLResponse對象的NSURLCache,你可以繼承 NSURLCache

這裏有個例子,使用 CACHE_EXPIRES 來判斷在獲取源數據之前對緩存數據保留多長時間。

(感謝 Mattt Thompson反饋)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@interface CustomURLCache : NSURLCache

static NSString * const CustomURLCacheExpirationKey = @"CustomURLCacheExpiration";
static NSTimeInterval const CustomURLCacheExpirationInterval = 600;

@implementation CustomURLCache

+ (instancetype)standardURLCache {
    static CustomURLCache *_standardURLCache = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _standardURLCache = [[CustomURLCache alloc]
                                 initWithMemoryCapacity:(2 * 1024 * 1024)
                                 diskCapacity:(100 * 1024 * 1024)
                                 diskPath:nil];
    }

    return _standardURLCache;
}

#pragma mark - NSURLCache

- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
    NSCachedURLResponse *cachedResponse = [super cachedResponseForRequest:request];

    if (cachedResponse) {
        NSDate* cacheDate = cachedResponse.userInfo[CustomURLCacheExpirationKey];
        NSDate* cacheExpirationDate = [cacheDate dateByAddingTimeInterval:CustomURLCacheExpirationInterval];
        if ([cacheExpirationDate compare:[NSDate date]] == NSOrderedAscending) {
            [self removeCachedResponseForRequest:request];
            return nil;
        }
    }
}

    return cachedResponse;
}

- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse
                 forRequest:(NSURLRequest *)request
{
    NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:cachedResponse.userInfo];
    userInfo[CustomURLCacheExpirationKey] = [NSDate date];

    NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:cachedResponse.response data:cachedResponse.data userInfo:userInfo storagePolicy:cachedResponse.storagePolicy];

    [super storeCachedResponse:modifiedCachedResponse forRequest:request];
}

@end

既然你有了自己的 NSURLCache子類,不要忘了在AppDelegate裏邊初始化並使用它

1
2
3
4
CustomURLCache *URLCache = [[CustomURLCache alloc] initWithMemoryCapacity:2 * 1024 * 1024
                                                   diskCapacity:100 * 1024 * 1024
                                                                 diskPath:nil];
[NSURLCache setSharedURLCache:URLCache];
Overriding the NSURLResponse before caching

-connection:willCacheResponse代理方法是在被緩存之前用於截斷和編輯由NSURLConnection創建的NSURLCacheResponse的地方。爲了編輯NSURLCacheResponse,返回一個可變的拷貝,如下(代碼來自NSHipster blog):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection
                  willCacheResponse:(NSCachedURLResponse *)cachedResponse {
    NSMutableDictionary *mutableUserInfo = [[cachedResponse userInfo] mutableCopy];
    NSMutableData *mutableData = [[cachedResponse data] mutableCopy];
    NSURLCacheStoragePolicy storagePolicy = NSURLCacheStorageAllowedInMemoryOnly;

    // ...

    return [[NSCachedURLResponse alloc] initWithResponse:[cachedResponse response]
                                                    data:mutableData
                                                userInfo:mutableUserInfo
                                           storagePolicy:storagePolicy];
}

// If you do not wish to cache the NSURLCachedResponse, just return nil from the delegate function:

- (NSCachedURLResponse *)connection:(NSURLConnection *)connection
                  willCacheResponse:(NSCachedURLResponse *)cachedResponse {
    return nil;
}
Disabling NSURLCache

不想使用 NSURLCache,可以,只需要將內存和磁盤空間容量設爲零就可以了

1
2
3
4
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0
                                              diskCapacity:0
                                              diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];

總結

我寫這篇博客是爲了iOS社區貢獻一份力,總結了一下我在處理關於 AFNetworking緩存相關的問題。我們有個內部App加載了好多圖片,導致內存問題以及性能問題。我主要職責就是診斷這個App的緩存行爲。在這個研究過程中,我在網上搜索了好多資料並且做了好多調試。然後我總結之後寫到了這篇博客中。我希望這篇文章能夠爲其他人用AFNetworking的時候提供幫助,真心希望對你們有用處!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章