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结合的方式管理视频下载,如下图从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;
}
效果还是较明显的哦!
最后SD中对于细节的处理还是很值得我们研究的(例如对循环引用的处理,线程间数据的处理,内存方面的考虑),有时间还会继续更新。
写在最后:编程就是一个把复杂任务不断的分割成更小更简单的部分,然后去实现这些小部分的过程,不应该是边写边分割,功能实现了就行呗,这往往是写不出好代码的原因。