上一章节主要对缓存进行了重构,使其更具扩展性。本章节将对网络加载部分进行重构,并增长进度回调和取消加载功能。git
对于一些size较大的图片(特别是GIF图片),从网络中下载下来须要一段时间。为了不这段加载时间显示空白,每每会经过设置placeholder或显示加载进度。github
在此以前,咱们是经过NSURLSession
的block
回调来直接获取到网络所获取的内容缓存
NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// 对data进行处理
}];
复制代码
显然,这么处理咱们只能获取到最终的结果,没办法获取到进度。为了获取到下载的实时进度,咱们就须要本身去实现NSURLSession
的协议NSURLSessionDelegate
。网络
NSURLSession
的协议比较多,具体能够查看官网。这里只列举当前所须要用到的协议方法:session
#pragma mark - NSURLSessionDataDelegate
//该方法能够获取到下载数据的大小
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler;
//该方法能够获取到分段下载的数据
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data;
#pragma mark - NSURLSessionTaskDelgate
//该回调表示下载完成
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error;
复制代码
block
为了实现进度回调下载,咱们须要定义两种
block
类型,一种是下载过程当中返回进度的block
,另外一种是下载完成以后对数据的回调。数据结构
typedef void(^JImageDownloadProgressBlock)(NSInteger receivedSize, NSInteger expectedSize, NSURL *_Nullable targetURL);
typedef void(^JImageDownloadCompletionBlock)(NSData *_Nullable imageData, NSError *_Nullable error, BOOL finished);
复制代码
考虑到一个下载对象可能存在多个监听,好比两个imageView
的下载地址为同一个url
。咱们须要用数据结构将对应的block
暂存起来,并在下载过程和下载完成以后回调block
。app
typedef NSMutableDictionary<NSString *, id> JImageCallbackDictionary;
static NSString *const kImageProgressCallback = @"kImageProgressCallback";
static NSString *const kImageCompletionCallback = @"kImageCompletionCallback";
#pragma mark - callbacks
- (id)addProgressHandler:(JImageDownloadProgressBlock)progressBlock withCompletionBlock:(JImageDownloadCompletionBlock)completionBlock {
JImageCallbackDictionary *callback = [NSMutableDictionary new];
if(progressBlock) [callback setObject:[progressBlock copy] forKey:kImageProgressCallback];
if(completionBlock) [callback setObject:[completionBlock copy] forKey:kImageCompletionCallback];
LOCK(self.callbacksLock);
[self.callbackBlocks addObject:callback];
UNLOCK(self.callbacksLock);
return callback;
}
- (nullable NSArray *)callbacksForKey:(NSString *)key {
LOCK(self.callbacksLock);
NSMutableArray *callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
UNLOCK(self.callbacksLock);
[callbacks removeObject:[NSNull null]];
return [callbacks copy];
}
复制代码
如上所示,咱们用NSArray<NSDictionary>
这样的数据结构来存储block
,并用不一样的key来区分progressBlock
和completionBlock
。这么作的目的是统一管理回调,减小数据成员变量,不然咱们须要使用两个NSArray
来分别保存progressBlock
和completionBlock
。此外,咱们还可使用NSArray
的valueForKey
方法便捷地根据key来获取到对应的block
。框架
#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);
@property (nonatomic, strong) dispatch_semaphore_t callbacksLock;
self.callbacksLock = dispatch_semaphore_create(1);
复制代码
因为对block
的添加和移除的调用可能来自不一样线程,咱们这里使用锁来避免因为时序问题而致使数据错误。async
if (!self.session) {
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
}
self.dataTask = [self.session dataTaskWithRequest:self.request];
[self.dataTask resume];
for (JImageDownloadProgressBlock progressBlock in [self callbacksForKey:kImageProgressCallback]){
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
复制代码
如上所示,咱们若是要本身去实现URLSession
的协议的话,不能简单地使用[NSURLSession sharedSession]
来建立,须要经过sessionWithConfiguration
方法来实现。ide
#pragma mark - NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
//获取到对应的数据总大小
NSInteger expectedSize = (NSInteger)response.expectedContentLength;
self.expectedSize = expectedSize > 0 ? expectedSize : 0;
for (JImageDownloadProgressBlock progressBlock in [self callbacksForKey:kImageProgressCallback]) {
progressBlock(0, self.expectedSize, self.request.URL);
}
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
if (!self.imageData) {
self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize];
}
[self.imageData appendData:data]; //append分段的数据,并回调下载进度
for (JImageDownloadProgressBlock progressBlock in [self callbacksForKey:kImageProgressCallback]) {
progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
}
}
#pragma mark - NSURLSessionTaskDelgate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
for (JImageDownloadCompletionBlock completionBlock in [self callbacksForKey:kImageCompletionCallback]) { //下载完成,回调总数据
completionBlock([self.imageData copy], error, YES);
}
[self done];
}
复制代码
这里要值得注意的是didReceiveResponse
方法,获取完数据的大小以后,咱们要返回一个NSURLSessionResponseDisposition
类型。这么作的目的是告诉服务端咱们接下来的操做是什么,若是咱们不须要下载数据,那么能够返回NSURLSessionResponseCancel
,反之则传入NSURLSessionResponseAllow
。
对于一些较大的图片,可能存在加载到一半以后,用户不想看了,点击返回。此时,咱们应该取消正在加载的任务,以免没必要要的消耗。图片的加载耗时主要来自于网络下载和磁盘加载两方面,因此这两个过程咱们都须要支持取消操做。
对于任务的取消,系统提供了
NSOperation
对象,经过调用cancel
方法来实现取消当前的任务。具体关于NSOperation
的使用能够查看这里。
@interface JImageDownloadOperation : NSOperation <JImageOperation>
- (instancetype)initWithRequest:(NSURLRequest *)request;
- (id)addProgressHandler:(JImageDownloadProgressBlock)progressBlock withCompletionBlock:(JImageDownloadCompletionBlock)completionBlock;
- (BOOL)cancelWithToken:(id)token;
@end
@interface JImageDownloadOperation() <NSURLSessionDataDelegate, NSURLSessionTaskDelegate>
@property (nonatomic, assign, getter=isFinished) BOOL finished;
@end
@implementation JImageDownloadOperation
@synthesize finished = _finished;
#pragma mark - NSOperation
- (void)start {
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
if (!self.session) {
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
}
self.dataTask = [self.session dataTaskWithRequest:self.request];
[self.dataTask resume]; //开始网络下载
for (JImageDownloadProgressBlock progressBlock in [self callbacksForKey:kImageProgressCallback]){
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
}
- (void)cancel {
if (self.finished) {
return;
}
[super cancel];
if (self.dataTask) {
[self.dataTask cancel]; //取消网络下载
}
[self reset];
}
- (void)reset {
LOCK(self.callbacksLock);
[self.callbackBlocks removeAllObjects];
UNLOCK(self.callbacksLock);
self.dataTask = nil;
if (self.session) {
[self.session invalidateAndCancel];
self.session = nil;
}
}
#pragma mark - setter
- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:@"isFinished"];
_finished = finished;
[self didChangeValueForKey:@"isFinished"];
}
@end
复制代码
如上所示,咱们自定义了NSOperation
,并分别复写了其start
和cancel
方法来控制网络下载的启动和取消。这里要注意的一点是咱们须要“告诉”NSOperation
什么时候完成任务,不然任务完成以后会一直存在,不会被移除,它的completionBlock
方法也不会被调用。因此咱们这里经过KVO方式重写finished
变量,来通知NSOperation
任务是否完成。
咱们知道取消网络下载,只须要调用咱们自定义
JImageDownloadOperation
的cancel
方法便可,但什么时候应该取消网络下载呢?因为一个网络任务对应多个监听者,有可能部分监听者取消了下载,而另外一部分没有取消,那么此时则不能取消网络下载。
- (id)addProgressHandler:(JImageDownloadProgressBlock)progressBlock withCompletionBlock:(JImageDownloadCompletionBlock)completionBlock {
JImageCallbackDictionary *callback = [NSMutableDictionary new];
if(progressBlock) [callback setObject:[progressBlock copy] forKey:kImageProgressCallback];
if(completionBlock) [callback setObject:[completionBlock copy] forKey:kImageCompletionCallback];
LOCK(self.callbacksLock);
[self.callbackBlocks addObject:callback];
UNLOCK(self.callbacksLock);
return callback; //返回监听对应的一个标识
}
#pragma mark - cancel
- (BOOL)cancelWithToken:(id)token { //根据标志取消
BOOL shouldCancelTask = NO;
LOCK(self.callbacksLock);
[self.callbackBlocks removeObjectIdenticalTo:token];
if (self.callbackBlocks.count == 0) { //若当前无监听者,则取消下载任务
shouldCancelTask = YES;
}
UNLOCK(self.callbacksLock);
if (shouldCancelTask) {
[self cancel];
}
return shouldCancelTask;
}
复制代码
如上所示,咱们在加入监听时,返回一个标志,若监听者须要取消任务,则根据这个标志取消掉监听事件,若下载任务监听数为零时,表示没人监听该任务,则能够取消下载任务。
对于缓存加载的取消,咱们一样能够利用
NSOperation
可取消的特性在查询缓存过程当中创建一个钩子,查询前判断是否要执行该任务。
- (NSOperation *)queryImageForKey:(NSString *)key cacheType:(JImageCacheType)cacheType completion:(void (^)(UIImage * _Nullable, JImageCacheType))completionBlock {
if (!key || key.length == 0) {
SAFE_CALL_BLOCK(completionBlock, nil, JImageCacheTypeNone);
return nil;
}
NSOperation *operation = [NSOperation new];
void(^queryBlock)(void) = ^ {
if (operation.isCancelled) { //创建钩子,若任务取消,则再也不从缓存中加载
NSLog(@"cancel cache query for key: %@", key ? : @"");
return;
}
UIImage *image = nil;
JImageCacheType cacheFrom = cacheType;
if (cacheType == JImageCacheTypeMemory) {
image = [self.memoryCache objectForKey:key];
} else if (cacheType == JImageCacheTypeDisk) {
NSData *data = [self.diskCache queryImageDataForKey:key];
if (data) {
image = [[JImageCoder shareCoder] decodeImageSyncWithData:data];
}
} else if (cacheType == JImageCacheTypeAll) {
image = [self.memoryCache objectForKey:key];
cacheFrom = JImageCacheTypeMemory;
if (!image) {
NSData *data = [self.diskCache queryImageDataForKey:key];
if (data) {
cacheFrom = JImageCacheTypeDisk;
image = [[JImageCoder shareCoder] decodeImageSyncWithData:data];
if (image) {
[self.memoryCache setObject:image forKey:key cost:image.memoryCost];
}
}
}
}
SAFE_CALL_BLOCK(completionBlock, image, cacheFrom);
};
dispatch_async(self.ioQueue, queryBlock);
return operation;
}
复制代码
如上所示,若咱们须要取消加载任务时,只需调用返回的NSOperation
的cancel
方法便可。
咱们要取消加载的对象是UIView,那么势必要将UIView和对应的operation进行关联。
@protocol JImageOperation <NSObject>
- (void)cancelOperation;
@end
复制代码
如上,咱们定义了一个JImageOperation
的协议,用于取消operation。接下来,咱们要将UIView与Operation进行关联:
static char kJImageOperation;
typedef NSMutableDictionary<NSString *, id<JImageOperation>> JOperationDictionay;
@implementation UIView (JImageOperation)
- (JOperationDictionay *)operationDictionary {
@synchronized (self) {
JOperationDictionay *operationDict = objc_getAssociatedObject(self, &kJImageOperation);
if (operationDict) {
return operationDict;
}
operationDict = [[NSMutableDictionary alloc] init];
objc_setAssociatedObject(self, &kJImageOperation, operationDict, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return operationDict;
}
}
- (void)setOperation:(id<JImageOperation>)operation forKey:(NSString *)key {
if (key) {
[self cancelOperationForKey:key]; //先取消当前任务,再从新设置加载任务
if (operation) {
JOperationDictionay *operationDict = [self operationDictionary];
@synchronized (self) {
[operationDict setObject:operation forKey:key];
}
}
}
}
- (void)cancelOperationForKey:(NSString *)key {
if (key) {
JOperationDictionay *operationDict = [self operationDictionary];
id<JImageOperation> operation;
@synchronized (self) {
operation = [operationDict objectForKey:key];
}
if (operation && [operation conformsToProtocol:@protocol(JImageOperation)]) {//判断当前operation是否实现了JImageOperation协议
[operation cancelOperation];
}
@synchronized (self) {
[operationDict removeObjectForKey:key];
}
}
}
- (void)removeOperationForKey:(NSString *)key {
if (key) {
JOperationDictionay *operationDict = [self operationDictionary];
@synchronized (self) {
[operationDict removeObjectForKey:key];
}
}
}
@end
复制代码
如上所示,咱们使用对象关联的方式将UIView和Operation绑定在一块儿,这样就能够直接调用cancelOperationForKey
方法取消当前加载任务了。
因为网络下载和缓存加载是分别在不一样的
NSOperation
中的,若要取消加载任务,则须要分别调用它们的cancel
方法。为此,咱们定义一个JImageCombineOperation
将二者关联,并实现JImageOpeartion
协议,与UIView
关联。
@interface JImageCombineOperation : NSObject <JImageOperation>
@property (nonatomic, strong) NSOperation *cacheOperation;
@property (nonatomic, strong) JImageDownloadToken* downloadToken;
@property (nonatomic, copy) NSString *url;
@end
@implementation JImageCombineOperation
- (void)cancelOperation {
NSLog(@"cancel operation for url:%@", self.url ? : @"");
if (self.cacheOperation) { //取消缓存加载
[self.cacheOperation cancel];
}
if (self.downloadToken) { //取消网络加载
[[JImageDownloader shareInstance] cancelWithToken:self.downloadToken];
}
}
@end
- (id<JImageOperation>)loadImageWithUrl:(NSString *)url progress:(JImageProgressBlock)progressBlock completion:(JImageCompletionBlock)completionBlock {
__block JImageCombineOperation *combineOperation = [JImageCombineOperation new];
combineOperation.url = url;
combineOperation.cacheOperation = [self.imageCache queryImageForKey:url cacheType:JImageCacheTypeAll completion:^(UIImage * _Nullable image, JImageCacheType cacheType) {
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
SAFE_CALL_BLOCK(completionBlock, image, nil);
});
NSLog(@"fetch image from %@", (cacheType == JImageCacheTypeMemory) ? @"memory" : @"disk");
return;
}
JImageDownloadToken *downloadToken = [[JImageDownloader shareInstance] fetchImageWithURL:url progressBlock:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
dispatch_async(dispatch_get_main_queue(), ^{
SAFE_CALL_BLOCK(progressBlock, receivedSize, expectedSize, targetURL);
});
} completionBlock:^(NSData * _Nullable imageData, NSError * _Nullable error, BOOL finished) {
if (!imageData || error) {
dispatch_async(dispatch_get_main_queue(), ^{
SAFE_CALL_BLOCK(completionBlock, nil, error);
});
return;
}
[[JImageCoder shareCoder] decodeImageWithData:imageData WithBlock:^(UIImage * _Nullable image) {
[self.imageCache storeImage:image imageData:imageData forKey:url completion:nil];
dispatch_async(dispatch_get_main_queue(), ^{
SAFE_CALL_BLOCK(completionBlock, image, nil);
});
}];
}];
combineOperation.downloadToken = downloadToken;
}];
return combineOperation; //返回一个联合的operation
}
复制代码
咱们经过loadImageWithUrl
方法返回一个实现了JImageOperation
协议的operation,这样就能够将其与UIView
绑定在一块儿,以便咱们能够取消任务的加载。
@implementation UIView (JImage)
- (void)setImageWithURL:(NSString *)url progressBlock:(JImageProgressBlock)progressBlock completionBlock:(JImageCompletionBlock)completionBlock {
id<JImageOperation> operation = [[JImageManager shareManager] loadImageWithUrl:url progress:progressBlock completion:completionBlock];
[self setOperation:operation forKey:NSStringFromClass([self class])]; //将view与operation关联
}
- (void)cancelLoadImage { //取消加载任务
[self cancelOperationForKey:NSStringFromClass([self class])];
}
@end
复制代码
[self.imageView setImageWithURL:gifUrl progressBlock:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
CGFloat progress = (float)receivedSize / expectedSize;
hud.progress = progress;
NSLog(@"expectedSize:%ld, receivedSize:%ld, targetURL:%@", expectedSize, receivedSize, targetURL.absoluteString);
} completionBlock:^(UIImage * _Nullable image, NSError * _Nullable error) {
[hud hideAnimated:YES];
__strong typeof (weakSelf) strongSelf = weakSelf;
if (strongSelf && image) {
if (image.imageFormat == JImageFormatGIF) {
strongSelf.imageView.animationImages = image.images;
strongSelf.imageView.animationDuration = image.totalTimes;
strongSelf.imageView.animationRepeatCount = image.loopCount;
[strongSelf.imageView startAnimating];
} else {
strongSelf.imageView.image = image;
}
}
}];
//模拟2s以后取消加载任务
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.imageView cancelLoadImage];
});
复制代码
以下所示,咱们能够看到图片加载到一部分以后,就被取消掉了。
以前咱们在实现网络请求时,通常是一个外部请求对应一个
request
,这么处理虽然简单,但存在必定弊端,好比对于相同url的多个外部请求,咱们不能只请求一次。为了解决这个问题,咱们对外部请求进行了管理,针对相同的url,共用同一个request
。
#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);
@interface JImageDownloader()
@property (nonatomic, strong) NSURLSession *session;
@property (nonatomic, strong) NSOperationQueue *operationQueue;
@property (nonatomic, strong) NSMutableDictionary<NSURL *, JImageDownloadOperation *> *URLOperations;
@property (nonatomic, strong) dispatch_semaphore_t URLsLock;
@end
@implementation JImageDownloader
+ (instancetype)shareInstance {
static JImageDownloader *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[JImageDownloader alloc] init];
[instance setup];
});
return instance;
}
- (void)setup {
self.session = [NSURLSession sharedSession];
self.operationQueue = [[NSOperationQueue alloc] init];
self.URLOperations = [NSMutableDictionary dictionary];
self.URLsLock = dispatch_semaphore_create(1);
}
- (JImageDownloadToken *)fetchImageWithURL:(NSString *)url progressBlock:(JImageDownloadProgressBlock)progressBlock completionBlock:(JImageDownloadCompletionBlock)completionBlock {
if (!url || url.length == 0) {
return nil;
}
NSURL *URL = [NSURL URLWithString:url];
if (!URL) {
return nil;
}
LOCK(self.URLsLock);
JImageDownloadOperation *operation = [self.URLOperations objectForKey:URL];
if (!operation || operation.isCancelled || operation.isFinished) {//若operation不存在或被取消、已完成,则从新建立请求
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:URL];
operation = [[JImageDownloadOperation alloc] initWithRequest:request];
__weak typeof(self) weakSelf = self;
operation.completionBlock = ^{ //请求完成以后,须要将operation移除
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
LOCK(self.URLsLock);
[strongSelf.URLOperations removeObjectForKey:URL];
UNLOCK(self.URLsLock);
};
[self.operationQueue addOperation:operation]; //添加到任务队列中
[self.URLOperations setObject:operation forKey:URL];
}
UNLOCK(self.URLsLock);
id downloadToken = [operation addProgressHandler:progressBlock withCompletionBlock:completionBlock];
JImageDownloadToken *token = [JImageDownloadToken new];
token.url = URL;
token.downloadToken = downloadToken;
return token; //返回请求对应的标志,以便取消
}
- (void)cancelWithToken:(JImageDownloadToken *)token {
if (!token || !token.url) {
return;
}
LOCK(self.URLsLock);
JImageDownloadOperation *opertion = [self.URLOperations objectForKey:token.url];
UNLOCK(self.URLsLock);
if (opertion) {
BOOL hasCancelTask = [opertion cancelWithToken:token.downloadToken];
if (hasCancelTask) { //若网络下载被取消,则移除对应的operation
LOCK(self.URLsLock);
[self.URLOperations removeObjectForKey:token.url];
UNLOCK(self.URLsLock);
NSLog(@"cancle download task for url:%@", token.url ? : @"");
}
}
}
@end
复制代码
本章节主要实现了网络层的进度回调和取消下载的功能,并对网络层进行了优化,避免相同url的额外请求。