AFNetworking和YTKNetwork的緩存策略

Untold numbers of developers have hacked together an awkward, fragile system for network caching functionality, all because they weren’t aware that NSURLCache could be setup in two lines and do it 100× better. Even more developers have never known the benefits of network caching, and never attempted a solution, causing their apps to make untold numbers of unnecessary requests to the server. 無數開發者嘗試本身作一個醜陋而脆弱的系統來實現網絡緩存的功能,卻不知NSURLCache只要兩行代碼就能搞定,而且好上100倍。甚至更多的開發者根本不知道網絡緩存的好處,歷來沒有嘗試過解決方案,致使他們的App向服務器發出無數沒必要要的請求。ios

iOS系統的緩存策略

    上面是引用Mattt大神在NSHipster介紹NSURLCache時的原話。git

服務端的緩存策略

    先看看服務端的緩存策略。當第一次請求後,客戶端會緩存數據,當有第二次請求的時候,客戶端會額外在請求頭加上If-Modified-Since或者If-None-MatchIf-Modified-Since會攜帶緩存的最後修改時間,服務端會把這個時間和實際文件的最後修改時間進行比較。github

  • 相同就返回狀態碼304,且不返回數據,客戶端拿出緩存數據,渲染頁面
  • 不一樣就返回狀態碼200,而且返回數據,客戶端渲染頁面,而且更新緩存

    固然相似的還有Cache-ControlExpiresEtag,都是爲了校驗本地緩存文件和服務端是否一致,這裏就帶過了。數組

NSURLCache

    NSURLCache是iOS系統提供的內存以及磁盤的綜合緩存機制。NSURLCache對象被存儲沙盒中Library/cache目錄下。在咱們只須要在didFinishLaunchingWithOptions函數裏面加上下面的代碼,就能夠知足通常的緩存要求。(是的,搞定NSURLCache就是這麼簡單)緩存

NSURLCache * sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:20 * 1024 *1024 diskCapacity:100 * 1024 * 1024 diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];
複製代碼

    下面是幾個經常使用的API安全

//設置內存緩存的最大容量
[cache setMemoryCapacity:1024 * 1024 * 20];

//設置磁盤緩存的最大容量
[cache setDiskCapacity:1024 * 1024 * 100];

//獲取某個請求的緩存
[cache cachedResponseForRequest:request];

//清除某個請求的緩存
[cache removeCachedResponseForRequest:request];

//請求策略,設置了系統會自動用NSURLCache進行數據緩存
request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
複製代碼

iOS經常使用的緩存策略

    NSURLRequestCachePolicy是個枚舉,指的是不一樣的緩存策略,一共有7種,可是能用的只有4種。bash

typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy)
{
    //若是有協議,對於特定的URL請求,使用協議實現定義的緩存邏輯。(默認的緩存策略)
    NSURLRequestUseProtocolCachePolicy = 0,

    //請求僅從原始資源加載URL,不使用任何緩存
    NSURLRequestReloadIgnoringLocalCacheData = 1,

    //不只忽略本地緩存,還要忽略協議緩存和其餘緩存 (未實現)
    NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4,

    //被NSURLRequestReloadIgnoringLocalCacheData替代
    NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,

    //無視緩存的有效期,有緩存就取緩存,沒有緩存就會從原始地址加載
    NSURLRequestReturnCacheDataElseLoad = 2,

    //無視緩存的有效期,有緩存就取緩存,沒有緩存就視爲失敗 (能夠用於離線模式)
    NSURLRequestReturnCacheDataDontLoad = 3,

    //會從初始地址校驗緩存的合法性,合法就用緩存數據,不合法從原始地址加載數據 (未實現)
    NSURLRequestReloadRevalidatingCacheData = 5, // Unimplemented
};
複製代碼

AFNetworking的緩存策略

    以前寫了SDWebImage的源碼解析 裏面介紹過SDWebImage的緩存策略,有兩條線根據時間和空間來管理緩存和AFNetworking很類似。AFNetworkingAFImageDownloader使用AFAutoPurgingImageCacheNSURLCache管理圖片緩存。服務器

