SDWebImage框架底層講解

本文通過(guò)模擬SDWebImage基本功能實(shí)現(xiàn),從而幫助讀者理解SDWebImage的底層實(shí)現(xiàn)機(jī)制

一張圖講清楚二級(jí)緩存餐曹!

首先看一下官方的架構(gòu)圖:

SDWebImageSequenceDiagram.png
SDWebImageClassDiagram.png

一. 異步加載圖片

1.搭建界面&數(shù)據(jù)準(zhǔn)備

  • 數(shù)據(jù)準(zhǔn)備
@interface AppInfo : NSObject
///  App 名稱
@property (nonatomic, copy) NSString *name;
///  圖標(biāo) URL
@property (nonatomic, copy) NSString *icon;
///  下載數(shù)量
@property (nonatomic, copy) NSString *download;

+ (instancetype)appInfoWithDict:(NSDictionary *)dict;
///  從 Plist 加載 AppInfo
+ (NSArray *)appList;

@end
+ (instancetype)appInfoWithDict:(NSDictionary *)dict {
    id obj = [[self alloc] init];

    [obj setValuesForKeysWithDictionary:dict];

    return obj;
}

///  從 Plist 加載 AppInfo
+ (NSArray *)appList {

    NSURL *url = [[NSBundle mainBundle] URLForResource:@"apps.plist" withExtension:nil];
    NSArray *array = [NSArray arrayWithContentsOfURL:url];

    NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:array.count];

    [array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        [arrayM addObject:[self appInfoWithDict:obj]];
    }];

    return arrayM.copy;
}
  • 視圖控制器數(shù)據(jù)
///  應(yīng)用程序列表
@property (nonatomic, strong) NSArray *appList;
  • 懶加載
- (NSArray *)appList {
    if (_appList == nil) {
        _appList = [AppInfo appList];
    }
    return _appList;
}
  • 表格數(shù)據(jù)源方法
#pragma mark - 數(shù)據(jù)源方法
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

    return self.appList.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"AppCell"];

    // 設(shè)置 Cell...
    AppInfo *app = self.appList[indexPath.row];

    cell.textLabel.text = app.name;
    cell.detailTextLabel.text = app.download;

    return cell;
}

知識(shí)點(diǎn)

  1. 數(shù)據(jù)模型應(yīng)該負(fù)責(zé)所有數(shù)據(jù)準(zhǔn)備工作直秆,在需要時(shí)被調(diào)用
  2. 數(shù)據(jù)模型不需要關(guān)心被誰(shuí)調(diào)用
  3. 數(shù)組使用
    • [NSMutableArray arrayWithCapacity:array.count]; 的效率更高
    • 使用塊代碼遍歷的效率比 for 要快
  4. @"AppCell" 格式定義的字符串是保存在常量區(qū)的
  5. 在 OC 中咧叭,懶加載是無(wú)處不在的
    • 設(shè)置 cell 內(nèi)容時(shí)如果沒(méi)有指定圖像,則不會(huì)創(chuàng)建 imageView

2.同步加載圖像

// 同步加載圖像
// 1. 模擬延時(shí)
NSLog(@"正在下載 %@", app.name);
[NSThread sleepForTimeInterval:0.5];

// 2. 同步加載網(wǎng)絡(luò)圖片
NSURL *url = [NSURL URLWithString:app.icon];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];

cell.imageView.image = image;

注意:之前沒(méi)有設(shè)置 imageView 時(shí)减细,imageView 并不會(huì)被創(chuàng)建

  • 存在的問(wèn)題
    1. 如果網(wǎng)速慢毛俏,會(huì)卡爆了砂豌!影響用戶體驗(yàn)
    2. 滾動(dòng)表格,會(huì)重復(fù)下載圖像轧苫,造成用戶經(jīng)濟(jì)上的損失楚堤!

解決辦法--->異步下載圖像

3.異步下載圖像

  • 全局操作隊(duì)列
///  全局隊(duì)列,統(tǒng)一管理所有下載操作
@property (nonatomic, strong) NSOperationQueue *downloadQueue;
  • 懶加載
- (NSOperationQueue *)downloadQueue {
    if (_downloadQueue == nil) {
        _downloadQueue = [[NSOperationQueue alloc] init];
    }
    return _downloadQueue;
}
  • 異步下載
// 異步加載圖像
// 1. 定義下載操作
// 異步加載圖像
NSBlockOperation *downloadOp = [NSBlockOperation blockOperationWithBlock:^{
    // 1. 模擬延時(shí)
    NSLog(@"正在下載 %@", app.name);
    [NSThread sleepForTimeInterval:0.5];

    // 2. 異步加載網(wǎng)絡(luò)圖片
    NSURL *url = [NSURL URLWithString:app.icon];
    NSData *data = [NSData dataWithContentsOfURL:url];
    UIImage *image = [UIImage imageWithData:data];

    // 3. 主線程更新 UI
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        cell.imageView.image = image;
    }];
}];

