SDWebImage设计思路及对我们项目的启迪。

1.要实现网络图片下载首先要思考几个问题。

1>.要在异步线程中执行,否则会阻塞主线程(线程管理)

2>.考虑图片是否需要下载(1.已经确定失效的URL不必下。2.已经缓存(内存、磁盘)的图片不必下。)

3>.同一个URL不要重复下载

4>.缓存策略

2.SDWebImage的主要功能

1>.实现了UIImageView的扩展,一行代码实现异步下载图片的功能,通过block回调下载进度和下载状态。


headerImage?.sd_setImage(with: url, placeholderImage: image, options: 0, progress: { (receivedSize, expectedSize) in

//返回进度

}, completed: { (image, error, type, url) in

//进度返回

})

2>.使用 SDImageCache 异步缓存图片


//添加内存缓存图片默认

SDImageCache.shared().store(image, forKey: imageKey)


//读取内存缓存图片

SDImageCache.shared().queryDiskCache(forKey: imageKey) { (image, type) in

}

3>.有时候,一张图片的 URL 中的一部分可能是动态变化的(比如获取权限上的限制),所以我们只需要把 URL 中不变的部分作为缓存用的 key,通过传入代码块,来实现自定义设置key值。


SDWebImageManager.shared().cacheKeyFilter = { (url) -> String in

//巴拉巴拉返回字符串

}

3.从SDWebImage的类开始介绍

1>.SDWebImageManager主要负责串联图片缓存和图片下载逻辑,先看下最重要的两个属性


//SDImageCache负责缓存逻辑

@property (strong, nonatomic, readwrite) SDImageCache *imageCache;

//SDWebImageDownloader下载器负责下载任务管理

@property (strong, nonatomic, readwrite) SDWebImageDownloader *imageDownloader;

2>.SDWebImageDownloader用来下载图片和优化图片加载的。


//下载任务队列,存储着每个下载任务SDWebImageDownloaderOperation,也是通过NSOperationQueue的属性设置最大并发数的。

@property (strong, nonatomic) NSOperationQueue *downloadQueue;

//图片下载的回调 block 都是存储在这个属性中,该属性是一个字典,key 是图片的 URL,value 是一个数组,包含每个图片的多组回调信息。这个数组会保存调用方法的闭包,如果url对应的闭包可以找到,则说明该图片已经在下载中了不会添加下载任务,如果没找到会创建下载任务,并且任务回调的时候回把url对应的所有闭包都回调,这样避免了同一个URL重复下载,实现了不同控件共享同一个下载任务,后面介绍方法的时候会详述,用 JSON 格式表示如下{

"url1": [

{

"kProgressCallbackKey": "progressCallback1_1",

"kCompletedCallbackKey": "completedCallback1_1"

},

{

"kProgressCallbackKey": "progressCallback1_2",

"kCompletedCallbackKey": "completedCallback1_2"

}

],

"url2": [

{

"kProgressCallbackKey": "progressCallback2_1",

"kCompletedCallbackKey": "completedCallback2_1"

},

{

"kProgressCallbackKey": "progressCallback2_2",

"kCompletedCallbackKey": "completedCallback2_2"

}

]

}

@property (strong, nonatomic) NSMutableDictionary *URLCallbacks;

接下来看一下SDWebImageDownloader的核心方法- downloadImageWithURL: options: progress: completed:,该方法首先调用了-addProgressCallback: andCompletedBlock: forURL: createCallback:,这个方法就是判断URLCallbacks中字典元素中对应的闭包是否存在(存在说明已经在下载中了,不存在说明第一次下载),如果不存在回调createCallback这个block,并将传入的进度、状态block保存到URLCallbacks中,注意由于多个线程可能访问URLCallbacks用barrierQueue来保卫下防止数据竞争,然后在SDWebImageDownloader 的createCallback回调中创建下载任务SDWebImageDownloaderOperation,并添加到SDWebImageDownloader下载器的任务队列中,利用NSOperationQueue的特性,添加到队列中的OP会自动执行。接下来我们看下SDWebImageDownloaderOperation。

3>.SDWebImageDownloaderOperation继承自NSOperation,NSOperation可以通过重写main和start方法去实现异步操作。该类才是真正处理下载任务的类。通过下载器传入的request、闭包如下


- (id)initWithRequest:(NSURLRequest *)request

options:(SDWebImageDownloaderOptions)options

progress:(SDWebImageDownloaderProgressBlock)progressBlock

completed:(SDWebImageDownloaderCompletedBlock)completedBlock

