前言:本文章非快餐搬运,需要花点耐心阅读,因为是结合自己理解从Manager 开始一个方法一个方法去阅读解析,这个过程是痛苦的、但是读完之后是喜悦的,也希望指出个人理解不准确的地方,大家共同进步。
文章设定:较少篇幅的代码片段直接复制过来解析说明,篇幅较长的就上截图了,这样也方便各位观看。
本篇文章采用属性定位代码片段方法来探究SDImageCacheConfig 的一些配置属性具体作用,而不仅仅通过头文件的注释去理解
进入 SDImageCacheConfig 里面看看,这里面的一些配置还是比较重要的
SDImageCacheConfig.h 里面放开的配置属性,我们一条一条的来解读
/**
* Decompressing images that are downloaded and cached can improve performance but can consume lot of memory.
* Defaults to YES. Set this to NO if you are experiencing a crash due to excessive memory consumption.
*/
@property (assign, nonatomic) BOOL shouldDecompressImages;
这个属性作用就是 image 下载和缓存的时候就解压目的是用来提高性能,但是会消耗过多的内存,默认开启,如果因为这个问题导致崩溃可以关掉该属性
代码片段:
if (self.config.shouldDecompressImages) {
BOOLshouldScaleDown = options &SDImageCacheScaleDownLargeImages;
image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
}
optionsDict 以SDWebImageCoderScaleDownLargeImagesKey为key,SDImageCacheOptions 为value的值,shouldScaleDown 进行了位运算,作者的意思就是 不管选择哪个缓存策略最终都会带上SDImageCacheScaleDownLargeImages,这个枚举的意思大致为图片会以原始尺寸解码,在iOS设备上会自动缩放以适应不同的机型设备
进入 SDWebImageCodersManager 的 解压缩方法
截图可以发现最终核心解压缩的方法是 遵循了 SDWebImageCoder 协议的 SDWebimageImageIOCoder这个类,还有一个类是SDWebImageGifCoder,也是遵循SDWebImageCoder协议,只不过里面直接返回image, // GIF do not decompress 具体截图就不上了,我对这个实现的理解就是:解压缩的方法算是共性抽象的,并不是一定是指那个具体类负责,所以抽象成协议方法,让遵循这个协议的其他类都可以重写解压缩方法,即抽象方法实现(理解不对欢迎指正)。目前有 SDWebimageImageIOCoder和 SDWebImageGifCoder,后续如果有其他类只需遵循该协议即可,这也印证了设计模式中的一个:对修改关闭,对扩展开放,即开闭原则(也是个人理解哈)。
还是进入 SDWebImageImageIOCoder 里看细节:
是MAC 直接返回image, iOS或者watch 继续解压缩,如果我们没设置 SDImageCacheOptions,那么简单解析,进入 sd_decompressedImageWithImage 方法里面看看
shouldDecodeImage方法就一个意思:排除不符合解压的image,image 为nil 或者image 是gif图。
autoreleasepool 优化内存使用,注释都有
剩下的代码就是进行位图解析了,image 提取CGImage、颜色空间、透明通道等等这些参数提取出来之后开辟上下文环境在画布上画出来,最终获取解析之后的图片。
如果我们设置了SDImageCacheOptions ,那么进入sd_imageFormatForImageData方法,这个方法有点复杂,耐心点看完。
第一部分代码片段:
shouldDecodeImage 已经解释过了, 下面看看 shouldScaleDownImage 这个方法
#if SD_UIKIT || SD_WATCH
+ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image {
BOOLshouldScaleDown =YES;
CGImage RefsourceImageRef = image.CGImage;
CGSize sourceResolution =CGSizeZero;
sourceResolution.width=CGImageGetWidth(sourceImageRef);
sourceResolution.height=CGImageGetHeight(sourceImageRef);
float sourceTotalPixels = sourceResolution.width* sourceResolution.height;
float imageScale =kDestTotalPixels/ sourceTotalPixels;
if(imageScale <1) {
shouldScaleDown =YES;
}else{
shouldScaleDown =NO;
}
return shouldScaleDown;
}
第二部分代码片段:
主要就是取出SDWeb支持的最大源像素点sourceResolution 然后和image本身的比较,小于1则返回YES,反之返回false
我们继续回到sd_imageFormatForImageData方法,下面还是进入autoReleasePool,主要操作就是 利用SDWeb 支持最大的输入图片像素点来和1MB图片像素点求比例
kDestTotalPixels (1MB包含的像素点)= kBytesPerMB (1MB所包含的字节)/ kBytesPerPixel(一个像素点包含的字节)
所以输入图片的大小 就是 对应的宽,高 与缩放比例乘积 , 一时间想不透的可以拿笔展开公式看看。
destResolution.width = (int)(sourceResolution.width * imageScale)
destResolution.hight = (int)(sourceressoultion.height * imageScale)
第三部分代码片段:
就是开启上下文画出位图了,之前已经解释过了。
第四部分代码片段:(目前也没读太透彻),先说一个概念:增量大小,
因为SDWeb 定义了1MB内最大解析像素大小,所以输入image和输出image 必然是不对等的,所以两者差异就是所谓的增量大小(琢磨注释的英文好久理解出来的,如果不对请指正),
sourceTile.size.width = sourceResolution.width
sourceTile.size.height = (int)(kTitleTotalPixels(SDWeb定义的1MB的增量大小)) / sourceTile.size.width);
//输入增量和输入增量是同比例的,所以用输入增量 * 比例即可
destTitle.size.width = destResolution.width;
destTitle.size.height = sourceTile.size.height * imageScale;
//源重叠似乎与目标重叠成比例
float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);//计算出源图的重叠部分
int iterations = (int)( sourceResolution.height/ sourceTile.size.height);//源图高度/增量高度= 增量高度相比于源图片高度迭代次数
// If tile height doesn't divide the image height evenly, add another iteration
// to account for the remaining pixels.
int remainder = (int)sourceResolution.height% (int)sourceTile.size.height; //注释很明显 为了计算剩下的像素点
if(remainder) {
iterations++;
}
// Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations. 重叠部分增加到增量,但是要保存Y坐标系的原始重叠高度(理解不透。。。)
float sourceTileHeightMinusOverlap = sourceTile.size.height;
sourceTile.size.height += sourceSeemOverlap;//增量部分 增加 重叠高度
destTile.size.height += kDestSeemOverlap; //增量部分 增加 重叠高度
for(inty =0; y < iterations; ++y ) {
@autoreleasepool {
sourceTile.origin.y= y * sourceTileHeightMinusOverlap + sourceSeemOverlap; //计算源增量的y的位置
destTile.origin.y= destResolution.height- (( y +1) * sourceTileHeightMinusOverlap * imageScale +kDestSeemOverlap); //计算目标增量y位置
sourceTileImageRef =CGImageCreateWithImageInRect( sourceImageRef, sourceTile );//画出源增量图
if( y == iterations -1&& remainder ) {//最后一次迭代的情况,并且有剩下的像素点
float dify = destTile.size.height; //目标增量高度
destTile.size.height=CGImageGetHeight( sourceTileImageRef ) * imageScale;//根据源增量图比例计算出来的目标增量高度
dify -= destTile.size.height; //得出目标图增量差值
destTile.origin.y+= dify; // 得出目标增量Y位置
}
CGContextDrawImage( destContext, destTile, sourceTileImageRef );//画出目标增量图
CGImageRelease( sourceTileImageRef ); //
}
}
CGImageRef destImageRef = CGBitmapContextCreateImage(destContext); //上面遍历完成之后,增量图上下文也绘制完成了,取出增量图
CGContextRelease(destContext); //释放
if(destImageRef ==NULL) {
returnimage;
}
UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
CGImageRelease(destImageRef);
if(destImage ==nil) {
returnimage;
}
return destImage;
} //此括号是 上面的autoreleasepool 结束括号
我们回到函数- (nullable UIImage *)decompressedImageWithImage:(nullable UIImage *)image
data:(NSData*_Nullable*_Nonnull)data
options:(nullableNSDictionary*)optionsDict
方法里面,即上面第二张图
UIImage *scaledDownImage = [self sd_decompressedAndScaledDownImageWithImage:image];
if(scaledDownImage && !CGSizeEqualToSize(scaledDownImage.size, image.size)) {
// if the image is scaled down, need to modify the data pointer as well
SDImageFormat format = [NSData sd_imageFormatForImageData:*data];
NSData*imageData = [selfencodedDataWithImage:scaledDownImageformat:format];
if(imageData) {
*data = imageData;
}
}
return scaledDownImage;
解压完图片之后 判断一下 是否有 scaleDownImage 以及 是否图片被缩放处理过,参数data 是二级指针需要注意下,查了下整个SDWebImage 发现,这个二级指针的设计是为了弥补iOS 只能有一个返回值的方法(还是个人理解)。如果不这样写估计得用字典或者模型来返回image 和 data了。
我们进入sd_imageFormatForImageData 方法里面看看
此处省略前面的错误数据判断:
这个方法主要就是根据data 的第一个字节来判断图片格式,代码应该很好理解,不在细究了。
到这里图片解压缩基本完成了,我们梦回开始的地方,即第一张图的内容,设置一下图片的imageformat 就完成了。
到这里 shouldDecompressImages 这个属性的意义基本过完了,我们再看看第二个属性
/*--------------------------------------------属性分割线----------------------------------*/
/**
* Whether or not to disable iCloud backup
* Defaults to YES.
*/
@property (assign, nonatomic) BOOL shouldDisableiCloud;
这个比较简单,是否移除云存储备份
// disable iCloud backup
if (self.config.shouldDisableiCloud) {
[fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
}
shouldDisableiCloud 属性为YES的时候 会 以NSURLIsExcludedFromBackupKey 为key 进行备份
NSURLIsExcludedFromBackupKey 具体解释:
FOUNDATION_EXPORT NSURLResourceKey const NSURLIsExcludedFromBackupKey API_AVAILABLE(macos(10.8), ios(5.1), watchos(2.0), tvos(9.0)); // true if resource should be excluded from backups, false otherwise (Read-write, value type boolean NSNumber). This property is only useful for excluding cache and other application support files which are not needed in a backup. Some operations commonly made to user documents will cause this property to be reset to false and so this property should not be used on user documents.
大概意思就是:True 的时候 会从云存储里面干掉这个文件备份,false不管,但是最好不要在用户存储空间去操作,容易发绳错误,这个也就解释了为啥makeDiskCachePath 方法里面不以用户空间建立缓存机制而是选择cache(个人这样理解的,如果有误尽管喷,这会让我进步)。
/*--------------------------------------------属性分割线----------------------------------*/
重点来了:
/**
* Whether or not to use memory cache
* @note When the memory cache is disabled, the weak memory cache will also be disabled.
* Defaults to YES.
*/
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
是否需要用到内存缓存,最后一句话 the weak memory cache will also be disabled ,弱内存缓存
代码片段:
// if memory cache is enabled
if (self.config.shouldCacheImagesInMemory) {
NSUInteger cost =SDCacheCostForImage(image);
[self.memCachesetObject:imageforKey: key cost:cost];
}
阅读起来也不难, SDCacheCostForImage 内敛函数,主要计算出image 大小,
FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
#if SD_MAC
returnimage.size.height * image.size.width;
#elif SD_UIKIT || SD_WATCH
return image.size.height* image.size.width* image.scale* image.scale;
#endif
}
可以看出SDCacheCostForImage实现里面就是计算了图片像素点大小,然后在外面用cache 存储起来
然后以key 缓存起来,这个key 处理封装也是挺人性化,限于篇幅 直接提取 核心方法:
key 获取主要是通过 self.cacheKeyFilter 这个可以外放自定义的block。如果没有自定义就默认 url.absoluteString
- (nullable NSString*)cacheKeyForURL:(nullableNSURL*)url {
if(!url) {
return@"";
}
if (self.cacheKeyFilter) {
return self.cacheKeyFilter(url);
}else{
return url.absoluteString;
}
}
可以看到 例子 放到外面让使用者自己根据一定规则制定key,多说一句,这个其实和AF 的外放block 让开发这自定义异曲同工。
/*--------------------------------------------属性分割线----------------------------------*/
/**
* The option to control weak memory cache for images. When enable, `SDImageCache`'s memory cache will use a weak maptable to store the image at the same time when it stored to memory, and get removed at the same time.
* However when memory warning is triggered, since the weak maptable does not hold a strong reference to image instacnce, even when the memory cache itself is purged, some images which are held strongly by UIImageViews or other live instances can be recovered again, to avoid later re-query from disk cache or network. This may be helpful for the case, for example, when app enter background and memory is purged, cause cell flashing after re-enter foreground.
* Defautls to YES. You can change this option dynamically.
*/
@property (assign, nonatomic) BOOL shouldUseWeakMemoryCache;
弱引用表就是针对cache数据在缓存一次,但是它仅仅是引用而不持有,所以不影响引用计数,当图片缓存被系统清除的时候,但是图片其实还是被某个UIImageView或者其他的实例持有的可以再次恢复,避免此时从磁盘里面读图片(I/O口耗性能)。
图中可以看出weakCache 做为第二缓存,一旦因为内存问题cache被清掉,图片还是被其他实例引用这,这个时候就可以利用weakCache 来恢复cache而不需要从磁盘读取。
通过key 来获取obj(其实就是image)。如果没开启弱引用直接反馈obj,此时obj 可能有可能没有(不确定系统什么时候清NSCache),如果开启弱引用表,直接走下面逻辑,
if(obj) {
// Sync cache
NSUInteger cost =0;
if([obj isKindOfClass:[UIImage class]]) {
cost =SDCacheCostForImage(obj);
}
[super setObject:obj forKey:key cost:cost];
}
直接从弱引用表获取然后缓存到NSCache里面了,提高效率
/*--------------------------------------------属性分割线----------------------------------*/
/**
* The reading options while reading cache from disk.
* Defaults to 0. You can set this to `NSDataReadingMappedIfSafe` to improve performance.
*/
@property (assign, nonatomic) NSDataReadingOptions diskCacheReadingOptions;
系统的data读取属性,可以在系统的帮助文档查看到各个枚举含义,默认是0
NSDataReadingMappedIfSafe: 数据被读到虚拟内存(实际物理地址的一个扩展,但不是内存,通过虚拟地址映射得到,相关知识可以网上查下,所以不会占用当前程序的内存)。
NSDataReadingUncached:数据文件不会被缓存到内存里面,针对只会被使用一次的数据,提高性能
NSDataReadingMappedAlways:数据总是被缓存到内存里面,如果定义了NSDataReadingMappedIfSafe,NSDataReadingMappedAlways属性的优先级高,即缓存到内存。
/*--------------------------------------------属性分割线----------------------------------*/
/**
* The writing options while writing cache to disk.
* Defaults to `NSDataWritingAtomic`. You can set this to `NSDataWritingWithoutOverwriting` to prevent overwriting an existing file.
*/
@property (assign, nonatomic) NSDataWritingOptions diskCacheWritingOptions;
NSDataWritingAtmoic:数据写到临时文件后续会交换到指定文件路径中(保持原子性操作,写入过程如果出现意外则不影响现有文件) NSDataWritingAtmoic = 0x000000001
NSDataWritingWithoutOverwriting: 文件存在直接返回不写入, NSDataWritingWithoutOverwriting = 0x000000002
NSDataWritingFileProtectionNone: 文件不加密存入磁盘,可以在设备开启或者解锁的时候访问该文件。NSDataWritingFileProtectionNone = 0x10000000
NSDataWritingFileProtectionComplete:文件加密存入磁盘,仅仅在设备未锁定情况下访问数据,其他任何时候读或写都会失败 NSDataWritingFileProtectionComplete = 0x20000000
NSDataWritingFileProtectionCompleteUnlessOpen:设备被锁住的情况下无法读或者写,但是如果被文件被打开之后设备被锁定,仍然可以写或读 NSDataWritingFileProtectionCompleteUnlessOpen = 0x30000000
NSDataWritingFileProtectionCompleteUntilFirstUserAuthentication:设备锁住的情况下也可以读和写,但是一旦设备重启,性质就和NSDataWritingFileProtectionComplete一致了,(结合该字段的意思大概理解苹果想表达的是 重启设备之后用户授权解锁一次即可读或写,只要设备不重启 后续设备锁定依然可以读或写)NSDataWritingFileProtectionCompleteUntilFirstUserAuthentication = 0x40000000
NSDataWritingFileProtectionMask:为了给上面所有的属性做与(&)位运算用,方便获取第8个字节的具体值(个人理解,从上面的每个属性的二进制数据看都是第八位有数据,而此属性第八位是F,个人理解,不对欢迎指正)。NSDataWritingFileProtectionMask = 0xf0000000
例子:0xf0000000 & diskCacheWritingOptions == NSDataWritingFileProtectionComplete 即 diskCacheWritingOptions = NSDataWritingFileProtectionComplete
/*--------------------------------------------属性分割线----------------------------------*/
* The maximum length of time to keep an image in the cache, in seconds.
*/
@property (assign, nonatomic) NSInteger maxCacheAge;
SDWebImage 默认缓存时间是1周
/*--------------------------------------------属性分割线----------------------------------*/
/**
* The maximum size of the cache, in bytes.
*/
@property (assign, nonatomic) NSUInteger maxCacheSize;
SDWebImage 默认大小是0 ,可以自己设置,单位字节
/*--------------------------------------------属性分割线----------------------------------*/
/**
* The attribute which the clear cache will be checked against when clearing the disk cache
* Default is Modified Date
*/
@property (assign, nonatomic) SDImageCacheConfigExpireType diskCacheExpireType;
此属性定义了删除缓存数据的时间怎么算,SDImageCacheConfigExpireTypeAccessDate 是按照最近打开数据文件时间来算
SDImageCacheConfigExpireTypeModificationDate 图片最近修改时间来算
到此SDImageCacheConfig 总算阅读完了,中间肯定有许多不足,也希望大家能在评论区指出问题。谢谢!