// 2. 將下載操作添加到隊(duì)列
[self.downloadQueue addOperation:downloadOp];

運(yùn)行測(cè)試,存在的問(wèn)題--->下載完成后不顯示圖片

原因分析:

  • 使用的是系統(tǒng)提供的 cell
  • 異步方法中只設(shè)置了圖像浸剩,但是沒(méi)有設(shè)置 frame
  • 圖像加載后钾军,一旦與 cell 交互,會(huì)調(diào)用 cell 的 layoutSubviews 方法绢要,重新調(diào)整 cell 的布局

解決辦法--->使用占位圖像 or 自定義 Cell

注意演示不在主線程更新圖像的效果

4.占位圖像

// 占位圖像
UIImage *placeholder = [UIImage imageNamed:@"user_default"];
cell.imageView.image = placeholder;
  • 問(wèn)題
    1. 因?yàn)槭褂玫氖窍到y(tǒng)提供的 cell
    2. 每次和 cell 交互吏恭,layoutSubviews 方法會(huì)根據(jù)圖像的大小自動(dòng)調(diào)整 imageView 的尺寸

解決辦法--->自定義 Cell

自定義 Cell
cell.nameLabel.text = app.name;
cell.downloadLabel.text = app.download;

// 異步加載圖像
// 0. 占位圖像
UIImage *placeholder = [UIImage imageNamed:@"user_default"];
cell.iconView.image = placeholder;

// 1. 定義下載操作
NSBlockOperation *downloadOp = [NSBlockOperation blockOperationWithBlock:^{
    // 1. 模擬延時(shí)
    NSLog(@"正在下載 %@", app.name);
    [NSThread sleepForTimeInterval:0.5];
    // 2. 異步加載網(wǎng)絡(luò)圖片
    NSURL *url = [NSURL URLWithString:app.icon];
    NSData *data = [NSData dataWithContentsOfURL:url];
    UIImage *image = [UIImage imageWithData:data];

    // 3. 主線程更新 UI
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        cell.iconView.image = image;
    }];
}];

// 2. 將下載操作添加到隊(duì)列
[self.downloadQueue addOperation:downloadOp];
  • 問(wèn)題
    1. 如果網(wǎng)絡(luò)圖片下載速度不一致,同時(shí)用戶滾動(dòng)圖片重罪,可能會(huì)出現(xiàn)圖片顯示"錯(cuò)行"的問(wèn)題

    2. 修改延時(shí)代碼樱哼,查看錯(cuò)誤

// 1. 模擬延時(shí)
if (indexPath.row > 9) {
    [NSThread sleepForTimeInterval:3.0];
}

上下滾動(dòng)一下表格即可看到 cell 復(fù)用的錯(cuò)誤

解決辦法---> MVC

5.MVC

  • 在模型中添加 image 屬性
#import <UIKit/UIKit.h>

///  下載的圖像
@property (nonatomic, strong) UIImage *image;
使用 MVC 更新表格圖像
  • 判斷模型中是否已經(jīng)存在圖像
if (app.image != nil) {
    NSLog(@"加載模型圖像...");
    cell.iconView.image = app.image;
    return cell;
}
  • 下載完成后設(shè)置模型圖像
// 3. 主線程更新 UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
    // 設(shè)置模型中的圖像
    app.image = image;
    // 刷新表格
    [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}];
  • 問(wèn)題

    1. 如果圖像下載很慢,用戶滾動(dòng)表格很快剿配,會(huì)造成重復(fù)創(chuàng)建下載操作

    2. 修改延時(shí)代碼

// 模擬延時(shí)
if (indexPath.row == 0) {
    [NSThread sleepForTimeInterval:10.0];
}

快速滾動(dòng)表格搅幅,將第一行不斷“滾出/滾入”界面可以查看操作被重復(fù)創(chuàng)建的問(wèn)題

解決辦法 ---> 操作緩沖池

6.操作緩沖池

所謂緩沖池,其實(shí)就是一個(gè)容器呼胚,能夠存放多個(gè)對(duì)象

  • 數(shù)組:按照下標(biāo)茄唐,可以通過(guò) indexPath 可以判斷操作是否已經(jīng)在進(jìn)行中
    • 無(wú)法解決上拉&下拉刷新
  • NSSet -> 無(wú)序的
    • 無(wú)法定位到緩存的操作
  • 字典:按照key,可以通過(guò)下載圖像的 URL(唯一定位網(wǎng)絡(luò)資源的字符串)

小結(jié):選擇字典作為操作緩沖池

緩沖池屬性
///  操作緩沖池
@property (nonatomic, strong) NSMutableDictionary *operationCache;
  • 懶加載
