老司机出品———疯狂造轮子之图片异步下载类

图片异步下载类

SDWebImage,我估计没有一个做iOS的不知道这个三方库吧,他为我们提供了简洁的图片异步下载方式。在他为我一句api带来这么大便利的同时,你有没有想过他是怎么实现的呢?让我们先来看看他为我们做了什么?

  • 图片异步加载
  • 图片缓存
  • 图片编解码
  • 图片渐进式下载
  • 下载任务管理

So,你以为我要给你讲讲SDWebImage实现原理?
NONONO!SD这么一个成熟的框架早已有无数人对其架构进行过透彻的分析,老司机说了也是一样的,但作为程序员最快的成长就是不断地重造轮子。当然你造轮子不一定是要替代原来的,只是扩展一种思路。

所以,今天老司机就带着你来实现一个简单的图片下载类

让我们先分析以下我们究竟需要些什么?

下载思路

这是一个完整的图片下载思路,编解码等图片处理的老司机没有纳在其中,因为需要详尽的图片编解码知识才能读懂代码,而且本期教程也重在整理下载思路。

其实有了上面的分析我们需要做的就很明显了。

  • 首先我们需要一个图片下载类,为我们进行图片下载任务,并在完成时执行相关回调。
  • 其次我们需要一个图片缓存类,图片下载完成时将图片进行缓存。
  • 最后我们需要一个下载任务管理类,帮助我们管理当前下载任务,避免重复下载。

那我们接下来一一分析相关需求。


图片下载类

其实要写一个下载类,我们的思路应该很明显。
既然是数据请求,我们当然应该立刻想到NSURLSession做下载。

NSURLSession是iOS7推出的与NSURLConnection并列的网络请求库,并且在iOS9中苹果宣布废弃NSURLConnection,NSURLSession从此正式步入历史舞台,大多数还在维护的网络相关的三方库都跟随苹果的脚步将底层Api替换为NSURLSession相关。
————引自《老司机瞎逼逼》第一卷第一章第一篇第一行第一句

那么我们来使用NSURLSession写一个下载类。

NSURLSession其实是一个会话,管理着发生在其之上的所有数据交换任务。一个会话可以同时管理多个数据请求。并且NSURLSession还向我们提供了指定任务回调的队列的Api,让我们方便的选择在主线程或子线程中回调。

一般来讲,没有特殊需求,我们应该尽量复用我们的会话,毕竟频繁的创建与释放对象都是系统资源上的浪费。

NSURLSession为我们提供了两种初始化方式

+sessionWithConfiguration:
+sessionWithConfiguration:delegate:delegateQueue:

这里可以根据不同的需求选择对应粒度的Api进行初始化。

其中Configuration这个参数我们可以传进去一个配置对象,来定制我们session会话的不同参数。
这里系统为我们预置了3中配置

defaultSessionConfiguration

默认配置使用的是持久化的硬盘缓存,存储证书到用户钥匙链。存储cookie到shareCookie。

标注:如果想要移植原来基于NSURLConnect的代码到NSURLSession,可使用该默认配置,然后再根据需要定制该默认配置。

ephemeralSessionConfiguration

返回一个不适用永久持存cookie、证书、缓存的配置,最佳优化数据传输。

标注:当程序作废session时,所有的ephemeral session 数据会立即清除。此外,如果你的程序处于暂停状态,内存数据可能不会立即清除,但是会在程序终止或者收到内存警告或者内存压力时立即清除。

backgroundSessionConfigurationWithIdentifier

生成一个可以上传下载HTTP和HTTPS的后台任务(程序在后台运行)。
在后台时,将网络传输交给系统的单独的一个进程。

重要:identifier 是configuration的唯一标示,不能为空或nil

摘自 NSURLSessionConfiguration API详解

这里我们使用默认配置单独设置一下请求超时时长即可。


NSURLSession

有了session对象,我们就可以以request初始化NSURLSessionTask对象来做数据交换。

NSURLSessionUploadTask:上传用的Task,传完以后不会再下载返回结果;

NSURLSessionDownloadTask:下载用的Task;

NSURLSessionDataTask:可以上传内容,上传完成后再进行下载。

引自NSURLSession使用说明及后台工作流程分析

