老司機出品—瘋狂造輪子之圖片異步下載類

MaybellPine 7年前發布 | 11K 次閱讀 iOS開發 移動開發

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 





 * 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





  *)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







///緩存策略
@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: @"xxxxxxxxxxxxxxxx",
             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







///線程字典
/**
 url為key,對應任務線程
 */
@property (nonatomic ,strong) NSMutableDictionary 



     * 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





  *)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吧~愛你喲~

 

來自:http://www.cocoachina.com/ios/20170418/19079.html

 

 本文由用戶 MaybellPine 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!