- (NSMutableDictionary *)operationCache {
    if (_operationCache == nil) {
        _operationCache = [NSMutableDictionary dictionary];
    }
    return _operationCache;
}
修改代碼
  • 判斷下載操作是否被緩存——正在下載
// 異步加載圖像
// 0. 占位圖像
UIImage *placeholder = [UIImage imageNamed:@"user_default"];
cell.iconView.image = placeholder;

// 判斷操作是否存在
if (self.operationCache[app.icon] != nil) {
    NSLog(@"正在玩命下載中...");
    return cell;
}
  • 將操作添加到操作緩沖池
// 2. 將操作添加到操作緩沖池
[self.operationCache setObject:downloadOp forKey:app.icon];

// 3. 將下載操作添加到隊(duì)列
[self.downloadQueue addOperation:downloadOp];

修改占位圖像的代碼位置,觀察會(huì)出現(xiàn)的問(wèn)題

  • 下載完成后沪编,將操作從緩沖池中刪除
[self.operationCache removeObjectForKey:app.icon];
循環(huán)引用分析呼盆!
  • 弱引用 self 的編寫方法:
__weak typeof(self) weakSelf = self;
  • 利用 dealloc 輔助分析
- (void)dealloc {
    NSLog(@"我給你最后的疼愛(ài)是手放開(kāi)");
}
  • 注意
    • 如果使用 self,視圖控制器會(huì)在下載完成后被銷毀
    • 而使用 weakSelf蚁廓,視圖控制器在第一時(shí)間被銷毀

8.代碼重構(gòu)

重構(gòu)目的
  • 相同的代碼最好只出現(xiàn)一次
  • 主次方法
    • 主方法
      • 只包含實(shí)現(xiàn)完整邏輯的子方法
      • 思維清楚访圃,便于閱讀
    • 次方法
      • 實(shí)現(xiàn)具體邏輯功能
      • 測(cè)試通過(guò)后,后續(xù)幾乎不用維護(hù)
重構(gòu)的步驟
  • 1.新建一個(gè)方法
    • 新建方法
    • 把要抽取的代碼相嵌,直接復(fù)制到新方法中
    • 根據(jù)需求調(diào)整參數(shù)
  • 2.調(diào)整舊代碼
    • 注釋原代碼腿时,給自己一個(gè)后悔的機(jī)會(huì)
    • 調(diào)用新方法
  • 3.測(cè)試
  • 4.優(yōu)化代碼
    • 在原有位置,因?yàn)橐疹櫢嗟倪壿嫹贡觯a有可能是合理的
    • 而抽取之后批糟,因?yàn)榇a少了,可以檢查是否能夠優(yōu)化
    • 分支嵌套多捏雌,不僅執(zhí)行性能會(huì)差跃赚,而且不易于閱讀
  • 5.測(cè)試
  • 6.修改注釋
    • 在開(kāi)發(fā)中,注釋不是越多越好
    • 如果忽視了注釋性湿,有可能過(guò)一段時(shí)間纬傲,自己都看不懂那個(gè)注釋
    • .m 關(guān)鍵的實(shí)現(xiàn)邏輯,或者復(fù)雜代碼肤频,需要添加注釋叹括,否則,時(shí)間長(zhǎng)了自己都看不懂宵荒!
    • .h 中的所有屬性和方法汁雷,都需要有完整的注釋,因?yàn)?.h 文件是給整個(gè)團(tuán)隊(duì)看的

重構(gòu)一定要小步走报咳,要邊改變測(cè)試

重構(gòu)后的代碼
- (void)downloadImage:(NSIndexPath *)indexPath {

    // 1. 根據(jù) indexPath 獲取數(shù)據(jù)模型
    AppInfo *app = self.appList[indexPath.row];

    // 2. 判斷操作是否存在
    if (self.operationCache[app.icon] != nil) {
        NSLog(@"正在玩命下載中...");
        return;
    }

    // 3. 定義下載操作
    __weak typeof(self) weakSelf = self;
    NSBlockOperation *downloadOp = [NSBlockOperation blockOperationWithBlock:^{
        // 1. 模擬延時(shí)
        NSLog(@"正在下載 %@", app.name);
        if (indexPath.row == 0) {
            [NSThread sleepForTimeInterval:3.0];
        }
        // 2. 異步加載網(wǎng)絡(luò)圖片
        NSURL *url = [NSURL URLWithString:app.icon];
        NSData *data = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:data];

        // 3. 主線程更新 UI
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            // 將下載操作從緩沖池中刪除
            [weakSelf.operationCache removeObjectForKey:app.icon];

            if (image != nil) {
                // 設(shè)置模型中的圖像
                [weakSelf.imageCache setObject:image forKey:app.icon];
                // 刷新表格
                [weakSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
            }
        }];
    }];

    // 4. 將操作添加到操作緩沖池
    [self.operationCache setObject:downloadOp forKey:app.icon];

    // 5. 將下載操作添加到隊(duì)列
    [self.downloadQueue addOperation:downloadOp];
}