有了上面两个参考资料,这里我假设你已经会使用NSURLSession了(毕竟这不是我今天的主题),鉴于我不关心下载过程,只关心下载结果,所以我选择了最简单直接的Api。

Task

可以看到,老司机在现在完成的回调中一共做了以下几件事:

  • 检验是否下载失败,若失败,抛出错误信息
  • 若成功取到UIImage对象,使用缓存类进行数据缓存
  • 遍历回调数组进行回调

代码都很简单,也不用多做解释,这样我们的下载类就完成了。

放一下下载类的全部代码

#pragma mark --- 图片下载类 ---
@interface DWWebImageDownloader : NSObject

///回调数组
@property (nonatomic ,strong) NSMutableArray <DWWebImageCallBack>* callBacks;

///下载任务
@property (nonatomic ,strong) NSURLSessionDataTask * task;

///下载图像实例
/**
 任务完成前为nil
 */
@property (nonatomic ,strong) UIImage * image;

///现在完成标志
@property (nonatomic ,assign) BOOL downloadFinish;

///初始化方法
-(instancetype)initWithSession:(NSURLSession *)session;

///以url下载图片
-(void)downloadImageWithUrlString:(NSString *)url;

///开启下载
-(void)resume;

///取消下载
-(void)cancel;

@end

#pragma mark --- DWWebImageDownloader ---
@interface DWWebImageDownloader ()

@property (nonatomic ,copy) NSString * url;

@property (nonatomic ,strong) NSURLSession * session;

@end

@implementation DWWebImageDownloader

#pragma mark --- 接口方法 ---
-(instancetype)initWithSession:(NSURLSession *)session {
    self = [super init];
    if (self) {
        _session = session;
        _downloadFinish = NO;
    }
    return self;
}

-(void)downloadImageWithUrlString:(NSString *)url
{
    if (!url.length) {
        dispatch_async_main_safe((^(){
            [[NSNotificationCenter defaultCenter] postNotificationName:DWWebImageDownloadFinishNotification object:nil userInfo:@{@"error":DWErrorWithDescription(10001,@"url为空"),@"url":self.url}];
        }));
        return;
    }
    [self downloadImageWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:url]]];
}

-(void)resume {
    [self.task resume];
}

-(void)cancel {
    [self.task cancel];
}

#pragma mark --- Tool Method ---
-(void)downloadImageWithRequest:(NSURLRequest *)request
{
    if (!request) {
        dispatch_async_main_safe((^(){
            [[NSNotificationCenter defaultCenter] postNotificationName:DWWebImageDownloadFinishNotification object:nil userInfo:@{@"error":DWErrorWithDescription(10002,@"无法生成request对象"),@"url":self.url}];
        }));
        return;
    }
    
    self.url = request.URL.absoluteString;
    
    self.task = [self.session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error) {///下载错误
            dispatch_async_main_safe((^(){
                [[NSNotificationCenter defaultCenter] postNotificationName:DWWebImageDownloadFinishNotification object:nil userInfo:@{@"error":DWErrorWithDescription(10003, @"任务取消或错误"),@"url":self.url}];
            }));
            return ;
        }
        _session = nil;
        UIImage * image = [UIImage imageWithData:data];
        self.downloadFinish = YES;///标志下载完成
        self.image = image;
        if (!image) {
            dispatch_async_main_safe((^(){
                [[NSNotificationCenter defaultCenter] postNotificationName:DWWebImageDownloadFinishNotification object:nil userInfo:@{@"error":DWErrorWithDescription(10000, ([NSString stringWithFormat:@"图片下载失败:%@",self.url])),@"url":self.url}];
            }));
            return ;
        }
        //保存数据
        [[DWWebImageCache shareCache] cacheObj:data forKey:self.url];
        
        ///并发遍历
        [self.callBacks enumerateObjectsWithOptions:(NSEnumerationConcurrent | NSEnumerationReverse) usingBlock:^(DWWebImageCallBack  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            if (obj) {
                //图片回调
                dispatch_async_main_safe(^(){
                    obj(image);
                });
            }
        }];
        ///发送通知
        dispatch_async_main_safe((^(){
            [[NSNotificationCenter defaultCenter] postNotificationName:DWWebImageDownloadFinishNotification object:nil userInfo:@{@"url":self.url,@"image":image}];
        }));
    }];
}

