iOS----------SDWebimage源码解析(3)

上一篇我们遗留了一些问题:
1、UIImageView+WebCache核心代码中开始停止所有的operation;
2、SDWebImageCombinedOperation类的原因好处以及出现的subOperation的原因 ;
3、SDImageCache缓存类的实现方式 ;
4、SDWebImageDownloader下载类的实现 ;

这一篇我们主要来解决SDImageCache这个类的缓存问题。跟其他类一样,我们先看其.h文件。
SDImageCache.h
(1)枚举

typedef NS_ENUM(NSInteger, SDImageCacheType) {
    SDImageCacheTypeNone,//缓存中没有该image
    SDImageCacheTypeDisk,//硬盘中的image
    SDImageCacheTypeMemory//内存中的image
};

(2)3个blcok

从缓存中查找图片完成的block
typedef void(^SDWebImageQueryCompletedBlock)(UIImage *image, SDImageCacheType cacheType);
图片是否存在于缓存中的block
typedef void(^SDWebImageCheckCacheCompletionBlock)(BOOL isInCache);
缓存中图片大小的block
typedef void(^SDWebImageCalculateSizeBlock)(NSUInteger fileCount, NSUInteger totalSize);

(3)对象方法

//设置硬盘缓存的路径
-(NSString *)makeDiskCachePath:(NSString*)fullNamespace;
//将path路径下的设置为只读
- (void)addReadOnlyCachePath:(NSString *)path;
//根据key存储image
- (void)storeImage:(UIImage *)image forKey:(NSString *)key;
//根据key存储image并设置是否存储在硬盘中
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;
//是否计算。。。
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
核心方法:在缓存中根据key查询图片处理block
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
...其他方法我们之间看名字就能知道它是干什么的,我们介绍核心方法

SDImageCache.m文件中
(1)属性
NSCache *memCache:内存缓存
NSString *diskCachePath:硬盘缓存路径
NSMutableArray *customPaths:路径数组
dispatch_queue_t ioQueue:队列
(2)方法
第一个方法是初始化方法:

- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory {
    if ((self = [super init])) {
        //缓存空间的名字
        NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];

        // initialise PNG signature data
        kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8];

        // Create IO serial queue
        //
        _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);

        // Init default values
        _maxCacheAge = kDefaultCacheMaxCacheAge;

        // Init the memory cache
        //缓存的时间
        _memCache = [[AutoPurgeCache alloc] init];
        _memCache.name = fullNamespace;

        // Init the disk cache
        //初始化硬盘缓存的路径,如果有自定义的路径 之间将名字拼接起来
        if (directory != nil) {
            _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
        } else {
            //没有路径,获取到NSCachesDirectory路径然后拼接
            NSString *path = [self makeDiskCachePath:ns];
            _diskCachePath = path;
        }

        // Set decompression to YES
        //是否进行解压缩
        _shouldDecompressImages = YES;

        // memory cache enabled
        //是否缓存到内存中
        _shouldCacheImagesInMemory = YES;

        // Disable iCloud
        //是否支持云
        _shouldDisableiCloud = YES;

        //_ioQueue队列中同步执行,创建文件管理器
        dispatch_sync(_ioQueue, ^{
            _fileManager = [NSFileManager new];
        });

#if TARGET_OS_IPHONE
        // Subscribe to app events
        //添加一些通知,接收到内存警告
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(clearMemory)
                                                     name:UIApplicationDidReceiveMemoryWarningNotification
                                                   object:nil];
        //进程将要结束的通知
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(cleanDisk)
                                                     name:UIApplicationWillTerminateNotification
                                                   object:nil];
        //程序进入后台的通知
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(backgroundCleanDisk)
                                                     name:UIApplicationDidEnterBackgroundNotification
                                                   object:nil];
#endif
    }
    return self;
}

下面我们来看看几个核心方法
(1)根据key从缓存中查找图片的方法

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    if (!doneBlock) {
        return nil;
    }

    if (!key) {
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }

    // First check the in-memory cache...
    //从内存中取出图片 之间调用block,返回nil
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }

    //采用new的方式只能采用默认的init方法完成初始化,采用alloc的方式可以用其他定制的初始化方法
    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            return;
        }

        @autoreleasepool {
            //从硬盘中取出图片
            UIImage *diskImage = [self diskImageForKey:key];
            if (diskImage && self.shouldCacheImagesInMemory) {
                //如果从硬盘缓存中获取到图片,并且需要缓存在内存中 那么进行内存缓存
                NSUInteger cost = SDCacheCostForImage(diskImage);
                // cost 被用来计算缓存中所有对象的代价。当内存受限或者所有缓存对象的总代价超过了最大允许的值时,缓存会移除其中的一些对象。
                // 通常,精确的 cost 应该是对象占用的字节数。
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
            //异步函数中调用block
            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });

        //这里主要是从缓存中取出图片的同时创建了NSOperation对象并没有进行其他操作
    return operation;
}