9.內(nèi)存警告

如果接收到內(nèi)存警告,程序一定要做處理暑刃,否則后果很嚴(yán)重!A锸取架谎!

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];

    // 1. 取消下載操作
    [self.downloadQueue cancelAllOperations];

    // 2. 清空緩沖池
    [self.operationCache removeAllObjects];
    [self.imageCache removeAllObjects];
}

10.沙盒緩存實(shí)現(xiàn)

沙盒目錄介紹
  • Documents

    • 保存由應(yīng)用程序產(chǎn)生的文件或者數(shù)據(jù)谷扣,例如:涂鴉程序生成的圖片,游戲關(guān)卡記錄
    • iCloud 會(huì)自動(dòng)備份 Document 中的所有文件
    • 如果保存了從網(wǎng)絡(luò)下載的文件裹匙,在上架審批的時(shí)候野哭,會(huì)被拒!
  • tmp

    • 臨時(shí)文件夾幻件,保存臨時(shí)文件
    • 保存在 tmp 文件夾中的文件,系統(tǒng)會(huì)自動(dòng)回收蛔溃,譬如磁盤空間緊張或者重新啟動(dòng)手機(jī)
    • 程序員不需要管 tmp 文件夾中的釋放
  • Caches

    • 緩存绰沥,保存從網(wǎng)絡(luò)下載的文件,后續(xù)仍然需要繼續(xù)使用贺待,例如:網(wǎng)絡(luò)下載的緩存數(shù)據(jù)徽曲,圖片
    • Caches目錄下面的文件,當(dāng)手機(jī)存儲(chǔ)空間不足的時(shí)候,會(huì)自動(dòng)刪除
    • 要求程序必需提供一個(gè)完善的清除緩存目錄的"解決方案"!
  • Preferences

    • 系統(tǒng)偏好麸塞,用戶偏好
    • 操作是通過(guò) [NSUserDefaults standardDefaults] 來(lái)直接操作
NSString+Path
#import "NSString+Path.h"

@implementation NSString (Path)

- (NSString *)appendDocumentPath {
    NSString *dir = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
    return [dir stringByAppendingPathComponent:self.lastPathComponent];
}

- (NSString *)appendCachePath {
    NSString *dir = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject;
    return [dir stringByAppendingPathComponent:self.lastPathComponent];
}

- (NSString *)appendTempPath {
    return [NSTemporaryDirectory() stringByAppendingPathComponent:self.lastPathComponent];
}

@end
沙盒緩存
  • 將圖像保存至沙盒
if (data != nil) {
    [data writeToFile:app.icon.appendCachePath atomically:true];
}
  • 檢查沙盒緩存
// 判斷沙盒文件是否存在
UIImage *image = [UIImage imageWithContentsOfFile:app.icon.appendCachePath];
if (image != nil) {
    NSLog(@"從沙盒加載圖像 ... %@", app.name);
    // 將圖像添加至圖像緩存
    [self.imageCache setObject:image forKey:app.icon];
    cell.iconView.image = image;

    return cell;
}

11.SDWebImage初體驗(yàn)

簡(jiǎn)介
  • iOS中著名的牛逼的網(wǎng)絡(luò)圖片處理框架
  • 包含的功能:圖片下載秃臣、圖片緩存、下載進(jìn)度監(jiān)聽(tīng)哪工、gif處理等等
  • 用法極其簡(jiǎn)單奥此,功能十分強(qiáng)大,大大提高了網(wǎng)絡(luò)圖片的處理效率
  • 國(guó)內(nèi)超過(guò)90%的iOS項(xiàng)目都有它的影子
  • 框架地址:https://github.com/rs/SDWebImage
演示 SDWebImage
  • 導(dǎo)入框架
  • 添加頭文件
#import "UIImageView+WebCache.h"
  • 設(shè)置圖像
[cell.iconView sd_setImageWithURL:[NSURL URLWithString:app.icon]];
思考:SDWebImage 是如何實(shí)現(xiàn)的雁比?
  • 將網(wǎng)絡(luò)圖片的異步加載功能封裝在 UIImageView 的分類中
  • UITableView 完全解耦

要實(shí)現(xiàn)這一目標(biāo)稚虎,需要解決以下問(wèn)題:

  • UIImageView 下載圖像的功能
  • 要解決表格滾動(dòng)時(shí),因?yàn)閳D像下載速度慢造成的圖片錯(cuò)行問(wèn)題偎捎,可以在給 UIImageView 設(shè)置新的 URL 時(shí)蠢终,取消之前未完成的下載操作

目標(biāo)鎖定:取消正在執(zhí)行中的操作!

12.小結(jié)

