iOS开发之SDwebImage的前世今生

请你说说SDwebImage这个框架的内部实现原理?
当你看到这句话的时候,是不是很熟悉,有可能你在博客上看到一些关于SDwebImage原理的详解,也大概记忆了一些标准的答案,但是小编今天要说的网上那些解读都太肤浅了,如果面试官是个高手,深入问几句,也许你就熄火了。
首先我们要了解SDwebImage框架为什么出现?
因为在一个App中多图片下载是一个耗时操作,也是消耗内存的操作,如果让下载过的图片不重复下载,当面临这些的问题,该如何解决?
在SDwebImage这个框架还没有出现之前,一些比较优秀的互联网公司一些优秀App他们是怎么处理这个问题的呢?

@interface ViewController ()
/** tableView的数据源 */
@property (nonatomic, strong) NSArray *apps;
@end

@implementation ViewController

pragma mark ----------------------

pragma mark lazy loading

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

    //字典数组
    NSArray *arrayM = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle]pathForResource:@"apps.plist" ofType:nil]];
    
    //字典数组---->模型数组
    NSMutableArray *arrM = [NSMutableArray array];
    for (NSDictionary *dict in arrayM) {
        [arrM addObject:[XMGAPP appWithDict:dict]];
    }
    _apps = arrM;
}
return _apps;

}

pragma mark ----------------------

pragma mark UITableViewDatasource

-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.apps.count;
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *ID = @"app";

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

//2.设置cell的数据
//2.1 拿到该行cell对应的数据
XMGAPP *appM = self.apps[indexPath.row];

//2.2 设置标题
cell.textLabel.text = appM.name;

//2.3 设置子标题
cell.detailTextLabel.text = appM.download;

//2.4 设置图标
NSURL *url = [NSURL URLWithString:appM.icon];
NSData *imageData = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:imageData];
cell.imageView.image = image;
NSLog(@"%zd-----",indexPath.row);

//3.返回cell
return cell;

}

假设我们直接这样去下载图片,你可以尝试写个小个demo,它会存在两个问题:
①UI很不流畅
②图片重复下载

①当UI不流畅,我们应该怎么解决呢?
可以开子线程下载图片,然后回到主线程刷新UI。

②图片重复下载,这个又该如何处理?
先把之前已经下载过的图片保存起来,我们可以想到用一个字典讲它存储起来。这个时候又会存在存在一个问题,如果只是单纯用一个字典去存储,当App退出的时候,下次进入还是重新发起请求,因为你只是将图片进行内存缓存,当App退出的时候,就会释放掉。这个时候,我们不仅仅要做内存缓存,还要做磁盘缓存。

这个时候我们完善后的代码是这样的

import "ViewController.h"

import "XMGAPP.h"

@interface ViewController ()
/** tableView的数据源 /
@property (nonatomic, strong) NSArray apps;
/
内存缓存 /
@property (nonatomic, strong) NSMutableDictionary images;
/
队列 /
@property (nonatomic, strong) NSOperationQueue queue;
/
操作缓存 */
@property (nonatomic, strong) NSMutableDictionary *operations;
@end

@implementation ViewController

pragma mark ----------------------

pragma mark lazy loading

-(NSOperationQueue *)queue
{
if (_queue == nil) {
_queue = [[NSOperationQueue alloc]init];
//设置最大并发数
_queue.maxConcurrentOperationCount = 5;
}
return _queue;
}
-(NSMutableDictionary *)images
{
if (_images == nil) {
_images = [NSMutableDictionary dictionary];
}
return _images;
}
-(NSArray *)apps
{
if (_apps == nil) {

    //字典数组
    NSArray *arrayM = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle]pathForResource:@"apps.plist" ofType:nil]];
    
    //字典数组---->模型数组
    NSMutableArray *arrM = [NSMutableArray array];
    for (NSDictionary *dict in arrayM) {
        [arrM addObject:[XMGAPP appWithDict:dict]];
    }
    _apps = arrM;
}
return _apps;

}

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

pragma mark ----------------------

pragma mark UITableViewDatasource

-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.apps.count;
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *ID = @"app";

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

//2.设置cell的数据
//2.1 拿到该行cell对应的数据
XMGAPP *appM = self.apps[indexPath.row];

//2.2 设置标题
cell.textLabel.text = appM.name;

//2.3 设置子标题
cell.detailTextLabel.text = appM.download;

//2.4 设置图标

//先去查看内存缓存中该图片时候已经存在,如果存在那么久直接拿来用,否则去检查磁盘缓存
//如果有磁盘缓存,那么保存一份到内存,设置图片,否则就直接下载
//1)没有下载过
//2)重新打开程序

