美文网首页
SDWebimage的问题 底层实现原理

SDWebimage的问题 底层实现原理

作者: 瞌睡树懒 | 来源:发表于2019-03-27 22:37 被阅读0次

    二、SDWebImage内部实现过程

    入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。

    进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:,交给 SDImageCache 从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:.

    先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。

    SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展示图片。

    如果内存缓存中没有,生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存。

    根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。

    如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。

    如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。

    共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。

    图片下载由 NSURLConnection 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。

    connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。

    connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。

    图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。

    在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。

    imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。

    通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。

    将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。

    SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。

    SDWI 也提供了 UIButton+WebCache 和 MKAnnotationView+WebCache,方便使用。

    SDWebImagePrefetcher 可以预先下载图片,方便后续使用。

    从上面流程可以看出,当你调用setImageWithURL:方法的时候,他会自动去给你干这么多事,当你需要在某一具体时刻做事情的时候,你可以覆盖这些方法。比如在下载某个图片的过程中要响应一个事件,就覆盖这个方法:

    其实有些框架的实现原理,并没有想象中那么难,思想也很简单,主要是更新第三方框架的作者对自己写的代码,进行了多层封装,使代码的可读性降低,也就使得框架看起来比较难.以下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.如果磁盘缓存中无图片,那么再去下载,并且把下载完的图片保存到内存缓存和磁盘缓存
    3.图片不显示
    原因: 图片的显示操作是异步执行的,也就是说需要重新刷新该行的cell
    4.图片重复下载问题(二)
    这种情况需要模拟一定的情况,例如网络信号不好(网络信号不好可以用耗时操作代替),造成这种情况的原因,就是在网络情况不好的情况下,用户重复滑动.就会重复的发送下载图片的请求,最终造成重复下载.
    解决办法: 我在这里用的操作队列,对应的解决办法,就是把对应的事件以字典的方式存储到内存,放置重复发送下载图片的请求
    5.图片显示错乱的问题
    由于cell的重用导致,用户下拉或者上拉,当网络不好的情况,该cell的图片还没有被加载,但是对应的cell已经被显示,就会显示cell被重用之前的数据,造成数据混乱
    解决办法: 设置每个cell中image为nil或者设置默认图片.
    6.中间还要加一些容错处理.
    例如: 若是服务器返回的url是错误的,就会造成程序闪退,需要做处理
    7.内存缓存的处理
    模型中的代码

    #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
    

    上边是在控制器实现,以下是SD业务层的缓存操作

    业务层:
    缓存&&磁盘操作(SDImageCache)
    - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock { if (!key) { if (doneBlock) {
                doneBlock(nil, nil, SDImageCacheTypeNone);
            } return nil;
        } // First check the in-memory cache... //搜索磁盘缓存 UIImage *image = [self imageFromMemoryCacheForKey:key]; if (image) { NSData *diskData = nil; if (image.images) {
                diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            } if (doneBlock) {
                doneBlock(image, diskData, SDImageCacheTypeMemory);
            } return nil;
        } NSOperation *operation = [NSOperation new]; dispatch_async(self.ioQueue, ^{ if (operation.isCancelled) { // do not call the completion if cancelled return;
            } @autoreleasepool { //搜索硬盘 NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key]; UIImage *diskImage = [self diskImageForKey:key]; //缓存到内存、默认为YES if (diskImage && self.config.shouldCacheImagesInMemory) { NSUInteger cost = SDCacheCostForImage(diskImage); //使用NSChache缓存。 [self.memCache setObject:diskImage forKey:key cost:cost];
                } if (doneBlock) { dispatch_async(dispatch_get_main_queue(), ^{
                        doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                    });
                }
            }
        }); return operation;
    } //查询缓存 - (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key { //self.memCache  为NSCache实例 return [self.memCache objectForKey:key];
    } //查询磁盘 - (nullable UIImage *)diskImageForKey:(nullable NSString *)key { NSData *data = [self diskImageDataBySearchingAllPathsForKey:key]; if (data) { //图片解码、调整方向 UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:data]; //调整图片缩放比例 @2x/@3x image = [self scaledImageForKey:key image:image]; //压缩图片 if (self.config.shouldDecompressImages) {
                image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
            } return image;
        } else { return nil;
        }
    } //写入缓存 && 磁盘 - (void)storeImage:(nullable UIImage *)image
             imageData:(nullable NSData *)imageData
                forKey:(nullable NSString *)key
                toDisk:(BOOL)toDisk
            completion:(nullable SDWebImageNoParamsBlock)completionBlock { if (!image || !key) { if (completionBlock) {
                completionBlock();
            } return;
        } // if memory cache is enabled if (self.config.shouldCacheImagesInMemory) { //写入缓存 NSUInteger cost = SDCacheCostForImage(image);
            [self.memCache setObject:image forKey:key cost:cost];
        } if (toDisk) { //写入磁盘 dispatch_async(self.ioQueue, ^{ @autoreleasepool { NSData *data = imageData; if (!data && image) { // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format SDImageFormat format; if (SDCGImageRefContainsAlpha(image.CGImage)) {
                            format = SDImageFormatPNG;
                        } else {
                            format = SDImageFormatJPEG;
                        }
                        data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format];
                    }
                    [self storeImageDataToDisk:data forKey:key];
                } if (completionBlock) { dispatch_async(dispatch_get_main_queue(), ^{
                        completionBlock();
                    });
                }
            });
        } else { if (completionBlock) {
                completionBlock();
            }
        }
    } //正式写入磁盘 - (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key { if (!imageData || !key) { return;
        }
         
        [self checkIfQueueIsIOQueue]; //如果文件中不存在磁盘缓存路径 则创建 if (![_fileManager fileExistsAtPath:_diskCachePath]) {
            [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
        } // get cache Path for image key  得到该key的缓存路径 NSString *cachePathForKey = [self defaultCachePathForKey:key]; // transform to NSUrl  将缓存路径转化为url NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey]; //将imageData存储起来 [_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil]; // disable iCloud backup  如果调用者关闭icloud 关闭iCloud备份 if (self.config.shouldDisableiCloud) {
            [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
        }
    }
    

    SDWebImage 的内存警告是如何处理的!
    利用通知中心观察

    • UIApplicationDidReceiveMemoryWarningNotification 接收到内存警告的通知
      执行 clearMemory 方法,清理内存缓存!
    • UIApplicationWillTerminateNotification 接收到应用程序将要终止通知
      执行 cleanDisk 方法,清理磁盘缓存!
    • UIApplicationDidEnterBackgroundNotification 接收到应用程序进入后台通知
      执行 backgroundCleanDisk 方法,后台清理磁盘!
      通过以上通知监听,能够保证缓存文件的大小始终在控制范围之内!
      clearDisk 清空磁盘缓存,将所有缓存目录中的文件,全部删除!实际工作,将缓存目录直接删除,再次创建一个同名空目录!

    SDWebImage 缓存图片的名称是怎么确定的!
    md5
    如果单纯使用 文件名保存,重名的几率很高!
    使用 MD5 的散列函数!对完整的 URL 进行 md5,结果是一个 32 个字符长度的字符串!
    SDWebImage是如何区分不同格式的图像的
    根据图像数据第一个字节来判断的!
    PNG:0x89 压缩比没有JPG高,但是无损压缩,解压缩性能高,苹果推荐的图像格式!
    JPG:0xff 压缩比最高的一种图片格式,有损压缩!最多使用的场景,照相机!解压缩的性能不好!
    GIF:0x47 序列桢动图,特点:只支持256种颜色!最流行的时候在1998~1999,有专利的!

    相关文章

      网友评论

          本文标题:SDWebimage的问题 底层实现原理

          本文链接:https://www.haomeiwen.com/subject/nakxbqtx.html