代碼實(shí)現(xiàn)回顧
  • tableView 數(shù)據(jù)源方法入手
  • 根據(jù) indexPath 異步加載網(wǎng)絡(luò)圖片
  • 使用操作緩沖池避免下載操作重復(fù)被創(chuàng)建
  • 使用圖像緩沖池實(shí)現(xiàn)內(nèi)存緩存茴她,同時(shí)能夠?qū)?nèi)存警告做出響應(yīng)
  • 使用沙盒緩存實(shí)現(xiàn)再次運(yùn)行程序時(shí)寻拂,直接從沙盒加載圖像祭钉,提高程序響應(yīng)速度,節(jié)約用戶網(wǎng)絡(luò)流量
遺留問(wèn)題
  • 代碼耦合度太高遂铡,由于下載功能是與數(shù)據(jù)源的 indexPath 綁定的,如果想將下載圖像抽取到 cell 中钾怔,難度很大愚臀!

二. 仿SDWebImage

  • 目標(biāo):模擬 SDWebImage 的實(shí)現(xiàn)
  • 說(shuō)明:整體代碼與異步加載圖片基本一致,只是編寫順序會(huì)有變化舶斧!

1.下載操作實(shí)現(xiàn)

#import "NSString+Path.h"

@interface DownloadImageOperation()
/// 要下載圖像的 URL 字符串
@property (nonatomic, copy) NSString *URLString;
/// 完成回調(diào) Block
@property (nonatomic, copy) void (^finishedBlock)(UIImage *image);
@end

@implementation DownloadImageOperation

+ (instancetype)downloadImageOperationWithURLString:(NSString *)URLString finished:(void (^)(UIImage *))finished {
    DownloadImageOperation *op = [[DownloadImageOperation alloc] init];

    op.URLString = URLString;
    op.finishedBlock = finished;

    return op;
}

- (void)main {
    @autoreleasepool {

        // 1. NSURL
        NSURL *url = [NSURL URLWithString:self.URLString];
        // 2. 獲取二進(jìn)制數(shù)據(jù)
        NSData *data = [NSData dataWithContentsOfURL:url];
        // 3. 保存至沙盒
        if (data != nil) {
            [data writeToFile:self.URLString.appendCachePath atomically:YES];
        }

        if (self.isCancelled) {
            NSLog(@"下載操作被取消");
            return;
        }

        // 4. 主線程回調(diào)
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            self.finishedBlock([UIImage imageWithData:data]);
        }];
    }
}

2.測(cè)試下載操作

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    int seed = arc4random_uniform((UInt32)self.appList.count);
    AppInfo *app = self.appList[seed];

    // 取消之前的下載操作
    if (![app.icon isEqualToString:self.currentURLString]) {
        // 取消之前操作
        [self.operationCache[self.currentURLString] cancel];
    }

    // 記錄當(dāng)前操作
    self.currentURLString = app.icon;

    // 創(chuàng)建下載操作
    DownloadImageOperation *op = [DownloadImageOperation downloadImageOperationWithURLString:app.icon finished:^(UIImage *image) {
        self.iconView.image = image;

        // 從緩沖池刪除操作
        [self.operationCache removeObjectForKey:app.icon];
    }];

    // 將操作添加到緩沖池
    [self.operationCache setObject:op forKey:app.icon];
    // 將操作添加到隊(duì)列
    [self.downloadQueue addOperation:op];
}
框架結(jié)構(gòu)設(shè)計(jì)

3.下載管理器

  • 單例實(shí)現(xiàn)