-(NSMutableArray<DWWebImageCallBack> *)callBacks
{
    if (!_callBacks) {
        _callBacks = [NSMutableArray array];
    }
    return _callBacks;
}

@end

图片缓存类

SD中对图片进行了多级缓存,包括内存缓存和磁盘缓存。
这里我们也来模拟一下其实现过程。
对于这个缓存类,我们可以给自己提几个需求:

1.支持内存缓存及磁盘缓存两种缓存方式

2.对于缓存类缓存文件应做加密

3.磁盘缓存应保留清除缓存接口,并且应具备过期缓存自动清除功能

对自己好一点,少提一些需求吧┑( ̄Д  ̄)┍

所以按照需求我们可以大概知道几个技术点,一一分析一下。

内存缓存

这里我们使用的内存缓存是系统提供的NSCache类。

NSCache基本使用方法与字典相同,以key值存值和取值。不同的是,NSCache会在内存吃紧的时候自动释放内存。且相对于字典来说,NSCache是线程安全的,所以你并不需要手动加锁哦。

所以确定了内存缓存的实现方式后,我们只要部署缓存逻辑即可。

我们知道,内存读取速度是要大于磁盘读取速度的,所以当去缓存的时候我们优先取内存缓存使我们的主要策略。
另外进行磁盘缓存的时候我们还要注意两点,第一点,一定要异步子线程去执行,这样可以避免线程阻塞。第二点,既然开启了子线程就应该注意线程安全,所以这里应注意加线程安全相关的代码。

缓存读写

缓存加密

这里我们采取与SDWebImage相同的做法,以图片下载URL做MD5加密后的字符串当做key与缓存一一对应。加密算法相对固定,再次不做赘述,稍后会有统一放代码。

自动清理

自动清理的核心思想则是每当首次加载我们的Api的时候检测我们的磁盘缓存文件的最后修改时间,如果距离当前超过我们预设的过期时间则将文件移除。


移除过期文件

下面是图片缓存类的代码

#pragma mark --- 缓存管理类 ---
@interface DWWebImageCache : NSObject<NSCopying>

///缓存策略
@property (nonatomic ,assign) DWWebImageCachePolicy cachePolicy;

///缓存数据类型
@property (nonatomic ,assign) DWWebImageCacheType cacheType;

///缓存过期时间,默认值7天
@property (nonatomic ,assign) unsigned long long expirateTime;

///是否加密缓存
@property (nonatomic ,assign) BOOL useSecureKey;

///缓存空间
@property (nonatomic ,copy) NSString * cacheSpace;

///单例
+(instancetype)shareCache;

///通过key存缓存
-(void)cacheObj:(id)obj forKey:(NSString *)key;

///通过key取缓存
-(id)objCacheForKey:(NSString *)key;

///通过key移除缓存
-(void)removeCacheByKey:(NSString *)key;

///移除过期缓存
-(void)removeExpiratedCache;

@end

#pragma mark --- DWWebImageCache ---
@interface DWWebImageCache ()

@property (nonatomic ,strong) NSCache * memCache;

@property (nonatomic ,strong) dispatch_semaphore_t semaphore;

@property (nonatomic ,strong) NSFileManager * fileMgr;

@end

@implementation DWWebImageCache

#pragma mark --- 接口方法 ---
-(instancetype)init
{
    self = [super init];
    if (self) {
        _memCache = [[NSCache alloc] init];
        _memCache.totalCostLimit = DWWebImageCacheDefaultCost;
        _memCache.countLimit = 20;
        _expirateTime = DWWebImageCacheDefaultExpirateTime;
        _useSecureKey = YES;
        _cachePolicy = DWWebImageCachePolicyDisk;
        _cacheType = DWWebImageCacheTypeData;
        _semaphore = dispatch_semaphore_create(1);
        _fileMgr = [NSFileManager defaultManager];
        [self createTempPath];
    }
    return self;
}