AFNetworking中的NSURLCache

    AFImageDownloader中設置NSURLCache,低版本iOS版本中設置內存容量和磁盤容量會閃退(這個我沒有考證,iOS 7的手機還真沒有)網絡

if ([[[UIDevice currentDevice] systemVersion] compare:@"8.2" options:NSNumericSearch] == NSOrderedAscending) 
{
    return [NSURLCache sharedURLCache];
}
return [[NSURLCache alloc] initWithMemoryCapacity:20 * 1024 * 1024 diskCapacity:150 * 1024 * 1024 diskPath:@"com.alamofire.imagedownloader"];
複製代碼

AFNetworking中的AFAutoPurgingImageCache

    AFAutoPurgingImageCache是專門用來圖片緩存的。能夠看到內部有三個屬性,一個是用來裝載AFImageCache對象的字典容器,一個是能夠用內存空間大小、一個同步隊列。AFAutoPurgingImageCache在初始化的時候,會註冊UIApplicationDidReceiveMemoryWarningNotification通知,收到內存警告的時候會清除全部緩存。多線程

@interface AFAutoPurgingImageCache ()
@property (nonatomic, strong) NSMutableDictionary <NSString* , AFCachedImage*> *cachedImages;
@property (nonatomic, assign) UInt64 currentMemoryUsage;
@property (nonatomic, strong) dispatch_queue_t synchronizationQueue;
@end
複製代碼

    AFCachedImage是單個圖片緩存對象

@property (nonatomic, strong) UIImage *image;

//標誌符(這個值就是圖片的請路徑 request.URL.absoluteString)
@property (nonatomic, strong) NSString *identifier;

//圖片大小
@property (nonatomic, assign) UInt64 totalBytes;

//緩存日期
@property (nonatomic, strong) NSDate *lastAccessDate;

//當前可用內存空間大小
@property (nonatomic, assign) UInt64 currentMemoryUsage;

複製代碼

    來看看AFCachedImage初始化的時候。iOS使用圖標標準是ARGB_8888,即一像素佔位4個字節。內存大小 = 寬×高×每像素字節數。

-(instancetype)initWithImage:(UIImage *)image identifier:(NSString *)identifier 
{
    if (self = [self init]) 
    {
        self.image = image;
        self.identifier = identifier;

        CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
        CGFloat bytesPerPixel = 4.0;
        CGFloat bytesPerSize = imageSize.width * imageSize.height;
        self.totalBytes = (UInt64)bytesPerPixel * (UInt64)bytesPerSize;
        self.lastAccessDate = [NSDate date];
    }
    return self;
}
複製代碼

    來看看添加緩存的代碼,用了dispatch_barrier_async柵欄函數將添加操做和刪除緩存操做分割開來。每添加一個緩存對象,都從新計算當前緩存大小和可用空間大小。當內存超過設定值時,會按照日期的倒序來遍歷緩存圖片,刪除最先日期的緩存,一直到知足緩存空間爲止。

- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier 
{
    dispatch_barrier_async(self.synchronizationQueue, ^{
        AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];

        AFCachedImage *previousCachedImage = self.cachedImages[identifier];
        if (previousCachedImage != nil) 
        {
            self.currentMemoryUsage -= previousCachedImage.totalBytes;
        }

        self.cachedImages[identifier] = cacheImage;
        self.currentMemoryUsage += cacheImage.totalBytes;
    });

    dispatch_barrier_async(self.synchronizationQueue, ^{
        if (self.currentMemoryUsage > self.memoryCapacity) 
        {
            UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;
            NSMutableArray <AFCachedImage*> *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];
            NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate" ascending:YES];
            [sortedImages sortUsingDescriptors:@[sortDescriptor]];

            UInt64 bytesPurged = 0;

            for (AFCachedImage *cachedImage in sortedImages) 
            {
                [self.cachedImages removeObjectForKey:cachedImage.identifier];
                bytesPurged += cachedImage.totalBytes;
                if (bytesPurged >= bytesToPurge) 
                {
                    break ;
                }
            }
            self.currentMemoryUsage -= bytesPurged;
        }
    });
}
複製代碼