+ (instancetype)sharedManager {
    static id instance;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

之所以設(shè)計(jì)成單例矾缓,是為了實(shí)現(xiàn)全局的圖像下載管理

  • 移植屬性和懶加載代碼
/// 下載隊(duì)列
@property (nonatomic, strong) NSOperationQueue *downloadQueue;
/// 下載操作緩存
@property (nonatomic, strong) NSMutableDictionary *operationCache;

// MARK: - 懶加載
- (NSMutableDictionary *)operationCache {
    if (_operationCache == nil) {
        _operationCache = [NSMutableDictionary dictionary];
    }
    return _operationCache;
}

- (NSOperationQueue *)downloadQueue {
    if (_downloadQueue == nil) {
        _downloadQueue = [[NSOperationQueue alloc] init];
    }
    return _downloadQueue;
}
  • 定義方法
///  下載指定 URL 的圖像
///
///  @param URLString 圖像 URL 字符串
///  @param finished  下載完成回調(diào)
- (void)downloadImageOperationWithURLString:(NSString *)URLString finished:(void (^)(UIImage *image))finished;
  • 方法實(shí)現(xiàn)
- (void)downloadImageOperationWithURLString:(NSString *)URLString finished:(void (^)(UIImage *))finished {

    // 檢查操作緩沖池
    if (self.operationCache[URLString] != nil) {
        NSLog(@"正在玩命下載中,稍安勿躁");
        return;
    }

    // 創(chuàng)建下載操作
    DownloadImageOperation *op = [DownloadImageOperation downloadImageOperationWithURLString:URLString finished:^(UIImage *image) {
        // 從緩沖池刪除操作
        [self.operationCache removeObjectForKey:URLString];

        // 執(zhí)行回調(diào)
        finished(image);
    }];

    // 將操作添加到緩沖池
    [self.operationCache setObject:op forKey:URLString];
    // 將操作添加到隊(duì)列
    [self.downloadQueue addOperation:op];
}
修改 ViewController 中的代碼
  • 刪除相關(guān)屬性和懶加載方法
  • 用下載管理器接管之前的下載方法
// 創(chuàng)建下載操作
[[DownloadImageManager sharedManager] downloadImageOperationWithURLString:self.currentURLString finished:^(UIImage *image) {
    self.iconView.image = image;
}];
  • 增加取消下載功能
///  取消指定 URL 的下載操作
- (void)cancelDownloadWithURLString:(NSString *)URLString {
    // 1. 從緩沖池中取出下載操作
    DownloadImageOperation *op = self.operationCache[URLString];

    if (op == nil) {
        return;
    }

    // 2. 如果有取消
    [op cancel];
    // 3. 從緩沖池中刪除下載操作
    [self.operationCache removeObjectForKey:URLString];
}

運(yùn)行測(cè)試!

緩存管理
  • 定義圖像緩存屬性
/// 圖像緩存
@property (nonatomic, strong) NSMutableDictionary *imageCache;
  • 懶加載
- (NSMutableDictionary *)imageCache {
    if (_imageCache == nil) {
        _imageCache = [NSMutableDictionary dictionary];
    }
    return _imageCache;
}
  • 檢測(cè)圖像緩存方法準(zhǔn)備
///  檢查圖像緩存
///
///  @return 是否存在圖像緩存
- (BOOL)chechImageCache {
    return NO;
}
  • 方法調(diào)用
// 如果存在圖像緩存,直接回調(diào)
if ([self chechImageCache]) {
    finished(self.imageCache[URLString]);
    return;
}
  • 緩存方法實(shí)現(xiàn)
- (BOOL)chechImageCache:(NSString *)URLString {

    // 1. 如果存在內(nèi)存緩存锯茄,直接返回
    if (self.imageCache[URLString]) {
        NSLog(@"內(nèi)存緩存");
        return YES;
    }

    // 2. 如果存在磁盤緩存
    UIImage *image = [UIImage imageWithContentsOfFile:URLString.appendCachePath];
    if (image != nil) {
        // 2.1 加載圖像并設(shè)置內(nèi)存緩存
        NSLog(@"從沙盒緩存");
        [self.imageCache setObject:image forKey:URLString];
        // 2.2 返回
        return YES;
    }

    return NO;
}

運(yùn)行測(cè)試

4.自定義 UIImageView

  • 目標(biāo):

    • 利用下載管理器獲取指定 URLString 的圖像,完成后設(shè)置 image
    • 如果之前存在未完成的下載喂急,判斷是否與給定的 URLString 一致
    • 如果一致糕簿,等待下載結(jié)束
    • 如果不一致懂诗,取消之前的下載操作
  • 定義方法

///  設(shè)置指定 URL 字符串的網(wǎng)絡(luò)圖像
///
///  @param URLString 網(wǎng)絡(luò)圖像 URL 字符串
- (void)setImageWithURLString:(NSString *)URLString;
  • 方法實(shí)現(xiàn)
@interface WebImageView()
///  當(dāng)前正在下載的 URL 字符串
@property (nonatomic, copy) NSString *currentURLString;
@end

@implementation WebImageView

- (void)setImageWithURLString:(NSString *)URLString {

    // 取消之前的下載操作
    if (![URLString isEqualToString:self.currentURLString]) {
        // 取消之前操作
        [[DownloadImageManager sharedManager] cancelDownloadWithURLString:self.currentURLString];
    }

    // 記錄當(dāng)前操作
    self.currentURLString = URLString;

    // 創(chuàng)建下載操作
    __weak typeof(self) weakSelf = self;
    [[DownloadImageManager sharedManager] downloadImageOperationWithURLString:URLString finished:^(UIImage *image) {
        weakSelf.image = image;
    }];
}
@end
  • 修改 ViewController 中的調(diào)用代碼
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    int seed = arc4random_uniform((UInt32)self.appList.count);
    AppInfo *app = self.appList[seed];

    [self.iconView setImageWithURLString:app.icon];
}
  • 運(yùn)行時(shí)機(jī)制 —— 關(guān)聯(lián)對(duì)象
// MARK: - 運(yùn)行時(shí)關(guān)聯(lián)對(duì)象
const void *HMCurrentURLStringKey = "HMCurrentURLStringKey";