-(void)cacheObj:(id)obj forKey:(NSString *)key
{
    NSString * url = key;
    key = transferKey(key, self.useSecureKey);
    if (self.cachePolicy & DWWebImageCachePolicyDisk) {///磁盘缓存
        writeFileWithKey(obj, url, key, self.semaphore, self.fileMgr,self.cacheSpace);
    }
    if (self.cachePolicy & DWWebImageCachePolicyMemory) {
        ///做内存缓存
        [self.memCache setObject:obj forKey:key cost:costForObj(obj)];
    }
}

-(id)objCacheForKey:(NSString *)key
{
    __block id obj = nil;
    key = transferKey(key, self.useSecureKey);
    obj = [self.memCache objectForKey:key];
    if (!obj) {
        NSAssert((self.cacheType != DWWebImageCacheTypeUndefined), @"you must set a cacheType but not DWWebImageCacheTypeUndefined");
        readFileWithKey(key, self.cacheType, self.semaphore, self.cacheSpace,^(id object) {
            obj = object;
        });
    }
    return obj;
}

-(void)removeCacheByKey:(NSString *)key
{
    key = transferKey(key, self.useSecureKey);
    [self.memCache removeObjectForKey:key];
    [self.fileMgr removeItemAtPath:objPathWithKey(key,self.cacheSpace) error:nil];
}

-(void)removeExpiratedCache
{
    if (self.expirateTime) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSDirectoryEnumerator *dir=[self.fileMgr enumeratorAtPath:sandBoxPath(self.cacheSpace)];
            NSString *path=[NSString new];
            unsigned long long timeStamp = [[NSDate date] timeIntervalSince1970];
            while ((path=[dir nextObject])!=nil) {
                NSString * fileP = objPathWithKey(path,self.cacheSpace);
                NSDictionary * attrs = [self.fileMgr attributesOfItemAtPath:fileP error:nil];
                NSDate * dataCreate = attrs[NSFileModificationDate];
                if ((timeStamp - [dataCreate timeIntervalSince1970]) > self.expirateTime) {
                    [self.fileMgr removeItemAtPath:fileP error:nil];
                }
            }
        });
    }
}

#pragma mark -- Tool Method ---
-(void)createTempPath
{
    if (![self.fileMgr fileExistsAtPath:sandBoxPath(self.cacheSpace)]) {
        [self.fileMgr createDirectoryAtPath:sandBoxPath(self.cacheSpace) withIntermediateDirectories:YES attributes:nil error:NULL];
    }
}

#pragma mark --- Setter、getter ---
-(void)setExpirateTime:(unsigned long long)expirateTime
{
    _expirateTime = expirateTime;
    if (expirateTime) {
        [self removeExpiratedCache];
    }
}

-(NSString *)cacheSpace
{
    if (!_cacheSpace) {
        return @"defaultCacheSpace";
    }
    return _cacheSpace;
}

#pragma mark --- 单例 ---
static DWWebImageCache * cache = nil;
+(instancetype)shareCache
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        cache = [[self alloc] init];
    });
    return cache;
}

+(instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        cache = [super allocWithZone:zone];
    });
    return cache;
}

-(id)copyWithZone:(NSZone *)zone
{
    return cache;
}

#pragma mark --- 内联函数 ---

/**
 异步文件写入

 @param obj 写入对象
 @param url 下载url
 @param key 缓存key
 @param semaphore 信号量
 @param fileMgr 文件管理者
 @param cacheSpace  缓存空间
 */
static inline void writeFileWithKey(id obj,NSString * url,NSString * key,dispatch_semaphore_t semaphore,NSFileManager * fileMgr,NSString * cacheSpace){
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSString * path = objPathWithKey(key,cacheSpace);
        if ([fileMgr fileExistsAtPath:path]) {
            [fileMgr removeItemAtPath:path error:nil];
        }
        if ([obj2Data(obj) writeToFile:path atomically:YES]) {
            dispatch_async_main_safe(^(){
                [[NSNotificationCenter defaultCenter] postNotificationName:
                 DWWebImageCacheCompleteNotification object:nil userInfo:@{@"url":url}];
            });
        }
        dispatch_semaphore_signal(semaphore);
    });
};


/**
 文件读取

 @param key 缓存key
 @param type 文件类型
 @param semaphore 信号量
 @param cacheSpace 缓存空间
 @param completion 读取完成回调
 */