YTKNetwork的緩存策略

    YTKNetwork是猿題庫技術團隊開源的一個網絡請求框架,內部封裝了AFNetworking。它把每一個請求實例化,管理它的生命週期,也能夠管理多個請求。筆者在一個電商的PaaS項目中就是使用YTKNetwork,它的特色還有支持請求結果緩存,支持批量請求,支持多請求依賴等。

準備請求以前

    先來看看請求基類YTKRequest在請求以前作了什麼

- (void)start 
{

    //忽略緩存的標誌 手動設置 是否利用緩存
    if (self.ignoreCache) 
    {
        [self startWithoutCache];
        return;
    }

    // 還有未完成的請求 是否還有未完成的請求
    if (self.resumableDownloadPath) 
    {
        [self startWithoutCache];
        return;
    }

    //加載緩存是否成功
    if (![self loadCacheWithError:nil]) 
    {
        [self startWithoutCache];
        return;
    }

    _dataFromCache = YES;

    dispatch_async(dispatch_get_main_queue(), ^{

        //將請求數據寫入文件
        [self requestCompletePreprocessor];
        [self requestCompleteFilter];

        //這個時候直接去相應 請求成功的delegate和block ,沒有發送請求
        YTKRequest *strongSelf = self;
        [strongSelf.delegate requestFinished:strongSelf];
        if (strongSelf.successCompletionBlock) 
        {
            strongSelf.successCompletionBlock(strongSelf);
        }

        //將block置空
        [strongSelf clearCompletionBlock];
    });
}
複製代碼

緩存數據寫入文件

- (void)requestCompletePreprocessor 
{
    [super requestCompletePreprocessor];

    if (self.writeCacheAsynchronously) 
    {
        dispatch_async(ytkrequest_cache_writing_queue(), ^{
            [self saveResponseDataToCacheFile:[super responseData]];
        });
    } 
    else 
    {
        [self saveResponseDataToCacheFile:[super responseData]];
    }
}
複製代碼

    ytkrequest_cache_writing_queue是一個優先級比較低的串行隊列,當標誌dataFromCacheYES的時候,肯定能拿到數據,在這個串行隊列中異步的寫入文件。來看看寫入緩存的具體操做。

- (void)saveResponseDataToCacheFile:(NSData *)data 
{
    if ([self cacheTimeInSeconds] > 0 && ![self isDataFromCache]) 
    {
        if (data != nil) 
        {
            @try {
                // New data will always overwrite old data.
                [data writeToFile:[self cacheFilePath] atomically:YES];

                YTKCacheMetadata *metadata = [[YTKCacheMetadata alloc] init];
                metadata.version = [self cacheVersion];
                metadata.sensitiveDataString = ((NSObject *)[self cacheSensitiveData]).description;
                metadata.stringEncoding = [YTKNetworkUtils stringEncodingWithRequest:self];
                metadata.creationDate = [NSDate date];
                metadata.appVersionString = [YTKNetworkUtils appVersionString];
                [NSKeyedArchiver archiveRootObject:metadata toFile:[self cacheMetadataFilePath]];
            } @catch (NSException *exception) {
                YTKLog(@"Save cache failed, reason = %@", exception.reason);
            }
        }
    }
}
複製代碼

    除了請求數據文件,YTK還會生成一個記錄緩存數據信息的元數據YTKCacheMetadata對象。YTKCacheMetadata記錄了緩存的版本號、敏感信息、緩存日期和App的版本號。

@property (nonatomic, assign) long long version;
@property (nonatomic, strong) NSString *sensitiveDataString;
@property (nonatomic, assign) NSStringEncoding stringEncoding;
@property (nonatomic, strong) NSDate *creationDate;
@property (nonatomic, strong) NSString *appVersionString;
複製代碼

    而後把請求方法、請求域名、請求URL和請求參數組成的字符串進行一次MD5加密,做爲緩存文件的名稱。YTKCacheMetadata和緩存文件同名,多了一個.metadata的後綴做爲區分。文件寫入的路徑是沙盒中Library/LazyRequestCache目錄下。