- (void)setCurrentURLString:(NSString *)currentURLString {
    objc_setAssociatedObject(self, HMCurrentURLStringKey, currentURLString, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)currentURLString {
    return objc_getAssociatedObject(self, HMCurrentURLStringKey);
}
  • 為了防止 Cell 重用芋类,取消之前下載操作的同時(shí),清空 image
self.image = nil;

三.關(guān)于NSCache緩存

介紹
  • NSCache 是蘋果提供的一個(gè)專門用來(lái)做緩存的類
  • 使用和 NSMutableDictionary 非常相似
  • 是線程安全的
  • 當(dāng)內(nèi)存不足的時(shí)候泡躯,會(huì)自動(dòng)清理緩存
  • 程序開(kāi)始時(shí),可以指定緩存的數(shù)量 & 成本
方法
  • 取值

    • - (id)objectForKey:(id)key;
  • 設(shè)置對(duì)象写穴,0成本

    • - (void)setObject:(id)obj forKey:(id)key;
  • 設(shè)置對(duì)象并指定成本

    • - (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g;
  • 成本示例,以圖片為例:

    • 方案一:緩存 100 張圖片
    • 方案二:總緩存成本設(shè)定為 10M馋没,以圖片的 寬 * 高當(dāng)作成本篷朵,圖像像素。這樣腮猖,無(wú)論緩存的多少?gòu)堈掌抗唬灰袼刂党^(guò) 10M误堡,就會(huì)自動(dòng)清理
    • 結(jié)論:在緩存圖像時(shí)锁施,使用成本,比單純?cè)O(shè)置數(shù)量要科學(xué)姥饰!
  • 刪除

    • - (void)removeObjectForKey:(id)key;
  • 刪除全部

    • - (void)removeAllObjects;
屬性
  • @property NSUInteger totalCostLimit;

    • 緩存總成本
  • @property NSUInteger countLimit;

    • 緩存總數(shù)量
  • @property BOOL evictsObjectsWithDiscardedContent;

    • 是否自動(dòng)清理緩存列粪,默認(rèn)是 YES
代碼演練
  • 定義緩存屬性
@property (nonatomic, strong) NSCache *cache;
  • 懶加載并設(shè)置限制
- (NSCache *)cache {
    if (_cache == nil) {
        _cache = [[NSCache alloc] init];
        _cache.delegate = self;
        _cache.countLimit = 10;
    }
    return _cache;
}
  • 觸摸事件添加緩存
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    for (int i = 0; i < 20; ++i) {
        NSString *str = [NSString stringWithFormat:@"%d", i];
        NSLog(@"set -> %@", str);
        [self.cache setObject:str forKey:@(i)];
        NSLog(@"set -> %@ over", str);
    }

    // 遍歷緩存
    NSLog(@"------");

    for (int i = 0; i < 20; ++i) {
        NSLog(@"%@", [self.cache objectForKey:@(i)]);
    }
}

// 代理方法,僅供觀察使用费什,開(kāi)發(fā)時(shí)不建議重寫此方法
- (void)cache:(NSCache *)cache willEvictObject:(id)obj {
    NSLog(@"remove -> %@", obj);
}
修改網(wǎng)絡(luò)圖片框架
  • 修改圖像緩沖池類型,并移動(dòng)到 .h 中氯质,以便后續(xù)測(cè)試
///  圖像緩沖池
@property (nonatomic, strong) NSCache *imageCache;
  • 修改懶加載闻察,并設(shè)置數(shù)量限制
- (NSCache *)imageCache {
    if (_imageCache == nil) {
        _imageCache = [[NSCache alloc] init];
        _imageCache.countLimit = 15;
    }
    return _imageCache;
}
  • 修改其他幾處代碼,將 self.imageCache[URLString] 替換為 [self.imageCache setObject:image forKey:URLString];

  • 測(cè)試緩存中的圖片變化

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    for (AppInfo *app in self.appList) {
        NSLog(@"%@ %@", [[DownloadImageManager sharedManager].imageCache objectForKey:app.icon], app.name);
    }
}
  • 注冊(cè)通知吴超,監(jiān)聽(tīng)內(nèi)存警告
- (instancetype)init
{
    self = [super init];
    if (self) {
        // 注冊(cè)通知
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(clearMemory) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
    }
    return self;
}

