在上一個章節主要描述瞭如何實現對GIF圖片的支持,這樣圖片的加載功能就大體完成了。但目前框架只是進行了一些簡單的封裝,還有不少功能還沒有完成。咱們在第一節中,使用了
NSCache
來做爲內存緩存和NSFileManager
來簡單地封裝爲磁盤緩存,如今咱們將對緩存進行重構。git
iOS系統自己就提供了
NSCache
來做爲內存緩存,它是線程安全的,且能保證在內存緊張的狀況下,會自動回收一部份內存。所以,咱們就沒必要再造輪子來實現一個內存緩存了。爲了提升框架的靈活性,咱們能夠提供一個接口來支持外部的擴展。github
@interface JImageManager : NSObject
+ (instancetype)shareManager;
- (void)setMemoryCache:(NSCache *)memoryCache;
@end
複製代碼
磁盤緩存簡單來講就是對文件增刪查改等操做,再複雜點就是可以控制文件保存的時間,以及文件的總大小。緩存
@interface JImageCacheConfig : NSObject
@property (nonatomic, assign) BOOL shouldCacheImagesInMemory; //是否使用內存緩存
@property (nonatomic, assign) BOOL shouldCacheImagesInDisk; //是否使用磁盤緩存
@property (nonatomic, assign) NSInteger maxCacheAge; //文件最大緩存時間
@property (nonatomic, assign) NSInteger maxCacheSize; //文件緩存最大限制
@end
static const NSInteger kDefaultMaxCacheAge = 60 * 60 * 24 * 7;
@implementation JImageCacheConfig
- (instancetype)init {
if (self = [super init]) {
self.shouldCacheImagesInDisk = YES;
self.shouldCacheImagesInMemory = YES;
self.maxCacheAge = kDefaultMaxCacheAge;
self.maxCacheSize = NSIntegerMax;
}
return self;
}
複製代碼
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol JDiskCacheDelegate <NSObject>
- (void)storeImageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key;
- (nullable NSData *)queryImageDataForKey:(nullable NSString *)key;
- (BOOL)removeImageDataForKey:(nullable NSString *)key;
- (BOOL)containImageDataForKey:(nullable NSString *)key;
- (void)clearDiskCache;
@optional
- (void)deleteOldFiles; //後臺更新文件
@end
NS_ASSUME_NONNULL_END
複製代碼
關於磁盤的增刪查改操做這裏就不一一複述了,這裏主要講解如何實現maxCacheAge
和maxCacheSize
屬性安全
maxCacheAge
和maxCacheSize
屬性這兩個屬性是針對文件的保存時間和總文件大小的限制,爲何須要這種限制呢?首先咱們來看
maxCacheSize
屬性,這個很好理解,咱們不可能不斷地擴大磁盤緩存,不然會致使APP佔用大量手機空間,對用戶的體驗很很差。而maxCacheAge
屬性,能夠這麼想,假如一個緩存的文件好久沒有被訪問或修改過,那麼大機率它以後也不會被訪問。所以,咱們也沒有必要去保留它。架構
maxCacheAge
屬性實現該屬性的大體流程:根據設置的存活時間計算出文件可保留的最先時間->遍歷文件,進行時間比對->若文件被訪問的時間早於最先時間,那麼刪除對應的文件app
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskPath isDirectory:YES];
//計算出文件可保留的最先時間
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentAccessDateKey];
//獲取到全部的文件以及文件屬性
NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL includingPropertiesForKeys:resourceKeys options:NSDirectoryEnumerationSkipsHiddenFiles errorHandler:NULL];
NSMutableArray <NSURL *> *deleteURLs = [NSMutableArray array];
for (NSURL *fileURL in fileEnumerator) {
NSError *error;
NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) { //錯誤或不存在文件屬性或爲文件夾的狀況忽略
continue;
}
NSDate *accessDate = resourceValues[NSURLContentAccessDateKey]; //獲取到文件最近被訪問的時間
if ([accessDate earlierDate:expirationDate]) { //若早於可保留的最先時間,則加入刪除列表中
[deleteURLs addObject:fileURL];
}
}
for (NSURL *URL in deleteURLs) {
NSLog(@"delete old file: %@", URL.absoluteString);
[self.fileManager removeItemAtURL:URL error:nil]; //刪除過期的文件
}
複製代碼
maxCacheSize
屬性實現該屬性的流程:遍歷文件計算文件總大小->若文件總大小超過限制的大小,則對文件按被訪問的時間順序進行排序->逐一刪除文件,直到小於總限制的一半爲止。框架
NSMutableDictionary<NSURL *, NSDictionary<NSString *, id>*> *cacheFiles = [NSMutableDictionary dictionary];
NSInteger currentCacheSize = 0;
for (NSURL *fileURL in fileEnumerator) {
NSError *error;
NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
//獲取文件的大小,並保存文件相關屬性
NSNumber *fileSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += fileSize.unsignedIntegerValue;
[cacheFiles setObject:resourceValues forKey:fileURL];
}
if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) { //超過總限制大小
NSUInteger desiredCacheSize = self.maxCacheSize / 2;
NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent usingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {
return [obj1[NSURLContentAccessDateKey] compare:obj2[NSURLContentAccessDateKey]];
}]; //對文件按照被訪問時間的順序來排序
for (NSURL *fileURL in sortedFiles) {
if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
NSNumber *fileSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= fileSize.unsignedIntegerValue;
if (currentCacheSize < desiredCacheSize) { //達到總限制大小的一半便可中止刪除
break;
}
}
}
}
複製代碼
爲何是刪除文件直到總限制大小的一半才中止刪除?因爲訪問和刪除文件是須要消耗必定性能的,若只是達到總限制大小就中止,那麼一旦再存入一小部分文件,就很快達到限制,就必須再執行該操做了。async
如上,咱們能夠看到maxCacheAge
和maxCacheSize
屬性的實現中有不少相同的步驟,好比獲取文件屬性。爲了不重複操做,咱們能夠將二者合併起來實現。函數
- (void)deleteOldFiles {
NSLog(@"start clean up old files");
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskPath isDirectory:YES];
NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentAccessDateKey, NSURLTotalFileAllocatedSizeKey];
NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL includingPropertiesForKeys:resourceKeys options:NSDirectoryEnumerationSkipsHiddenFiles errorHandler:NULL];
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
NSMutableArray <NSURL *> *deleteURLs = [NSMutableArray array];
NSMutableDictionary<NSURL *, NSDictionary<NSString *, id>*> *cacheFiles = [NSMutableDictionary dictionary];
NSInteger currentCacheSize = 0;
for (NSURL *fileURL in fileEnumerator) {
NSError *error;
NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
NSDate *accessDate = resourceValues[NSURLContentAccessDateKey];
if ([accessDate earlierDate:expirationDate]) {
[deleteURLs addObject:fileURL];
continue;
}
NSNumber *fileSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += fileSize.unsignedIntegerValue;
[cacheFiles setObject:resourceValues forKey:fileURL];
}
//刪除過期文件
for (NSURL *URL in deleteURLs) {
NSLog(@"delete old file: %@", URL.absoluteString);
[self.fileManager removeItemAtURL:URL error:nil];
}
//刪除過期文件以後,若仍是超過文件總大小限制,則繼續刪除
if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
NSUInteger desiredCacheSize = self.maxCacheSize / 2;
NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent usingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {
return [obj1[NSURLContentAccessDateKey] compare:obj2[NSURLContentAccessDateKey]];
}];
for (NSURL *fileURL in sortedFiles) {
if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
NSNumber *fileSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= fileSize.unsignedIntegerValue;
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
}
複製代碼
deleteOldFiles
函數,以保證磁盤緩存中的maxCacheAge
和maxCacheSize
由於咱們並不知道什麼時候磁盤總大小會超過限制或緩存的文件過期,假如使用
NSTimer
週期性去檢查,會致使沒必要要的性能消耗,也很差肯定輪詢的時間。爲了不這些問題,咱們能夠考慮在應用進入後臺時,啓動後臺任務去完成檢查和清理工做。oop
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
- (void)onDidEnterBackground:(NSNotification *)notification {
[self backgroundDeleteOldFiles];
}
- (void)backgroundDeleteOldFiles {
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
__block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
//交給後臺去完成
void(^deleteBlock)(void) = ^ {
if ([self.diskCache respondsToSelector:@selector(deleteOldFiles)]) {
[self.diskCache deleteOldFiles];
}
dispatch_async(dispatch_get_main_queue(), ^{
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
});
};
dispatch_async(self.ioQueue, deleteBlock);
}
複製代碼
JImageManager
做爲管理類,暴露相關設置接口,能夠用於外部自定義緩存相關內容;
JImageCache
爲緩存管理類,實際上爲中介者,統一管理緩存配置、內存緩存和磁盤緩存等,並將相關操做交給
NSCache
和
JDiskCache
來完成。
這裏以存儲圖片爲例:
- (void)storeImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key completion:(void (^)(void))completionBlock {
if (!key || key.length == 0 || (!image && !imageData)) {
SAFE_CALL_BLOCK(completionBlock);
return;
}
void(^storeBlock)(void) = ^ {
if (self.cacheConfig.shouldCacheImagesInMemory) {
if (image) {
[self.memoryCache setObject:image forKey:key cost:image.memoryCost];
} else if (imageData) {
UIImage *decodedImage = [[JImageCoder shareCoder] decodeImageWithData:imageData];
[self.memoryCache setObject:decodedImage forKey:key cost:decodedImage.memoryCost];
}
}
if (self.cacheConfig.shouldCacheImagesInDisk) {
if (imageData) {
[self.diskCache storeImageData:imageData forKey:key];
} else if (image) {
NSData *data = [[JImageCoder shareCoder] encodedDataWithImage:image];
if (data) {
[self.diskCache storeImageData:data forKey:key];
}
}
}
SAFE_CALL_BLOCK(completionBlock);
};
dispatch_async(self.ioQueue, storeBlock);
}
複製代碼
這裏定義了一個關於block的宏,爲了不參數傳遞的block
爲nil
,須要在使用前對block
進行判斷是否爲nil
#define SAFE_CALL_BLOCK(blockFunc, ...) \
if (blockFunc) { \
blockFunc(__VA_ARGS__); \
}
複製代碼
在第二章節中講解了NSData
轉換爲image
的實現,考慮到一種狀況,若參數中的imageData
爲空,但image
中包含數據,那麼咱們也應該將image
存儲下來。若要將數據存儲到磁盤中,這就須要咱們將image
轉換爲NSData
了。
image
轉換爲NSData
對於PNG或JPEG格式的圖片,處理起來比較簡單,咱們能夠分別調用
UIImagePNGRepresentation
和UIImageJPEGRepresentation
便可轉換爲NSData
。
因爲拍攝角度和拍攝設備的不一樣,若是不對圖片進行角度處理,那麼頗有可能出現圖片倒過來或側過來的狀況。爲了不這一狀況,那麼咱們在對圖片存儲時須要將圖片「擺正」,而後再存儲。具體相關能夠看這裏
- (UIImage *)normalizedImage {
if (self.imageOrientation == UIImageOrientationUp) { //圖片方向是正確的
return self;
}
UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale);
[self drawInRect:CGRectMake(0, 0, self.size.width, self.size.height)];
UIImage *normalizedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return normalizedImage;
}
複製代碼
如上所示,當圖片方向不正確是,利用drawInRect
方法對圖像進行從新繪製,這樣能夠保證繪製以後的圖片方向是正確的。
- (NSData *)encodedDataWithImage:(UIImage *)image {
if (!image) {
return nil;
}
switch (image.imageFormat) {
case JImageFormatPNG:
case JImageFormatJPEG:
return [self encodedDataWithImage:image imageFormat:image.imageFormat];
case JImageFormatGIF:{
return [self encodedGIFDataWithImage:image];
}
case JImageFormatUndefined:{
if (JCGImageRefContainsAlpha(image.CGImage)) {
return [self encodedDataWithImage:image imageFormat:JImageFormatPNG];
} else {
return [self encodedDataWithImage:image imageFormat:JImageFormatJPEG];
}
}
}
}
//對PNG和JPEG格式圖片的處理
- (nullable NSData *)encodedDataWithImage:(UIImage *)image imageFormat:(JImageFormat)imageFormat {
UIImage *fixedImage = [image normalizedImage];
if (imageFormat == JImageFormatPNG) {
return UIImagePNGRepresentation(fixedImage);
} else {
return UIImageJPEGRepresentation(fixedImage, 1.0);
}
}
複製代碼
如上所示,對PNG和JPEG圖片的處理都比較簡單。如今主要來說解下如何將GIF圖片轉換爲NSData
類型存儲到磁盤中。咱們先回顧下GIF圖片中NSData
如何轉換爲image
:
NSInteger loopCount = 0;
CFDictionaryRef properties = CGImageSourceCopyProperties(source, NULL);
if (properties) { //獲取loopcount
CFDictionaryRef gif = CFDictionaryGetValue(properties, kCGImagePropertyGIFDictionary);
if (gif) {
CFTypeRef loop = CFDictionaryGetValue(gif, kCGImagePropertyGIFLoopCount);
if (loop) {
CFNumberGetValue(loop, kCFNumberNSIntegerType, &loopCount);
}
}
CFRelease(properties);
}
NSMutableArray<NSNumber *> *delayTimeArray = [NSMutableArray array]; //存儲每張圖片對應的展現時間
NSMutableArray<UIImage *> *imageArray = [NSMutableArray array]; //存儲圖片
NSTimeInterval duration = 0;
for (size_t i = 0; i < count; i ++) {
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, i, NULL);
if (!imageRef) {
continue;
}
//獲取圖片
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
[imageArray addObject:image];
CGImageRelease(imageRef);
//獲取delayTime
float delayTime = kJAnimatedImageDefaultDelayTimeInterval;
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(source, i, NULL);
if (properties) {
CFDictionaryRef gif = CFDictionaryGetValue(properties, kCGImagePropertyGIFDictionary);
if (gif) {
CFTypeRef value = CFDictionaryGetValue(gif, kCGImagePropertyGIFUnclampedDelayTime);
if (!value) {
value = CFDictionaryGetValue(gif, kCGImagePropertyGIFDelayTime);
}
if (value) {
CFNumberGetValue(value, kCFNumberFloatType, &delayTime);
if (delayTime < ((float)kJAnimatedImageDelayTimeIntervalMinimum - FLT_EPSILON)) {
delayTime = kJAnimatedImageDefaultDelayTimeInterval;
}
}
}
CFRelease(properties);
}
duration += delayTime;
[delayTimeArray addObject:@(delayTime)];
}
複製代碼
咱們能夠看到,NSData
轉換爲image
主要是獲取loopCount、images
和delaytimes
,那麼咱們從image
轉換爲NSData
,即反過來,將這些屬性寫入到數據裏便可。
- (nullable NSData *)encodedGIFDataWithImage:(UIImage *)image {
NSMutableData *gifData = [NSMutableData data];
CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)gifData, kUTTypeGIF, image.images.count, NULL);
if (!imageDestination) {
return nil;
}
if (image.images.count == 0) {
CGImageDestinationAddImage(imageDestination, image.CGImage, nil);
} else {
NSUInteger loopCount = image.loopCount;
NSDictionary *gifProperties = @{(__bridge NSString *)kCGImagePropertyGIFDictionary : @{(__bridge NSString *)kCGImagePropertyGIFLoopCount : @(loopCount)}};
CGImageDestinationSetProperties(imageDestination, (__bridge CFDictionaryRef)gifProperties);//寫入loopCount
size_t count = MIN(image.images.count, image.delayTimes.count);
for (size_t i = 0; i < count; i ++) {
NSDictionary *properties = @{(__bridge NSString *)kCGImagePropertyGIFDictionary : @{(__bridge NSString *)kCGImagePropertyGIFDelayTime : image.images[i]}};
CGImageDestinationAddImage(imageDestination, image.images[i].CGImage, (__bridge CFDictionaryRef)properties); //寫入images和delaytimes
}
}
if (CGImageDestinationFinalize(imageDestination) == NO) {
gifData = nil;
}
CFRelease(imageDestination);
return [gifData copy];
}
複製代碼
本章節主要對緩存進行了重構,使其功能更完善,易擴展,另外還補充講解了對GIF圖片的存儲。