cancelled:(SDWebImageNoParamsBlock)cancelBlock {

if ((self = [super init])) {

_request = request;

_shouldDecompressImages = YES;

_shouldUseCredentialStorage = YES;

_options = options;

_progressBlock = [progressBlock copy];

_completedBlock = [completedBlock copy];

_cancelBlock = [cancelBlock copy];

_executing = NO;

_finished = NO;

_expectedSize = 0;

responseFromCached = YES; // Initially wrong until `connection:willCacheResponse:` is called or not called

}

return self;

}

通过NSURLConnection下载


self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
[self.connection start];

由NSURLConnectionDelegate回调下载状态。由于闭包是下载器SDWebImageDownloader 的- downloadImageWithURL: options: progress: completed:方法传入的,所以在SDWebImageDownloader中返回,返回后在URLCallbacks取出所有url对应的闭包进行回调。
如上这就是下载的全过程。下载逻辑并没有那么复杂,之所以难懂是因为对block的频繁操作和NSOperation的有点反人类思维的执行时机(要适应计算机思维~~)。接下来看下缓存逻辑

4>.SDImageCache类SDImageCache的内存缓存是通过一个NSCache类来实现的,NSCache比较类似于可变类型字典通过键值对存储的容器,它会有一个自动删除机制,内存紧张的时候NSCache回自动删除一些对象,并且他还是线程安全的,不用加锁。

4.对比我们的下载项目:

我们项目中的下载管理类更类似于SDWebImageDownloader,是通过NSOperationQueue类操作课件音频的下载任务,本地数据库和SDK结合的方式管理视频下载,如下图
屏幕快照 2017-11-26 下午4.03.40.png

从SDWebImage设计思路得到的启示:

1.现在我们项目中下载逻辑用通知回调在tableView查找cell的时候效率较低,可能会造成UI更新不及时的情况,优化:可以用字典保存闭包的方式用闭包回调下载状态和进度。这样会提高代码效率,且可读性很高。(这期争取实现!)

2.由于SDWebImage功能较强大,代码较多,可以借鉴SDWebImage的设计思路自己实现一个轻量级的图片缓存框架,方便维护。

5.最后说下我们对图片压缩的优化:

经过下载多张头像图片,发现server返回的头像图片大小都在100-200kb之间,但是头像并没有对清晰度那么高的要求,所以在SD的源码基础上进行修改。在异步操作SDWebImageDownloaderOperation中,下载完毕后对图片进行压缩。

策略:具体策略是参考微信,微信头像图片大小都在32kb左右,而apple提供的api有两种压缩图片的方法,一种按照质量压缩(会最大限度的保证图片质量,压缩到一定程度不进行压缩),一种是尺寸压缩(会有损图片质量),我们先用二分法循环6次看能否将图片大小保证在32kb-32kb*0.9之间,如果可以直接返回图片,如果不行,以32kb为标准按尺寸压缩,具体实现如下:


- (UIImage *)compressImage:(UIImage *)image toByte:(NSUInteger)maxLength {

// Compress by quality

CGFloat compression = 1;

NSData *data = UIImageJPEGRepresentation(image, compression);

NSLog(@"压缩前%lu",(unsigned long)data.length);

if (data.length < maxLength) return image;

CGFloat max = 1;

CGFloat min = 0;

for (int i = 0; i < 6; ++i) {

compression = (max + min) / 2;

data = UIImageJPEGRepresentation(image, compression);

if (data.length < maxLength * 0.9) {

min = compression;

} else if (data.length > maxLength) {

max = compression;

} else {

break;

}

}

UIImage *resultImage = [UIImage imageWithData:data];

if (data.length < maxLength) {

NSLog(@"压缩后%lu",(unsigned long)data.length);

return resultImage;

}

// Compress by size

NSUInteger lastDataLength = 0;

while (data.length > maxLength && data.length != lastDataLength) {

lastDataLength = data.length;

CGFloat ratio = (CGFloat)maxLength / data.length;

CGSize size = CGSizeMake((NSUInteger)(resultImage.size.width * sqrtf(ratio)),

(NSUInteger)(resultImage.size.height * sqrtf(ratio))); // Use NSUInteger to prevent white blank

UIGraphicsBeginImageContext(size);

[resultImage drawInRect:CGRectMake(0, 0, size.width, size.height)];

resultImage = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

data = UIImageJPEGRepresentation(resultImage, compression);

}

NSLog(@"压缩后%lu",(unsigned long)data.length);

return resultImage;

}

效果还是较明显的哦!

屏幕快照 2017-11-26 下午4.26.18.png

最后SD中对于细节的处理还是很值得我们研究的(例如对循环引用的处理,线程间数据的处理,内存方面的考虑),有时间还会继续更新。

写在最后:编程就是一个把复杂任务不断的分割成更小更简单的部分,然后去实现这些小部分的过程,不应该是边写边分割,功能实现了就行呗,这往往是写不出好代码的原因。

你可能感兴趣的:(SDWebImage设计思路及对我们项目的启迪。)