SDWebImage框架底層講解

本文通過模擬SDWebImage基本功能實(shí)現(xiàn),從而幫助讀者理解SDWebImage的底層實(shí)現(xiàn)機(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;
}

知識點(diǎn)

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

注意:之前沒有設(shè)置 imageView 時(shí),imageView 并不會被創(chuàng)建

  • 存在的問題
    1. 如果網(wǎng)速慢,會卡爆了!影響用戶體驗(yàn)
    2. 滾動表格,會重復(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)行測試,存在的問題--->下載完成后不顯示圖片

原因分析:

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

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

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

4.占位圖像

// 占位圖像
UIImage *placeholder = [UIImage imageNamed:@"user_default"];
cell.imageView.image = placeholder;
  • 問題
    1. 因?yàn)槭褂玫氖窍到y(tǒng)提供的 cell
    2. 每次和 cell 交互,layoutSubviews 方法會根據(jù)圖像的大小自動調(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];
  • 問題
    1. 如果網(wǎng)絡(luò)圖片下載速度不一致,同時(shí)用戶滾動圖片,可能會出現(xiàn)圖片顯示"錯行"的問題

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

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

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

解決辦法---> 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];
}];
  • 問題

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

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

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

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

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

6.操作緩沖池

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

  • 數(shù)組:按照下標(biāo),可以通過 indexPath 可以判斷操作是否已經(jīng)在進(jìn)行中
    • 無法解決上拉&下拉刷新
  • NSSet -> 無序的
    • 無法定位到緩存的操作
  • 字典:按照key,可以通過下載圖像的 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];

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

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

8.代碼重構(gòu)

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

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

重構(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)重?。?!

- (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 會自動備份 Document 中的所有文件
    • 如果保存了從網(wǎng)絡(luò)下載的文件,在上架審批的時(shí)候,會被拒!
  • tmp

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

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

    • 系統(tǒng)偏好,用戶偏好
    • 操作是通過 [NSUserDefaults standardDefaults] 來直接操作
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)

簡介
  • iOS中著名的牛逼的網(wǎng)絡(luò)圖片處理框架
  • 包含的功能:圖片下載、圖片緩存、下載進(jìn)度監(jiān)聽、gif處理等等
  • 用法極其簡單,功能十分強(qiáng)大,大大提高了網(wǎng)絡(luò)圖片的處理效率
  • 國內(nèi)超過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),需要解決以下問題:

  • UIImageView 下載圖像的功能
  • 要解決表格滾動時(shí),因?yàn)閳D像下載速度慢造成的圖片錯行問題,可以在給 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ò)流量
遺留問題
  • 代碼耦合度太高,由于下載功能是與數(shù)據(jù)源的 indexPath 綁定的,如果想將下載圖像抽取到 cell 中,難度很大!

二. 仿SDWebImage

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

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.測試下載操作

- (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)行測試!

緩存管理
  • 定義圖像緩存屬性
/// 圖像緩存
@property (nonatomic, strong) NSMutableDictionary *imageCache;
  • 懶加載
- (NSMutableDictionary *)imageCache {
    if (_imageCache == nil) {
        _imageCache = [NSMutableDictionary dictionary];
    }
    return _imageCache;
}
  • 檢測圖像緩存方法準(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)行測試

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)對象
// MARK: - 運(yùn)行時(shí)關(guān)聯(lián)對象
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 是蘋果提供的一個專門用來做緩存的類
  • 使用和 NSMutableDictionary 非常相似
  • 是線程安全的
  • 當(dāng)內(nèi)存不足的時(shí)候,會自動清理緩存
  • 程序開始時(shí),可以指定緩存的數(shù)量 & 成本
方法
  • 取值

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

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

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

    • 方案一:緩存 100 張圖片
    • 方案二:總緩存成本設(shè)定為 10M,以圖片的 寬 * 高當(dāng)作成本,圖像像素。這樣,無論緩存的多少張照片,只要像素值超過 10M,就會自動清理
    • 結(jié)論:在緩存圖像時(shí),使用成本,比單純設(shè)置數(shù)量要科學(xué)!
  • 刪除

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

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

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

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

    • 是否自動清理緩存,默認(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)]);
    }
}

// 代理方法,僅供觀察使用,開發(fā)時(shí)不建議重寫此方法
- (void)cache:(NSCache *)cache willEvictObject:(id)obj {
    NSLog(@"remove -> %@", obj);
}
修改網(wǎng)絡(luò)圖片框架
  • 修改圖像緩沖池類型,并移動到 .h 中,以便后續(xù)測試
///  圖像緩沖池
@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];

  • 測試緩存中的圖片變化

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

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

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

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

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

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

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

_maxCacheAge = kDefaultCacheMaxCacheAge

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

NSCache

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

maxConcurrentDownloads = 6

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

4> SDWebImage 支持動圖嗎?GIF

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

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

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

    • PNG:壓縮比沒有JPG高,但是無損壓縮,解壓縮性能高,蘋果推薦的圖像格式!
    • JPG:壓縮比最高的一種圖片格式,有損壓縮!最多使用的場景,照相機(jī)!解壓縮的性能不好!
    • GIF:序列楨動圖,特點(diǎn):只支持256種顏色!最流行的時(shí)候在1998~1999,有專利的!

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

  • md5

    • 如果單純使用 文件名保存,重名的幾率很高!
    • 使用 MD5 的散列函數(shù)!對完整的 URL 進(jìn)行 md5,結(jié)果是一個 32 個字符長度的字符串!

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

  • 利用通知中心觀察
  • - UIApplicationDidReceiveMemoryWarningNotification 接收到內(nèi)存警告的通知
    • 執(zhí)行 clearMemory 方法,清理內(nèi)存緩存!
  • - UIApplicationWillTerminateNotification 接收到應(yīng)用程序?qū)⒁K止通知
    • 執(zhí)行 cleanDisk 方法,清理磁盤緩存!
  • - UIApplicationDidEnterBackgroundNotification 接收到應(yīng)用程序進(jìn)入后臺通知
    • 執(zhí)行 backgroundCleanDisk 方法,后臺清理磁盤!
    • 通過以上通知監(jiān)聽,能夠保證緩存文件的大小始終在控制范圍之內(nèi)!
    • clearDisk 清空磁盤緩存,將所有緩存目錄中的文件,全部刪除!
      實(shí)際工作,將緩存目錄直接刪除,再次創(chuàng)建一個同名空目錄!
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容