我们来重点看下从硬盘中取出图片的方法
- (UIImage )diskImageForKey:(NSString )key

- (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key {
    //根据图片的key获取到全路径  这个路径是加密路径  从系统定义的路径中添加
    NSString *defaultPath = [self defaultCachePathForKey:key];
    //转为NSData
    NSData *data = [NSData dataWithContentsOfFile:defaultPath];
    if (data) {
        return data;
    }

    // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
    // checking the key with and without the extension
    data = [NSData dataWithContentsOfFile:[defaultPath stringByDeletingPathExtension]];
    if (data) {
        return data;
    }
    //如果没有就从路径的数组中便利所有路径取出
    NSArray *customPaths = [self.customPaths copy];
    for (NSString *path in customPaths) {
        NSString *filePath = [self cachePathForKey:key inPath:path];
        NSData *imageData = [NSData dataWithContentsOfFile:filePath];
        if (imageData) {
            return imageData;
        }
        // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
        // checking the key with and without the extension
        imageData = [NSData dataWithContentsOfFile:[filePath stringByDeletingPathExtension]];
        if (imageData) {
            return imageData;
        }
    }
    return nil;
}

这部分中获取文件名字的时候SDWebimage会根据key来加密(MD5)创建文件名,代码如下

- (NSString *)cachedFileNameForKey:(NSString *)key {
    const char *str = [key UTF8String];
    if (str == NULL) {
        str = "";
    }
    // 使用了MD5进行加密处理
    // 开辟一个16字节(128位:md5加密出来就是128bit)的空间
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
                          r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
                          r[11], r[12], r[13], r[14], r[15], [[key pathExtension] isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]];
    return filename;
}

小总结:这里是根据图片的key从缓存中取出图片,先从内存中取出,能取到直接调用block,如果取不到就从硬盘中取,能取到就将图片保存在内存中,调用block。

(2)保存图片。调用的核心方法

- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk 
{
        if (!image || !key) {
        return;
    }
    //内存缓存
    // if memory cache is enabled
    if (self.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(image);
        [self.memCache setObject:image forKey:key cost:cost];
    }
        //需要进行硬盘缓存 前往硬盘缓存
    if (toDisk) {
        dispatch_async(self.ioQueue, ^{
            //ioQueue,我们从字面上理解,就是一个磁盘io的dispatch_queue_t。说简单点,就是每个下载来的图片,需要进行磁盘io的过程都放在ioQueue中执行

            NSData *data = imageData;
            if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
                // We need to determine if the image is a PNG or a JPEG
                // PNGs are easier to detect because they have a unique signature (http://www.w3.org/TR/PNG-Structure.html)
                // The first eight bytes of a PNG file always contain the following (decimal) values:
                // 137 80 78 71 13 10 26 10

                // If the imageData is nil (i.e. if trying to save a UIImage directly or the image was transformed on download)
                // and the image has an alpha channel, we will consider it PNG to avoid losing the transparency

                // 我们需要判断image是PNG还是JPEG
                // PNG的图片很容易检测出来,因为它们有一个特定的标示 (http://www.w3.org/TR/PNG-Structure.html)
                // PNG图片的前8个字节不许符合下面这些值(十进制表示)
                // 137 80 78 71 13 10 26 10
                //0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A

                // 如果imageData为空(举个例子,比如image在下载后需要transform,那么就imageData就会为空)
                // 并且image有一个alpha通道, 我们将该image看做PNG以避免透明度(alpha)的丢失(因为JPEG没有透明色)
                int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
                BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                                  alphaInfo == kCGImageAlphaNoneSkipFirst ||
                                  alphaInfo == kCGImageAlphaNoneSkipLast);
                BOOL imageIsPng = hasAlpha;

                // But if we have an image data, we will look at the preffix
                if ([imageData length] >= [kPNGSignatureData length]) {
                    //判断是否是png图片
                    imageIsPng = ImageDataHasPNGPreffix(imageData);
                }

                if (imageIsPng) {
                    data = UIImagePNGRepresentation(image);
                }
                else {
                    data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
                }
#else
                // 当然,如果不是在iPhone平台上,就使用下面这个方法。不过不在我们研究范围之内
                data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
            }

            if (data) {
                // 首先判断disk cache的文件路径是否存在,不存在的话就创建一个
                // disk cache的文件路径是存储在_diskCachePath中的
                if (![_fileManager fileExistsAtPath:_diskCachePath]) {
                    [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
                }

                // get cache Path for image key  url路径
                NSString *cachePathForKey = [self defaultCachePathForKey:key];
                // transform to NSUrl
                NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];

                // 根据存储的路径(cachePathForKey)和存储的数据(data)将其存放到iOS的文件系统
                [_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];

                // disable iCloud backup
                // 如果不使用iCloud进行备份,就使用NSURLIsExcludedFromBackupKey
                if (self.shouldDisableiCloud) {
                    [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
                }
            }
        });
    }
}

