请你说说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<UITouch *> *)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 ()<NSCacheDelegate>
/** 注释 */
@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]);
}