static inline void readFileWithKey(NSString * key,DWWebImageCacheType type,dispatch_semaphore_t semaphore,NSString * cacheSpace,void (^completion)(id obj)){
    dispatch_sync(dispatch_get_global_queue(0, 0), ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSData * data = [NSData dataWithContentsOfFile:objPathWithKey(key,cacheSpace)];
        if (data && completion) {
            completion(transferDataToObj(data, type));
        }
        dispatch_semaphore_signal(semaphore);
    });
};


/**
 数据格式转换

 @param data 源数据
 @param type 数据类型
 @return 转换后数据
 */
static inline id transferDataToObj(NSData * data,DWWebImageCacheType type){
    switch (type) {
        case DWWebImageCacheTypeData:
            return data;
            break;
        case DWWebImageCacheTypeImage:
            return [UIImage imageWithData:data];
            break;
        default:
            return nil;
            break;
    }
};


/**
 返回文件路径

 @param key 缓存key
 @param cacheSpace 缓存空间
 @return 文件路径
 */
static inline NSString * objPathWithKey(NSString * key,NSString * cacheSpace){
    return [NSString stringWithFormat:@"%@/%@",sandBoxPath(cacheSpace),key];
};


/**
 对象转为NSData

 @param obj 对象
 @return 转换后data
 */
static inline NSData * obj2Data(id obj){
    NSData * data = nil;
    if ([obj isKindOfClass:[NSData class]]) {
        data = obj;
    }
    else if([obj isKindOfClass:[UIImage class]]) {
        data = UIImageJPEGRepresentation(obj, 1);
    }
    return data;
}


/**
 沙盒路径

 @param cacheSpace 缓存空间
 @return 沙盒路径
 */
static inline NSString * sandBoxPath(NSString * cacheSpace){
    return [NSHomeDirectory() stringByAppendingString:[NSString stringWithFormat:@"/Documents/DWWebImageCache/%@/",cacheSpace]];
};


/**
 计算对象所需缓存成本

 @param obj 对象
 @return 缓存成本
 */
static inline NSUInteger costForObj(id obj){
    NSUInteger cost = 0;
    ///根据数据类型计算cost
    if ([obj isKindOfClass:[NSData class]]) {
        cost = [[obj valueForKey:@"length"] unsignedIntegerValue];
    } else if ([obj isKindOfClass:[UIImage class]]) {
        UIImage * image = (UIImage *)obj;
        cost = (NSUInteger)image.size.width * image.size.height * image.scale * image.scale;
    }
    return cost;
};


/**
 返回缓存key

 @param originKey 原始key
 @param useSecureKey 是否加密
 @return 缓存key
 */
static inline NSString * transferKey(NSString * originKey,BOOL useSecureKey){
    return useSecureKey?encryptToMD5(originKey):originKey;
};


/**
 返回MD5加密字符串

 @param str 原始字符串
 @return 加密后字符串
 */
static inline NSString *encryptToMD5(NSString * str){
    CC_MD5_CTX md5;
    CC_MD5_Init (&md5);
    CC_MD5_Update (&md5, [str UTF8String], (CC_LONG)[str length]);
    
    unsigned char digest[CC_MD5_DIGEST_LENGTH];
    CC_MD5_Final (digest, &md5);
    return  [NSString stringWithFormat: @"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
             digest[0],  digest[1],
             digest[2],  digest[3],
             digest[4],  digest[5],
             digest[6],  digest[7],
             digest[8],  digest[9],
             digest[10], digest[11],
             digest[12], digest[13],
             digest[14], digest[15]];
};

@end


下载任务管理类

有没有发现分模块后思路很清晰,我们接着给自己捋捋需求吧。

  • 我们的管理类要能区分当前URL存在缓存的话,我们不需要开启下载任务,直接从缓存中读取。
  • 如果没有缓存,判断当前URL是否正在下载,如果正在下载不应开启新的下载任务,而是为之前的任务增加回调。
  • 应该为任务添加优先级,新追加的下载任务应该较之前添加且尚未开始的下载任务具有更高的优先级。

前两个需求,无非就是两个条件判断,而任务优先级我们可以通过NSOperation去添加依赖,从而实现。
我们知道NSOperation和NSURLSessionTask都是需要手动开启的,所以我们可以重写NSOperation的resume方法,可以同时开启下载任务。