- (NSString *)cacheFileName 
{
    NSString *requestUrl = [self requestUrl];
    NSString *baseUrl = [YTKNetworkConfig sharedConfig].baseUrl;
    id argument = [self cacheFileNameFilterForRequestArgument:[self requestArgument]];
    NSString *requestInfo = [NSString stringWithFormat:@"Method:%ld Host:%@ Url:%@ Argument:%@",
    (long)[self requestMethod], baseUrl, requestUrl, argument];
    NSString *cacheFileName = [YTKNetworkUtils md5StringFromString:requestInfo];
    return cacheFileName;
}
複製代碼

YTKNetwork緩存文件路徑.png

校驗緩存

    回到start方法中,loadCacheWithError是校驗緩存能不能成功加載出來,loadCacheWithError中會調用validateCacheWithError來檢驗緩存的合法性,校驗的依據正是YTKCacheMetadatacacheTimeInSeconds。要想使用緩存數據,請求實例要重寫cacheTimeInSeconds設置一個大於0的值,並且緩存還支持版本、App的版本。在實際項目上應用,get請求實例設置一個cacheTimeInSeconds就夠用了。

- (BOOL)validateCacheWithError:(NSError * _Nullable __autoreleasing *)error     
{
    // Date
    NSDate *creationDate = self.cacheMetadata.creationDate;
    NSTimeInterval duration = -[creationDate timeIntervalSinceNow];
    if (duration < 0 || duration > [self cacheTimeInSeconds]) 
    {
        if (error) 
        {
            *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorExpired userInfo:@{ NSLocalizedDescriptionKey:@"Cache expired"}];
        }
        return NO;
    }
    // Version
    long long cacheVersionFileContent = self.cacheMetadata.version;
    if (cacheVersionFileContent != [self cacheVersion]) 
    {
        if (error) 
        {
            *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorVersionMismatch userInfo:@{ NSLocalizedDescriptionKey:@"Cache version mismatch"}];
        }
        return NO;
    }
    // Sensitive data
    NSString *sensitiveDataString = self.cacheMetadata.sensitiveDataString;
    NSString *currentSensitiveDataString = ((NSObject *)[self cacheSensitiveData]).description;
    if (sensitiveDataString || currentSensitiveDataString) 
    {
        // If one of the strings is nil, short-circuit evaluation will trigger
        if (sensitiveDataString.length != currentSensitiveDataString.length || ![sensitiveDataString isEqualToString:currentSensitiveDataString]) 
        {
            if (error) 
            {
                *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorSensitiveDataMismatch userInfo:@{ NSLocalizedDescriptionKey:@"Cache sensitive data mismatch"}];
            }
            return NO;
        }
    }
    // App version
    NSString *appVersionString = self.cacheMetadata.appVersionString;
    NSString *currentAppVersionString = [YTKNetworkUtils appVersionString];
    if (appVersionString || currentAppVersionString) 
    {
        if (appVersionString.length != currentAppVersionString.length || ![appVersionString isEqualToString:currentAppVersionString]) 
        {
            if (error) 
            {
                *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorAppVersionMismatch userInfo:@{ NSLocalizedDescriptionKey:@"App version mismatch"}];
            }
            return NO;
        }
    }
    return YES;
}
複製代碼

清除緩存

    由於緩存的目錄是Library/LazyRequestCache,清除緩存就直接清空目錄下全部文件就能夠了。調用[[YTKNetworkConfig sharedConfig] clearCacheDirPathFilter]就行。

結語

    緩存的本質是用空間換取時間。大學裏面學過的《計算機組成原理》中就有介紹cache,除了磁盤和內存,還有L1和L2,對於iOS開發者來講,通常關注diskmemory就夠了。閱讀SDWebImage、AFNetworking、YTKNetwork的源碼後,能夠看出他們都很是重視數據的多線程的讀寫安全,在作深度優化時候,因地制宜,及時清理緩存文件。

相關文章
相關標籤/搜索