SDWebImage下载图片的底层实现

@WilliamAlex大叔

前言

SDWebImage框架是我们最常用的框架,我们下载图片,清除缓存等都可以使用该框架.下面我们模仿SDWebImage来实现多图片的下载

在写代码之前,我们先整理整理思路,下载图片我们分为两种情况,有缓存和没缓存,下面我们来看看我做的两幅图

  • 无沙盒缓存


    无沙盒缓存.png
  • 有沙盒缓存

有沙盒缓存.png
  • 纠正一点: 图片中的下载操作是是保存到一个可变的字典中的,不是数组,画图的时候一整天都没有吃饭,肚子太饿了,出了错,还望大家见谅

代码实现

在正式写代码之前,需要说明几步操作

  • 定义模型类
  • 将storyboard中的控制器移除,拖入一个新的UITableViewController控制器,绑定ID:"apps"以及绑定控制器,将类型设置为subtitle,最后给控制器设置启动箭头.
  • 将ViewController的父类换成UITableViewController
  • 代码存在很大问题(需要优化)
    • 问题 1,滑动界面时有严重的卡顿现象,原因是下载操作是在主线程上执行的.
#import "ViewController.h"
#import "WGApps.h"

@interface ViewController ()

/** 图片缓存(plist文件中是字典存储的) */
@property (nonatomic, weak) NSMutableDictionary *images;

/** apps数据源*/
@property(nonatomic, strong) NSArray *apps;

@end

@implementation ViewController

#pragma mark - 生命周期方法
- (void)viewDidLoad
{
    [super viewDidLoad];

    self.tableView.rowHeight = 44;
}


#pragma mark ----------------
#pragma mark - lazyLoading

- (NSMutableDictionary *)images {
    if (_images == nil) {

        _images = [NSMutableDictionary dictionary];
    }

    return _images;
}

- (NSArray *)apps {
    if (_apps == nil) {

        // 加载plist文件
        NSString *path = [[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil];

        // 加载数据
        NSArray *dictArray = [NSArray arrayWithContentsOfFile:path];

        NSMutableArray *tempArray = [NSMutableArray arrayWithCapacity:dictArray.count];
        // 字典数组转模型数组
        for (NSDictionary *dict in dictArray) {

         [tempArray addObject:[WGApps appsWithDict:dict]];
        }
        _apps = tempArray;
    }
    return _apps;
}

#pragma mark - 数据源方法

// 一共有多少个cell
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.apps.count;
}

// 每一个cell显示什么内容
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{

    // 定义ID(最好和storyboard中定义的ID一致)
    static NSString *ID = @"apps";

    // 创建cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];

    // 通过模型拿到对应的资源
    WGApps *app = self.apps[indexPath.row];

    // 设置数据
    cell.textLabel.text = app.name;
    cell.detailTextLabel.text = app.download;

    // 下载头像

    // 1, 首先去内存缓存中取,如果没有再去沙盒中去
    UIImage *image = [self.images objectForKey:app.icon];

    if (image) {
        // 来到这里,说明图片缓存中已经有需要的图片,直接显示到对应的cell即可
        cell.imageView.image = image;
    } else
    {
        // 来到这里表示:图片缓存中没有所需要的图片,那么这时候就要到对应的沙盒中找有没有下载的图片

        // 1, 获取沙盒路径
        NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
        // 注意:拿到沙盒路径还不够,我们需要的是下载图片的路径,所以需要拼接,拿到全路径
        // 2, 获取模型中图片资源最后一个目录名
        NSString *fileName = [app.icon lastPathComponent];

        // 3, 拼接路径
        NSString *fullPath = [cachesPath stringByAppendingPathComponent:fileName];

        // 注意 : 在沙盒中的保存的资源是以二进制的形式存在的.我们还需要判断沙盒中是否有下载过的图片
        // 4, 通过图片路径,拿到下载的图片
        NSData *imageData = [NSData dataWithContentsOfFile:fullPath];

        // 5, 判断沙盒中是否有值
        if (imageData) {
            // 来到这里说明沙盒中有下载好的图片,直接将二进制转为图片显示即可,但是最后还需要讲图片报讯到图片缓存中,方便下次直接获取.
            UIImage *image = [UIImage imageWithData:imageData];

            // 显示图片
            cell.imageView.image = image;

            // 将图片保存到图片缓存中
            [self.images setObject:image forKey:app.icon];
        } else
        {

            // 来到这里说明沙盒中没有值,这时候我们就需要下载图片资源啦.
            // 下载图片
            NSURL *url = [NSURL URLWithString:app.icon];

            // 将url转为data保存到本地
            NSData *data = [NSData dataWithContentsOfURL:url];

            // 再将二进制转为图片显示到cell上
            UIImage *image = [UIImage imageWithData:data];

            // 显示图片
            cell.imageView.image = image;

            // 将下载的图片保存到图片缓存和沙盒中
            [self.images setObject:image forKey:app.icon];
            [data writeToFile:fullPath atomically:YES];

        }
    }
    return cell;
}