同时我们知道添加到NSOperationQueue中的NSOperation会按需自动调用resume方法,所以我们可以成功的借助NSOperationQueue实现我们下载任务的相互依赖关系。看一下代码:

为下载任务添加依赖

可能现在这么说还是不懂,先等下,接着看。

下载逻辑

我们看到,每一次当创建新的任务时,我都会将上次记录的任务的依赖设置为新的任务,这样新添加的任务就会优先于上一个任务执行。然后将它加入到队列中,这样就会自动开启任务。

管理类和线程类的全部代码放一下:

#pragma mark --- 任务线程类 ---
@interface DWWebImageOperation : NSOperation

///图片下载器
@property (nonatomic ,strong) DWWebImageDownloader * donwloader;

///下载任务是否完成
@property (nonatomic , assign, getter=isFinished) BOOL finished;

///以url及session下载图片
-(instancetype)initWithUrl:(NSString *)url session:(NSURLSession *)session;

@end



#pragma mark --- 下载管理类 ---
@interface DWWebImageManager : NSObject<NSCopying>

///线程字典
/**
 url为key,对应任务线程
 */
@property (nonatomic ,strong) NSMutableDictionary <NSString *,DWWebImageOperation *>* operations;

///缓存管理对象
@property (nonatomic ,strong) DWWebImageCache * cache;

///单例
+(instancetype)shareManager;

///以url下载图片,进行回调
-(void)downloadImageWithUrl:(NSString *)url completion:(DWWebImageCallBack)completion;

///以url移除下载任务
-(void)removeOperationByUrl:(NSString *)url;

@end

#pragma mark --- DWWebImageOperation ---
@implementation DWWebImageOperation
@synthesize finished = _finished;

-(instancetype)initWithUrl:(NSString *)url session:(NSURLSession *)session
{
    self = [super init];
    if (self) {
        _donwloader = [[DWWebImageDownloader alloc] initWithSession:session];
        [_donwloader downloadImageWithUrlString:url];
    }
    return self;
}

-(void)start
{
    [super start];
    [self.donwloader resume];
}

-(void)cancel
{
    [super cancel];
    [self.donwloader cancel];
}

-(void)setFinished:(BOOL)finished {
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}

@end

#pragma mark --- DWWebImageManager ---

@interface DWWebImageManager ()

@property (nonatomic ,strong) NSURLSession * session;

@property (nonatomic ,strong) dispatch_semaphore_t semaphore;

@property (nonatomic ,strong) NSOperationQueue * queue;

@property (nonatomic ,strong) DWWebImageOperation * lastOperation;

@end

@implementation DWWebImageManager

-(instancetype)init
{
    self = [super init];
    if (self) {
        self.semaphore = dispatch_semaphore_create(1);
        self.cache = [DWWebImageCache shareCache];
        self.cache.cachePolicy = DWWebImageCachePolicyDisk | DWWebImageCachePolicyMemory;
        [self.cache removeExpiratedCache];
        dispatch_async_main_safe(^(){
            [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cacheCompleteFinishNotice:) name:DWWebImageCacheCompleteNotification object:nil];
            [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(downloadFinishNotice:) name:DWWebImageDownloadFinishNotification object:nil];
        });
    }
    return self;
}

///下载图片
-(void)downloadImageWithUrl:(NSString *)url completion:(DWWebImageCallBack)completion
{
    NSAssert(url.length, @"url不能为空");
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        ///从缓存加载图片
        UIImage * image = [UIImage imageWithData:[self.cache objCacheForKey:url]];
        if (image) {
            dispatch_async_main_safe(^(){
                completion(image);
            });
        } else {///无缓存
            dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
            DWWebImageOperation * operation = self.operations[url];///取出下载任务
            if (!operation) {///无任务
                operation = [[DWWebImageOperation alloc] initWithUrl:url session:self.session];
                self.operations[url] = operation;
                if (self.lastOperation) {
                    [self.lastOperation addDependency:operation];
                }
                [self.queue addOperation:operation];
                self.lastOperation = operation;
            }
            if (!operation.donwloader.downloadFinish) {
                [operation.donwloader.callBacks addObject:[completion copy]];
            } else {
                ///从缓存读取图片回调
                dispatch_async_main_safe(^(){
                    completion(operation.donwloader.image);
                });
            }
            dispatch_semaphore_signal(self.semaphore);
        }
    });
}

