iOS-SDWebimage底层实现原理
其实有些框架的实现原理,并没有想象中那么难,思想也很简单,主要是更新第三方框架的作者对自己写的代码,进行了多层封装,使代码的可读性降低,也就使得框架看起来比较难.我来实现以下SDWebimage的的曾实现.
实现过程中可能遇到的问题:
1.UI卡顿: 当界面中需要下载多张图片的时候,由于图片下载是耗时操作,会短暂的占据着主线程的执行,也就会是UI界面看起来卡顿.
解决办法: 需要把耗时操作放在子线程中执行(若是对多线程不了解,我之前的博客写过关于多线程的知识)
NSBlockOperation *download = [self.operations objectForKey:app.icon]; if (download) { NSLog(@"该图片正在下载,请稍等"); }else { //封装下载图片的操作 download = [NSBlockOperation blockOperationWithBlock:^{ NSURL *url = [NSURL URLWithString:app.icon]; //耗时操作,模拟网速慢的情况 for (NSInteger i =0; i <1000000000; i++) { } NSData *data = [NSData dataWithContentsOfURL:url]; UIImage *image = [UIImage imageWithData:data]; NSLog(@"下载第%zd行cell对应的图片",indexPath.row); //容错处理 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.imageView.image = image; //刷新 //[tableView reloadData]; [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationMiddle]; }]; }]; //把操作保存到操作缓存中 [self.operations setObject:download forKey:app.icon]; //把操作添加到队列 [self.queue addOperation:download]; }
2.重复加载问题(一)
在下载图片的地方,打印相关的下载的图片的信息,你会发现图片会重复下载
解决办法: 1.内存缓存处理 2.二级缓存处理
内存的大小毕竟有限的,在开发中我们一般采用二级缓存处理.
二级缓存处理过程:
1.在显示图片之前,先检查内存缓存中时候有该图片
2.如果内存缓存中有图片,那么就直接使用,不下载
3.如果内存缓存中无图片,那么再检查是否有磁盘缓存
4.如果磁盘缓存中有图片,那么直接使用,还需要保存一份到内存缓存中(方便下一次使用)
5.如果磁盘缓存中无图片,那么再去下载,并且把下载完的图片保存到内存缓存和磁盘缓存
//保存图片到内存缓存中 [self.images setObject:image forKey:app.icon]; //保存数据到沙盒(磁盘缓存) [data writeToFile:fullPath atomically:YES];
3.图片不显示
原因: 图片的显示操作是异步执行的,也就是说需要重新刷新该行的cell
//线程间通信 主线程中设置图片 [[NSOperationQueue mainQueue] addOperationWithBlock:^{ //cell.imageView.image = image; //刷新 //[tableView reloadData]; [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationMiddle]; }];
4.图片重复下载问题(二)
这种情况需要模拟一定的情况,例如网络信号不好(网络信号不好可以用耗时操作代替),造成这种情况的原因,就是在网络情况不好的情况下,用户重复滑动.就会重复的发送下载图片的请求,最终造成重复下载.
解决办法: 我在这里用的操作队列,对应的解决办法,就是把对应的事件以字典的方式存储到内存,放置重复发送下载图片的请求
//把操作保存到操作缓存中 [self.operations setObject:download forKey:app.icon];
5.图片显示错乱的问题
由于cell的重用导致,用户下拉或者上拉,当网络不好的情况,该cell的图片还没有被加载,但是对应的cell已经被显示,就会显示cell被重用之前的数据,造成数据混乱
解决办法: 设置每个cell中image为nil或者设置默认图片.
6.中间还要加一些容错处理.
例如: 若是服务器返回的url是错误的,就会造成程序闪退,需要做处理
NSData *data = [NSData dataWithContentsOfURL:url]; UIImage *image = [UIImage imageWithData:data]; NSLog(@"下载第%zd行cell对应的图片",indexPath.row); //容错处理 if (image == nil) { [self.operations removeObjectForKey:app.icon]; return ; }
7.内存缓存的处理
-(void)didReceiveMemoryWarning { //清空内存缓存 [self.images removeAllObjects]; //取消队列中所有的操作 [self.queue cancelAllOperations]; }
完整的代码如下:
模型中的代码:
#import <Foundation/Foundation.h> @interface BOAppModel : NSObject @property (nonatomic, strong) NSString *name; @property (nonatomic, strong) NSString *icon; @property (nonatomic, strong) NSString *download; - (instancetype)initWithDict:(NSDictionary *)dict; + (instancetype)appModelWithDict:(NSDictionary *)dict; @end
#import "BOAppModel.h" @implementation BOAppModel - (instancetype)initWithDict:(NSDictionary *)dict{ if (self = [super init]) { [self setValuesForKeysWithDictionary:dict]; } return self; } + (instancetype)appModelWithDict:(NSDictionary *)dict{ return [[BOAppModel alloc] initWithDict:dict]; } - (void)setValue:(id)value forUndefinedKey:(NSString *)key{ } @end
控制器空代码如下:
#import "ViewController.h" #import "BOAppModel.h" @interface ViewController () @property (nonatomic, strong) NSArray *apps; @property (nonatomic, strong) NSCache *images; @property (nonatomic, strong) NSMutableDictionary *operations; @property (nonatomic, strong) NSOperationQueue *queue; @end @implementation ViewController #pragma mark ------------------ #pragma mark lazy loading -(NSOperationQueue *)queue { if (_queue == nil) { _queue = [[NSOperationQueue alloc]init]; } return _queue; } -(NSCache *)images { if (_images == nil) { _images = [[NSCache alloc] init]; } return _images; } -(NSMutableDictionary *)operations { if (_operations == nil) { _operations = [NSMutableDictionary dictionary]; } return _operations; } -(NSArray *)apps { if (_apps == nil) { //加载plist文件中的数据 NSArray *arrayM = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil]]; //字典转模型(字典数组-->模型数组) NSMutableArray *arr = [NSMutableArray array]; for (NSDictionary *dict in arrayM) { [arr addObject: [BOAppModel appModelWithDict:dict]]; } _apps = arr; } 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 { //01 创建cell static NSString *ID = @"app"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID]; //02 设置cell //001 拿到该行cell对应的数据 XMGApp *app = self.apps[indexPath.row]; //002 设置标题 cell.textLabel.text = app.name; //003 设置子标题 cell.detailTextLabel.text = [NSString stringWithFormat:@"%@",app.download]; //004 设置图片 /* 内存缓存处理: [1]在显示图片之前,先检查缓存中是否有该图片(是否已经下载过) [2]如果缓存中有图片,那么就直接使用,不下载 [3]如果缓存中无图片,那么再去下载,并且把下载完的图片保存到内存缓存 */ /* 二级(内存-磁盘)缓存处理: [1]在显示图片之前,先检查内存缓存中是否有该图片 [2]如果内存缓存中有图片,那么就直接使用,不下载 [3]如果内存缓存中无图片,那么再检查是否有磁盘缓存 [4]如果磁盘缓存中有图片,那么直接使用,还需要保存一份到内存缓存中(方便下一次) [5]如果磁盘缓存中无图片,那么再去下载,并且把下载完的图片保存到内存缓存和磁盘缓存 */ //检查内存缓存 UIImage *image = [self.images objectForKey:app.icon]; if (image) { cell.imageView.image = image; NSLog(@"第%zd行cell对应的图片从内存缓存中获取",indexPath.row); }else { //文件名称 NSString *fileName = [app.icon lastPathComponent]; //获得缓存路径 NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; //拼接文件的全路径 NSString *fullPath = [cache stringByAppendingPathComponent:fileName]; //检查磁盘缓存 NSData *data = [NSData dataWithContentsOfFile:fullPath]; data = nil; if (data) { //如果有磁盘缓存,那么久直接使用 UIImage *image = [UIImage imageWithData:data]; cell.imageView.image = image; //还需要保存一份到内存缓存中 [self.images setObject:image forKey:app.icon]; NSLog(@"第%zd行cell对应的图片使用了磁盘缓存--%@",indexPath.row,fullPath); }else { //开子线程下载图片 /* 对操作进行缓存处理 如果没有内存缓存也没有磁盘缓存,则 001 先检查该图片的下载操作是否已经存在(该图片是否正在下载) 002 如果下载操作已经存在,那么就等待即可 003 如果下载操作不存在,那么再去下载 */ //清空cell的图片 //cell.imageView.image = nil; //设置占位图片 cell.imageView.image = [UIImage imageNamed:@"Snip20161203_14"]; NSBlockOperation *download = [self.operations objectForKey:app.icon]; if (download) { NSLog(@"该图片正在下载,请稍等"); }else { //封装下载图片的操作 download = [NSBlockOperation blockOperationWithBlock:^{ NSURL *url = [NSURL URLWithString:app.icon]; //耗时操作,模拟网速慢的情况 for (NSInteger i =0; i <1000000000; i++) { } NSData *data = [NSData dataWithContentsOfURL:url]; UIImage *image = [UIImage imageWithData:data]; NSLog(@"下载第%zd行cell对应的图片",indexPath.row); //容错处理 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.imageView.image = image; //刷新 //[tableView reloadData]; [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationMiddle]; }]; }]; //把操作保存到操作缓存中 [self.operations setObject:download forKey:app.icon]; //把操作添加到队列 [self.queue addOperation:download]; } } } //03 返回cell return cell; } -(void)didReceiveMemoryWarning { //清空内存缓存 [self.images removeAllObjects]; //取消队列中所有的操作 [self.queue cancelAllOperations]; } /* 001 UI卡顿 ---> 开子线程下载图片 002 重复下载 --->内存缓存 -->磁盘缓存 003 图片不显示 --->图片的显示操作是异步执行的 004 新的重复下载 005 图片显示错乱的问题-->cell的重用 006 自己寻找 */ /* 沙盒文件: Documents 官方规定不能缓存处理 Library cache 缓存文件 偏好设置 Tmp 临时文件存储路径(随时可能被删除) */ @end