UIImage *image = [self.images objectForKey:appM.icon];
if (image) {
    cell.imageView.image = image;
    NSLog(@"%zd处的图片使用了内存缓存中的图片",indexPath.row) ;
}else
{
    //保存图片到沙盒缓存
    NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    //获得图片的名称,不能包含/
    NSString *fileName = [appM.icon lastPathComponent];
    //拼接图片的全路径
    NSString *fullPath = [caches stringByAppendingPathComponent:fileName];
    
    
    //检查磁盘缓存
    NSData *imageData = [NSData dataWithContentsOfFile:fullPath];
    //废除
    imageData = nil;
    
    if (imageData) {
        UIImage *image = [UIImage imageWithData:imageData];
        cell.imageView.image = image;
        
        NSLog(@"%zd处的图片使用了磁盘缓存中的图片",indexPath.row) ;
        //把图片保存到内存缓存
        [self.images setObject:image forKey:appM.icon];

// NSLog(@"%@",fullPath);
}else
{
//检查该图片时候正在下载,如果是那么久什么都捕捉,否则再添加下载任务
NSBlockOperation *download = [self.operations objectForKey:appM.icon];
if (download) {

        }else
        {
            
            //先清空cell原来的图片
            cell.imageView.image = [UIImage imageNamed:@"Snip20160221_306"];
            
            download = [NSBlockOperation blockOperationWithBlock:^{
                NSURL *url = [NSURL URLWithString:appM.icon];
                NSData *imageData = [NSData dataWithContentsOfURL:url];
                UIImage *image = [UIImage imageWithData:imageData];
                
                 NSLog(@"%zd--下载---",indexPath.row);
                
                //容错处理
                if (image == nil) {
                    [self.operations removeObjectForKey:appM.icon];
                    return ;
                }
                //演示网速慢的情况
                //[NSThread sleepForTimeInterval:3.0];
            
                //把图片保存到内存缓存
                [self.images setObject:image forKey:appM.icon];
                
                //NSLog(@"Download---%@",[NSThread currentThread]);
                //线程间通信
                [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                    
                    //cell.imageView.image = image;
                    //刷新一行
                    [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
                    //NSLog(@"UI---%@",[NSThread currentThread]);
                }];
                
                
                //写数据到沙盒
                [imageData writeToFile:fullPath atomically:YES];
               
                //移除图片的下载操作
                [self.operations removeObjectForKey:appM.icon];
                
            }];
            
            //添加操作到操作缓存中
            [self.operations setObject:download forKey:appM.icon];
            
            //添加操作到队列中
            [self.queue addOperation:download];
        }
        
    }
}

//3.返回cell
return cell;

}

-(void)didReceiveMemoryWarning
{
[self.images removeAllObjects];

//取消队列中所有的操作
[self.queue cancelAllOperations];

}

//1.UI很不流畅 --- > 开子线程下载图片
//2.图片重复下载 ---> 先把之前已经下载的图片保存起来(字典)
//内存缓存--->磁盘缓存

//3.图片不会刷新--->刷新某行
//4.图片重复下载(图片下载需要时间,当图片还未完全下载之前,又要重新显示该图片)
//5.数据错乱 ---设置占位图片

/*
Documents:会备份,不允许
Libray
Preferences:偏好设置 保存账号
caches:缓存文件
tmp:临时路径(随时会被删除)
*/

这里小编认为有两个问题比较重要。
①下载这个过程是很重要,一些细心的面试官会问你多图片下载是怎么实现的?

先判断放Operation的数组里,是否存在下载当前的任务,如果存在了就什么都不做,如果没有再初始化一个Operation任务,然后放到数组里。并且放到操作队列中去执行下载任务。当图片下载完了,把该Operation任务从该数组移除。

②面试官会问你,图片是如何进行内存缓存的?将图片存在字典里面,它的value是UIImage,但是key是什么呢?
这个key是图片的url,比如一张图片的url是http://p16.qhimg.com/dr/48_48_/438ae9d2fbb.png,那么key一定是从这个url字符串去做文章,因为沙盒路径中/表现文件的层级关系,所以我们可以截图url字符串的后部分作为key就可以了。

这个多图片的实现方式,差不多就是SDwebImage这个框架的前世。
那么SDwebImage的今生已经很成熟,大家也用得很多,但是很多人可能只用过它来设置UIImageView,其实SDwebImage不仅可以设置UIImageView,还可以设置UIButton,并且可以播放.gif图片,或者你想单独拿到UIIamge也是可以的。

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self download];
}