以上我们解决了两个核心的方法,一个是通过key在缓存中查询,一个是将图片保存在缓存中。
有了缓存我们当然要清除缓存,SDWebimage中也提供了清除缓存的方法,清除修改时间最早的file,代码已经奉上。

// 实现了一个简单的缓存清除策略:清除修改时间最早的file
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {

    dispatch_async(self.ioQueue, ^{
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
        NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

        // This enumerator prefetches useful properties for our cache files.
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];

        // 获取文件的过期时间,SDWebImage中默认是一个星期
        // 不过这里虽然称*expirationDate为过期时间,但是实质上并不是这样。
        // 其实是这样的,比如在2015/12/12/00:00:00最后一次修改文件,对应的过期时间应该是
        // 2015/12/19/00:00:00,不过现在时间是2015/12/27/00:00:00,我先将当前时间减去1个星期,得到
        // 2015/12/20/00:00:00,这个时间才是我们函数中的expirationDate。
        // 用这个expirationDate和最后一次修改时间modificationDate比较看谁更晚就行。
        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];

        // 用来存储对应文件的一些属性,比如文件所需磁盘空间
        NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];

        // 记录当前已经使用的磁盘缓存大小
        NSUInteger currentCacheSize = 0;

        // Enumerate all of the files in the cache directory.  This loop has two purposes:
        //
        //  1. Removing files that are older than the expiration date.
        //  2. Storing file attributes for the size-based cleanup pass.

        // 在缓存的目录开始遍历文件.  此次遍历有两个目的:
        //
        //  1. 移除过期的文件
        //  2. 同时存储每个文件的属性(比如该file是否是文件夹、该file所需磁盘大小,修改时间)
        NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];

            // Skip directories.
            if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }

            // Remove files that are older than the expiration date;
            // 移除过期文件
            // 这里判断过期的方式:对比文件的最后一次修改日期和expirationDate谁更晚,如果expirationDate更晚,就认为该文件已经过期,具体解释见上面
            NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
            if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }

            // Store a reference to this file and account for its total size.
            // 计算当前已经使用的cache大小,
            // 并将对应file的属性存到cacheFiles中
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
            [cacheFiles setObject:resourceValues forKey:fileURL];
        }

        for (NSURL *fileURL in urlsToDelete) {
            [_fileManager removeItemAtURL:fileURL error:nil];
        }

        // 如果我们当前cache的大小已经超过了允许配置的缓存大小,那就删除已经缓存的文件。
        // 删除策略就是,首先删除修改时间更早的缓存文件
        // If our remaining disk cache exceeds a configured maximum size, perform a second
        // size-based cleanup pass.  We delete the oldest files first.
        if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
            // Target half of our maximum cache size for this cleanup pass.
            // 直接将当前cache大小降到允许最大的cache大小的一半
            const NSUInteger desiredCacheSize = self.maxCacheSize / 2;

            // Sort the remaining cache files by their last modification time (oldest first).
            // 根据文件修改时间来给所有缓存文件排序,按照修改时间越早越在前的规则排序
            NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                            usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                            }];

            // Delete files until we fall below our desired cache size.
            // 每次删除file后,就计算此时的cache的大小
            // 如果此时的cache大小已经降到期望的大小了,就停止删除文件了
            for (NSURL *fileURL in sortedFiles) {
                if ([_fileManager removeItemAtURL:fileURL error:nil]) {
                    NSDictionary *resourceValues = cacheFiles[fileURL];
                    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                    currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];

                    if (currentCacheSize < desiredCacheSize) {
                        break;
                    }
                }
            }
        }
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}

当app进入后台后的操作

- (void)backgroundCleanDisk {
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
    __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
        // Clean up any unfinished task business by marking where you
        // stopped or ending the task outright.
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
    // Start the long-running task and return immediately.
    [self cleanDiskWithCompletionBlock:^{
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
}

缓存类到此就结束了,中间有些没有介绍到的方法,应该有基础的同学都能看懂,其实代码看懂不难,问题是当我们看到一个框架后里面很多方法,各种自定义的类,我们就会不知所措,所以经过分块来学习知道了核心代码的意义,其他的方法也会迎刃而解。
遗留问题总结:
1、UIImageView+WebCache核心代码中开始停止所有的operation;
2、SDWebImageCombinedOperation类的原因好处以及出现的subOperation的原因 ;
3、SDWebImageDownloader下载类的实现 ;
还有这三个问题,下一篇我们解决最最核心的SDWebImageDownloader类,也就是下载图片的方法,看看SDWebimage是如何处理这些下载器的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值