// 提示:雖然執(zhí)行不到跋涣,但是寫了也無(wú)所謂
- (void)dealloc {
    // 刪除通知
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
  • 清理內(nèi)存
- (void)clearMemory {
    NSLog(@"%s", __FUNCTION__);

    // 取消所有下載操作
    [self.downloadQueue cancelAllOperations];

    // 刪除緩沖池
    [self.operationChache removeAllObjects];
}

注意:內(nèi)存警告或者超出限制后奖年,緩存中的任何對(duì)象陋守,都有可能被清理。

四.一些你應(yīng)該知道的SDWebImage知識(shí)點(diǎn)

1> 圖片文件緩存的時(shí)間有多長(zhǎng):1周

_maxCacheAge = kDefaultCacheMaxCacheAge

2> SDWebImage 的內(nèi)存緩存是用什么實(shí)現(xiàn)的中燥?

NSCache

3> SDWebImage 的最大并發(fā)數(shù)是多少疗涉?

maxConcurrentDownloads = 6

  • 是程序固定死了峰尝,可以通過(guò)屬性進(jìn)行調(diào)整武学!

4> SDWebImage 支持動(dòng)圖嗎火窒?GIF

#import <ImageIO/ImageIO.h>
[UIImage animatedImageWithImages:images duration:duration];

5> SDWebImage是如何區(qū)分不同格式的圖像的

  • 根據(jù)圖像數(shù)據(jù)第一個(gè)字節(jié)來(lái)判斷的!

    • PNG:壓縮比沒(méi)有JPG高票编,但是無(wú)損壓縮卵渴,解壓縮性能高昔榴,蘋果推薦的圖像格式互订!
    • JPG:壓縮比最高的一種圖片格式岩榆,有損壓縮勇边!最多使用的場(chǎng)景,照相機(jī)奕坟!解壓縮的性能不好!
    • GIF:序列楨動(dòng)圖苛萎,特點(diǎn):只支持256種顏色腌歉!最流行的時(shí)候在1998~1999,有專利的馍驯!

6> SDWebImage 緩存圖片的名稱是怎么確定的!

  • md5

    • 如果單純使用 文件名保存吟吝,重名的幾率很高剑逃!
    • 使用 MD5 的散列函數(shù)蛹磺!對(duì)完整的 URL 進(jìn)行 md5裙品,結(jié)果是一個(gè) 32 個(gè)字符長(zhǎng)度的字符串!

7> SDWebImage 的內(nèi)存警告是如何處理的!

  • 利用通知中心觀察
  • - UIApplicationDidReceiveMemoryWarningNotification 接收到內(nèi)存警告的通知
    • 執(zhí)行 clearMemory 方法驰弄,清理內(nèi)存緩存!
  • - UIApplicationWillTerminateNotification 接收到應(yīng)用程序?qū)⒁K止通知
    • 執(zhí)行 cleanDisk 方法岔擂,清理磁盤緩存忆某!
  • - UIApplicationDidEnterBackgroundNotification 接收到應(yīng)用程序進(jìn)入后臺(tái)通知
    • 執(zhí)行 backgroundCleanDisk 方法点待,后臺(tái)清理磁盤!
    • 通過(guò)以上通知監(jiān)聽(tīng)弃舒,能夠保證緩存文件的大小始終在控制范圍之內(nèi)癞埠!
    • clearDisk 清空磁盤緩存,將所有緩存目錄中的文件聋呢,全部刪除苗踪!
      實(shí)際工作,將緩存目錄直接刪除削锰,再次創(chuàng)建一個(gè)同名空目錄颅夺!
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市昌讲,隨后出現(xiàn)的幾起案子窄驹,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡侵蒙,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門靴庆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人野瘦,你說(shuō)我怎么就攤上這事迟蜜。” “怎么了返敬?”我有些...
    開(kāi)封第一講書人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵元媚,是天一觀的道長(zhǎng)嗤无。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么轴捎? 我笑而不...
    開(kāi)封第一講書人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著摆出,像睡著了一般偎漫。 火紅的嫁衣襯著肌膚如雪魂务。 梳的紋絲不亂的頭發(fā)上翼抠,一...
    開(kāi)封第一講書人閱讀 49,749評(píng)論 1 289
  • 那天煞烫,我揣著相機(jī)與錄音,去河邊找鬼累颂。 笑死滞详,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的紊馏。 我是一名探鬼主播料饥,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼朱监!你這毒婦竟也來(lái)了岸啡?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤赫编,失蹤者是張志新(化名)和其女友劉穎巡蘸,沒(méi)想到半個(gè)月后奋隶,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡悦荒,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年唯欣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片逾冬。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡黍聂,死狀恐怖躺苦,靈堂內(nèi)的尸體忽然破棺而出身腻,到底是詐尸還是另有隱情,我是刑警寧澤匹厘,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布嘀趟,位于F島的核電站,受9級(jí)特大地震影響愈诚,放射性物質(zhì)發(fā)生泄漏她按。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一炕柔、第九天 我趴在偏房一處隱蔽的房頂上張望酌泰。 院中可真熱鬧,春花似錦匕累、人聲如沸陵刹。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)衰琐。三九已至,卻和暖如春炼蹦,著一層夾襖步出監(jiān)牢的瞬間羡宙,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工掐隐, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留狗热,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓虑省,卻偏偏與公主長(zhǎng)得像斗搞,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子慷妙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容