//1.下载图片且需要获取下载进度
//内存缓存&磁盘缓存
-(void)download
{
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://img4.duitang.com/uploads/blog/201310/18/20131018213446_smUw4.thumb.600_0.jpeg"] placeholderImage:[UIImage imageNamed:@"Snip20160221_306"] options:SDWebImageCacheMemoryOnly | SDWebImageProgressiveDownload progress:^(NSInteger receivedSize, NSInteger expectedSize) {

} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
    
    switch (cacheType) {
        case SDImageCacheTypeNone:
            NSLog(@"直接下载");
            break;
        case SDImageCacheTypeDisk:
            NSLog(@"磁盘缓存");
            break;
        case SDImageCacheTypeMemory:
            NSLog(@"内存缓存");
            break;
        default:
            break;
    }
}];

NSLog(@"%@",[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]);

}

//2.只需要简单获得一张图片,不设置
//内存缓存&磁盘缓存
-(void)download2
{
[[SDWebImageManager sharedManager] downloadImageWithURL:[NSURL URLWithString:@"http://img4.duitang.com/uploads/blog/201310/18/20131018213446_smUw4.thumb.600_0.jpeg"] options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {
NSLog(@"%f",1.0 * receivedSize / expectedSize);

} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
    
    //得到图片
    self.imageView.image = image;
}];

}

//3.不需要任何的缓存处理
//没有做任何缓存处理|
-(void)download3
{
//data:图片的二进制数据
[[SDWebImageDownloader sharedDownloader] downloadImageWithURL:[NSURL URLWithString:@"http://img4.duitang.com/uploads/blog/201310/18/20131018213446_smUw4.thumb.600_0.jpeg"] options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {

} completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
    
    [[NSOperationQueue mainQueue]addOperationWithBlock:^{
         self.imageView.image = image;
    }];
   
}];

}

//4.播放Gif图片
-(void)gif
{
NSLog(@"%s",func);
//self.imageView.image = [UIImage imageNamed:@"39e805d5ad6eddc4f80259d23bdbb6fd536633ca"];

UIImage *image = [UIImage sd_animatedGIFNamed:@"麻雀"];
self.imageView.image = image;

}

-(void)type
{
NSData *imageData = [NSData dataWithContentsOfFile:@"/Users/xiaomage/Desktop/Snip20160221_306.png"];
NSString *typeStr = [NSData sd_contentTypeForImageData:imageData];
NSLog(@"%@",typeStr);
}

SDwebImage中有一个关于options枚举,这个枚举功能很强大,假设后台给你URL图片地址,这个时候你将它缓存起来,但是URL的图片地址已经把图片更换了,这个启动APP你显示的还是以前那张图片,这个你可以设置SDwebImage中options的枚举值为SDWebImageRefreshCached,它可以刷新本地缓存。这一点很强大。

SDwebImage跟普通多图片下载不同之处?
①这里SDwebImage跟上面多图片下载有一点不同的是,SDwebImage中的沙盒缓存图片的命名方式为对该图片的URL进行MD5加密,然后得到一个新的字符串设置为key的。
②多图片下载的时候我们用的NSDictionary字典进行缓存的,但是SDwebImage使用了NSCache类这个类进行缓存,NSCache这个类有一个属性totalCostLimit可以设置总成本数,如果总成本数是5 ,如果发现存的数据超过中成本那么会自动回收之前的对象,不需要我们手动移除。自动管理内存缓存数量和内存大小。

import "ViewController.h"

@interface ViewController ()
/** 注释 */
@property (nonatomic, strong) NSCache *cache;
@end

@implementation ViewController

-(NSCache *)cache
{
if (_cache == nil) {
_cache = [[NSCache alloc]init];
_cache.totalCostLimit = 5;//总成本数是5 ,如果发现存的数据超过中成本那么会自动回收之前的对象
_cache.delegate = self;
}
return _cache;
}

//存数据

  • (IBAction)addBtnClick:(id)sender
    {
    //NSCache的Key只是对对象进行Strong引用,不是拷贝(和可变字典的区别)
    for (NSInteger i = 0; i<10; i++) {
    NSData *data = [NSData dataWithContentsOfFile:@"/Users/xiaomage/Desktop/Snip20160221_38.png"];

      //cost:成本
      [self.cache setObject:data forKey:@(i) cost:1];
      NSLog(@"存数据%zd",i);
    

    }
    }

//取数据

  • (IBAction)checkBtnClick:(id)sender
    {
    NSLog(@"+++++++++++++++");
    for (NSInteger i = 0; i<10; i++) {
    NSData *data = [self.cache objectForKey:@(i)];
    if (data) {
    NSLog(@"取出数据%zd",i);
    }
    }
    }

//删除数据

  • (IBAction)removeBtnClick:(id)sender
    {
    [self.cache removeAllObjects];
    }

pragma mark ----------------------

pragma mark NSCacheDelegate

//即将回收对象的时候调用该方法
-(void)cache:(NSCache *)cache willEvictObject:(id)obj
{
NSLog(@"回收%zd",[obj length]);
}

你可能感兴趣的:(iOS开发之SDwebImage的前世今生)