多图片下载(优化后的代码)

  • 解决的问题 : 解决了卡顿现象,避免了重复下载

#import "ViewController.h"
#import "WGApps.h"


@interface ViewController ()

/** 图片缓存(plist文件中是字典存储的) */
@property (nonatomic, weak) NSMutableDictionary *images;

/** apps数据源*/
@property(nonatomic, strong) NSArray *apps;

/** 下载操作 */
@property(nonatomic, strong) NSMutableDictionary *operations;

/** 队列 */
@property(nonatomic, strong) NSOperationQueue *queue;

@end

@implementation ViewController

#pragma mark - 生命周期方法
- (void)viewDidLoad
{
    [super viewDidLoad];

    self.tableView.rowHeight = 44;
}


#pragma mark ----------------
#pragma mark - lazyLoading

- (NSMutableDictionary *)operations
{
    if (_operations == nil) {
        _operations = [NSMutableDictionary dictionary];
    }
    return _operations;
}

- (NSOperationQueue *)queue {
    if (_queue == nil) {
        // 创建队列
        _queue = [[NSOperationQueue alloc] init];

        // 设置最大并发数
        _queue.maxConcurrentOperationCount = 3;
    }
    return _queue;
}

- (NSMutableDictionary *)images {
    if (_images == nil) {

        _images = [NSMutableDictionary dictionary];
    }

    return _images;
}

- (NSArray *)apps {
    if (_apps == nil) {

        // 加载plist文件
        NSString *path = [[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil];

        // 加载数据
        NSArray *dictArray = [NSArray arrayWithContentsOfFile:path];

        NSMutableArray *tempArray = [NSMutableArray arrayWithCapacity:dictArray.count];
        // 字典数组转模型数组
        for (NSDictionary *dict in dictArray) {

         [tempArray addObject:[WGApps appsWithDict:dict]];
        }
        _apps = tempArray;
    }
    return _apps;
}

#pragma mark - 数据源方法

// 一共有多少个cell
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.apps.count;
}