///下载完成回调
-(void)downloadFinishNotice:(NSNotification *)sender
{
    NSError * error = sender.userInfo[@"error"];
    if (error) {///移除任务
        [self removeOperationByUrl:sender.userInfo[@"url"]];
        [self removeCacheByUrl:sender.userInfo[@"url"]];
    } else {
        NSString * url = sender.userInfo[@"url"];
        DWWebImageOperation * operation = self.operations[url];///取出下载任务
        operation.finished = YES;
    }
}

///缓存完成通知回调
-(void)cacheCompleteFinishNotice:(NSNotification *)sender
{
    NSString * url = sender.userInfo[@"url"];
    if (url.length) {
        [self removeOperationByUrl:sender.userInfo[@"url"]];
    }
}

///移除下载进程
-(void)removeOperationByUrl:(NSString *)url
{
    DWWebImageOperation * operation = self.operations[url];
    [operation cancel];
    [self.operations removeObjectForKey:url];
}

///移除缓存
-(void)removeCacheByUrl:(NSString *)url
{
    [self.cache removeCacheByKey:url];
}

-(NSMutableDictionary<NSString *,DWWebImageOperation *> *)operations
{
    if (!_operations) {
        _operations = [NSMutableDictionary dictionary];
    }
    return _operations;
}

-(NSURLSession *)session
{
    if (!_session) {
        NSURLSessionConfiguration * config = [NSURLSessionConfiguration defaultSessionConfiguration];
        config.timeoutIntervalForRequest = 15;
        _session = [NSURLSession sessionWithConfiguration:config];
    }
    return _session;
}

-(NSOperationQueue *)queue
{
    if (!_queue) {
        _queue = [[NSOperationQueue alloc] init];
        _queue.maxConcurrentOperationCount = 6;
    }
    return _queue;
}

#pragma mark --- 单例 ---
static DWWebImageManager * mgr = nil;
+(instancetype)shareManager
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        mgr = [[self alloc] init];
    });
    return mgr;
}

+(instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        mgr = [super allocWithZone:zone];
    });
    return mgr;
}

-(id)copyWithZone:(NSZone *)zone
{
    return mgr;
}

@end


至此,你已经自己实现了一个异步下载类。你可以像SD一样,为UIImageView、UIButton等添加分类实现相同的效果。

这个下载思路与SD大同小异,相信你自己撸一份以后对SD会有更深的理解。
当然SD为我们做的远不止这些,你怎么可能凭一己之力抗衡千人。有空多读读成熟的第三方代码也是对自我的锻炼与提升。


同样的,老司机把写好的下载类同样放在了我的Git上,在这里


参考资料


你说老司机今天怎么不逗比了,人家一直是治学严谨的老学究好么!

傲娇
傲娇

恩,你在忍忍,这应该是我更新前最后一次做软广了=。=

DWCoreTextLabel更新到现在已经1.1.6版本了,现在除了图文混排功能,还支持文本类型的自动检测,异步绘制减少系统的卡顿,异步加载并缓存图片的功能。

version 1.1.0
全面支持自动链接支持、定制检测规则、图文混排、响应事件
优化大部分算法,提高响应效率及绘制效率

version 1.1.1
高亮取消逻辑优化
自动检测逻辑优化
部分常用方法改为内联函数,提高运行效率

version 1.1.2
绘制逻辑优化,改为异步绘制(源码修改自YYTextAsyncLayer)

version 1.1.3
异步绘制改造完成、去除事务管理类,事务管理类仍可改进,进行中

version 1.1.4
事务管理类去除,异步绘制文件抽出

version 1.1.5
添加网络图片异步加载库,支持绘制网络图片

DWCoreTextLabel

插入图片、绘制图片、添加事件统统一句话实现~

一句话实现

尽可能保持系统Label属性让你可以无缝过渡使用~

无缝过渡

恩,说了这么多,老司机放一下地址:DWCoreTextLabel,宝宝们给个star吧爱你哟

爱你哟
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容