// 每一个cell显示什么内容
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{

    // 定义ID(最好和storyboard中定义的ID一致)
    static NSString *ID = @"apps";

    // 创建cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];

    // 通过模型拿到对应的资源
    WGApps *app = self.apps[indexPath.row];

    // 设置数据
    cell.textLabel.text = app.name;
    cell.detailTextLabel.text = app.download;

    // 下载头像

    // 1, 首先去内存缓存中取,如果没有再去沙盒中去
    UIImage *image = [self.images objectForKey:app.icon];

    if (image) {
        // 来到这里,说明图片缓存中已经有需要的图片,直接显示到对应的cell即可
        cell.imageView.image = image;
    } else
    {
        // 来到这里表示:图片缓存中没有所需要的图片,那么这时候就要到对应的沙盒中找有没有下载的图片

        // 1, 获取沙盒路径
        NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
        // 注意:拿到沙盒路径还不够,我们需要的是下载图片的路径,所以需要拼接,拿到全路径
        // 2, 获取模型中图片资源最后一个目录名
        NSString *fileName = [app.icon lastPathComponent];

        // 3, 拼接路径
        NSString *fullPath = [cachesPath stringByAppendingPathComponent:fileName];

        // 注意 : 在沙盒中的保存的资源是以二进制的形式存在的.我们还需要判断沙盒中是否有下载过的图片
        // 4, 通过图片路径,拿到下载的图片
        NSData *imageData = [NSData dataWithContentsOfFile:fullPath];

        // 5, 判断沙盒中是否有值
        if (imageData) {
            // 来到这里说明沙盒中有下载好的图片,直接将二进制转为图片显示即可,但是最后还需要讲图片报讯到图片缓存中,方便下次直接获取.
            UIImage *image = [UIImage imageWithData:imageData];

            // 显示图片
            cell.imageView.image = image;

            // 将图片保存到图片缓存中
            [self.images setObject:image forKey:app.icon];
        } else
        {

            //设置展占位图片
            cell.imageView.image = [UIImage imageNamed:@"占位图片"];

            //查看该图片的下载操作是否存在
            NSBlockOperation *download = [self.operations objectForKey:app.icon];
            if (download == nil) {

                download = [NSBlockOperation blockOperationWithBlock:^{

                    // 下载图片
                    NSURL *url = [NSURL URLWithString:app.icon];

                    // 阻塞1秒
                    [NSThread sleepForTimeInterval:1.0];

                    // 将图片转为二进制保存到本地沙盒中
                    NSData *data = [NSData dataWithContentsOfURL:url];

                    // 将二进制转为图片显示
                    UIImage *image = [UIImage imageWithData:data];

                    // 如果没有图片,一定要讲下载操作从字典中移除
                    if (image == nil) {
                        [self.operations removeObjectForKey:app.icon];
                        return ;
                    }

                    //保存图片到内存缓存
                    [self.images setObject:image forKey:app.icon];

                    //保存图片到沙河缓存
                    [data writeToFile:fullPath atomically:YES];

                    //线程间通信
                    [[NSOperationQueue mainQueue]addOperationWithBlock:^{

                        //刷新cell
                        [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
                    }];
                    [self.operations removeObjectForKey:app.icon];
                }];

                //加入到操作缓存
                [self.operations setObject:download forKey:app.icon];

                //把操作添加到队列
                [self.queue addOperation:download];
            }
        }
    }
    return cell;
}

@end

我们使用框架来实现同样功能

  • 以上就是模仿SDWebImage内部实现多图片下载的原理,接下来我们使用SDWebImage来实现多图片下载
#import "ViewController.h"
#import "WGApps.h"
#import "UIImageView+WebCache.h"


@interface ViewController ()

/** apps数据源*/
@property(nonatomic, strong) NSArray *apps;

@end

@implementation ViewController

#pragma mark - 生命周期方法
- (void)viewDidLoad
{
    [super viewDidLoad];

    self.tableView.rowHeight = 84;
}

#pragma mark ----------------
#pragma mark - lazyLoading

- (NSArray *)apps {
    if (_apps == nil) {

        // 加载plist文件
        NSString *path = [[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil];

        // 加载数据
        NSArray *dictArray = [NSArray arrayWithContentsOfFile:path];

        NSMutableArray *tempArray = [NSMutableArray arrayWithCapacity:dictArray.count];
        // 字典数组转模型数组
        for (NSDictionary *dict in dictArray) {

         [tempArray addObject:[WGApps appsWithDict:dict]];
        }
        _apps = tempArray;
    }
    return _apps;
}

#pragma mark - 数据源方法

// 一共有多少个cell
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.apps.count;
}

// 每一个cell显示什么内容
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{

    // 定义ID(最好和storyboard中定义的ID一致)
    static NSString *ID = @"apps";

    // 创建cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];

    // 通过模型拿到对应的资源
    WGApps *app = self.apps[indexPath.row];

    // 设置数据
    cell.textLabel.text = app.name;
    cell.detailTextLabel.text = app.download;

    // 下载图片
    [cell.imageView sd_setImageWithURL:[NSURL URLWithString:app.icon] placeholderImage:[UIImage imageNamed:@"placehoder"]];

    return cell;
}

@end

总结

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

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,019评论 4 62
  • 一般建筑工程位置都较为偏远,为了方便建筑工程相关人员的生活,公司都会建造一个项目部生活区。生活区里是蚂蚁虽小五脏...
    茂繁阅读 486评论 10 21
  • 我们去看烟火好吗?去,去看那,繁花之中如何再生繁花,梦境之上如何再现梦境。让我们并肩走过荒凉的河岸仰望夜空,生命的...
    虚线阅读 201评论 0 0