iOS图片加载策略的简单实现

百家 作者:iOS开发 2018-01-04 11:32:15

点击上方“iOS开发”,选择“置顶公众号”

关键时刻,第一时间送达!


今天和大家一起来讨论如何进行iOS图片加载策略的简单实现,有疏忽的地方,还望各位不吝赐教。


一、不自量力的说明


对于iOS图片加载策略的实现,相信大家和我一样更多的还是借助于第三方,我在此班门弄斧的意义是应付一些公司的面试,尝试以一种简单的方式去实现图片加载策略,借此也说明一些其他方面的知识。在此我将使用一个TableView的例子进行说明,采用的是MVC的设计模式。如果采用的是Swift编写,还请给位大神自行转换。


二、逻辑叙述


这个逻辑的流程是我随手画的,只做参考。


逻辑实现.png


三、实现过程 -- 以多图下载为例


1、先上一个简单粗暴的实现。


    // cellForRowAtIndexPath:方法中的实现过程

    // 1、设置cell的重用标识

    static NSString *cellID = @"app";

    // 2、创建cell

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];

    // 3、设置cell的数据 AppItem是模型

    AppItem *item = self.apps[indexPath.row];

    // 4、设置标题

    cell.textLabel.text = item.name;

    // 5、设置子标题

    cell.detailTextLabel.text = item.download;

    // 6、设置图标

    NSURL *url = [NSURL URLWithString:item.icon];

    NSData *imageData = [NSData dataWithContentsOfURL:url];

    UIImage *image = [UIImage imageWithData:imageData];

    cell.imageView.image = image;

    NSLog(@"%zd----",indexPath.row);


2、以上的方式实现的问题


  • 图片重复下载,通过添加的打印可以很明显的看出来,解决方式是把之前下载好的图片保存起来,因为图片和文字要对应,所以采用字典的方式进行存储。


1、添加内存缓存解决图片重复下载问题


    // 针对图片重复下载的问题 

    // 1、创建一个NSMutableDictionary属性来保存图片

    /** 内存缓存 */

    @property (nonatomic, strong) NSMutableDictionary *images;

     // 2、懒加载实现

    - (NSMutableDictionary *)images{

        if (!_images) {

            _images = [NSMutableDictionary dictionary];

        }

        return _images;

    }

    // 3、设置图标改造

    // 先去查看内存缓存中该图片有没有被下载过,如果有直接拿来用,如果没有再下载

    // 设置图片,否则直接下载

    UIImage *image = [self.images objectForKey:item.icon];

    // 如果有值直接拿来用

    if(image){

        cell.imageView.image = image;

        NSLog(@"使用了内存缓存中的图片%zd----",indexPath.row);

    }else{

        NSURL *url = [NSURL URLWithString:item.icon];

        NSData *imageData = [NSData dataWithContentsOfURL:url];

        UIImage *image = [UIImage imageWithData:imageData];

        cell.imageView.image = image;

        // 保存图片到内存缓存 用图片的URL作为图片的key 保证key唯一

        [self.images setObject:image forKey:item.icon];

        NSLog(@"下载了图片%zd----",indexPath.row);

    }


2、添加内存缓存存在问题,需要用磁盘缓存(沙盒缓存)来补充


   /*

    * 两种情况 图片没有下载和应用关闭都要考虑 

    * 只是添加内存缓存在程序退出的时候内存缓存会被释放,接着优化

    */

    /*  沙盒缓存的相关概念

     document :会备份,苹果官方不允许将缓存放到这里,上架被拒绝

     library:

        preference:偏好设置,存放账号密码等数据

        cache: 保存缓存文件,不会被备份

     temp:临时路径(随时有可能被删除)

     */

    // 先去查看内存缓存中该图片有没有被下载过,如果有直接拿来用,如果没有就去检查磁盘缓存,如果没有磁盘缓存,就保存一份到内存,设置图片,否则就直接下载

    UIImage *image = [self.images objectForKey:item.icon];

    // 如果有值直接拿来用

    if(image){


        cell.imageView.image = image;

        NSLog(@"使用了内存缓存中的图片----%zd",indexPath.row);


    }else{

        

        // 沙盒缓存路径获取(磁盘缓存)

        NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];

        // 获得图片的名称

        NSString *imageName = [item.icon lastPathComponent];

        // 拼接全路径

        NSString *fullPath = [cachePath stringByAppendingPathComponent:imageName];

        // 检查磁盘缓存

        NSData *imageData = [NSData dataWithContentsOfFile:fullPath];

        if (imageData) {

            // 设置图标

            UIImage *image = [UIImage imageWithData:imageData];

            cell.imageView.image = image;

            NSLog(@"沙盒存储----%zd",indexPath.row);

            // 保存图片到内存缓存

            [self.images setObject:image forKey:item.icon];  


        }else{

            

            NSURL *url = [NSURL URLWithString:item.icon];

            NSData *imageData = [NSData dataWithContentsOfURL:url];

            UIImage *image = [UIImage imageWithData:imageData];

            cell.imageView.image = image;

            // 保存图片到内存缓存 用图片的URL作为图片的key 保证key唯一

            [self.images setObject:image forKey:item.icon];

            // 保存图片到沙盒缓存(磁盘缓存)

            [imageData writeToFile:fullPath atomically:YES];

            NSLog(@"下载了图片----%zd",indexPath.row);


        }  


    }


  • UI不流畅,因为下载图片操作和刷新UI的操作都是在主线程中操作的,解决方法把下载操作放到子线程中进行操作。


   /*

    * 改造下载图片的部分,下载图片放到子线程中去做,刷新UI放在主线程中做。

    */

    /** 并发队列  使用NSOperation为了防止重复创建队列,设置全局属性*/

    @property (nonatomic, strong) NSOperationQueue *queue;

    // 并发队列懒加载

    - (NSOperationQueue *)queue{

        if (!_queue) {

            _queue = [[NSOperationQueue alloc] init];

            // 设置最大并发数

            _queue.maxConcurrentOperationCount = 5;

        }

    

        return _queue;

    }

    // 设置图标的位置修改如下:

    // 先去查看内存缓存中该图片有没有被下载过,如果有直接拿来用,如果没有就去检查磁盘缓存,如果没有磁盘缓存,就保存一份到内存,设置图片,否则就直接下载。

    UIImage *image = [self.images objectForKey:item.icon];

    // 如果有值直接拿来直接使用

    if(image){


        cell.imageView.image = image;

        NSLog(@"使用了内存缓存中的图片----%zd",indexPath.row);


    }else{

        

        // 沙盒缓存路径获取(磁盘缓存)

        NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];

        // 获得图片的名称

        NSString *imageName = [item.icon lastPathComponent];

        // 拼接全路径

        NSString *fullPath = [cachePath stringByAppendingPathComponent:imageName];

        // 检查磁盘缓存

        NSData *imageData = [NSData dataWithContentsOfFile:fullPath];

        if (imageData) {

            // 设置图标

            UIImage *image = [UIImage imageWithData:imageData];

            cell.imageView.image = image;

            NSLog(@"沙盒存储----%zd",indexPath.row);

            // 保存图片到内存缓存

            [self.images setObject:image forKey:item.icon];  


        }else{

            // 创建操作

            NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{

                NSURL *url = [NSURL URLWithString:item.icon];

                NSData *imageData = [NSData dataWithContentsOfURL:url];

                UIImage *image = [UIImage imageWithData:imageData];

                NSLog(@"下载------%@",[NSThread currentThread]);

                [[NSOperationQueue mainQueue] addOperationWithBlock:^{

                    cell.imageView.image = image;

                    NSLog(@"UI------%@",[NSThread currentThread]);

                }];

                // 保存图片到内存缓存 用图片的URL作为图片的key 保证key唯一

                [self.images setObject:image forKey:item.icon];

                // 保存图片到沙盒缓存(磁盘缓存)

                [imageData writeToFile:fullPath atomically:YES];

                 NSLog(@"下载了图片%zd----",indexPath.row);

            }];

            // 添加下载操作到并发队列中

            [self.queue addOperation:blockOperation];


        }  

    }


3、进行了2以后产生的新问题


  • UI不会自动刷新,当我拖动的时候才会刷新页面,解决方式要使用代码手动刷新。因为下载操作是异步的,会先把没有图标的cell返回,此时因为图标的frame为0,所以之后即使图片下载下来,frame变为其他额尺寸,frame还是会一直为0,所以不会显示。如果我手动刷新,系统会从新走一遍cellForRowAtIndexPath:方法,到设置图标这一步时,会直接把内存中存在的图片(此时图片是有frame的)直接设置到cell中会显示。


   /*

    * 改造下载图片的部分,下载图片放到子线程中去做,刷新UI放在主线程中做。

    */

    // 设置图标的位置修改如下:

    // 先去查看内存缓存中该图片有没有被下载过,如果有直接拿来用,如果没有就去检查磁盘缓存,如果没有磁盘缓存,就保存一份到内存,设置图片,否则就直接下载。

    UIImage *image = [self.images objectForKey:item.icon];

    // 如果有值直接拿来直接使用

    if(image){


        cell.imageView.image = image;

        NSLog(@"使用了内存缓存中的图片----%zd",indexPath.row);


    }else{

        

        // 沙盒缓存路径获取(磁盘缓存)

        NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];

        // 获得图片的名称

        NSString *imageName = [item.icon lastPathComponent];

        // 拼接全路径

        NSString *fullPath = [cachePath stringByAppendingPathComponent:imageName];

        // 检查磁盘缓存

        NSData *imageData = [NSData dataWithContentsOfFile:fullPath];

        if (imageData) {

            // 设置图标

            UIImage *image = [UIImage imageWithData:imageData];

            cell.imageView.image = image;

            NSLog(@"沙盒存储----%zd",indexPath.row);

            // 保存图片到内存缓存

            [self.images setObject:image forKey:item.icon];  


        }else{

            // 创建下载操作

            NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{

                NSURL *url = [NSURL URLWithString:item.icon];

                NSData *imageData = [NSData dataWithContentsOfURL:url];

                UIImage *image = [UIImage imageWithData:imageData];

                NSLog(@"下载------%@",[NSThread currentThread]);

                [[NSOperationQueue mainQueue] addOperationWithBlock:^{

                    cell.imageView.image = image;

                    NSLog(@"UI------%@",[NSThread currentThread]);

                    // 手动刷新 刷新UITableView指定的行

                    [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationRight];

                }];

                // 保存图片到内存缓存 用图片的URL作为图片的key 保证key唯一

                [self.images setObject:image forKey:item.icon];

                // 保存图片到沙盒缓存(磁盘缓存)

                [imageData writeToFile:fullPath atomically:YES];

                 NSLog(@"下载了图片%zd----",indexPath.row);

            }];

            // 添加下载操作到并发队列中

            [self.queue addOperation:blockOperation];


        }  

    }


  • 由于拖动太快,导致的重复下载的问题。解决方式是定义一个操作缓存(字典),把之前的操作都保存起来,因为上面出现的原因本质就是因为blockOperation重复添加到queue中了。这个问题是这样的,需要显示的图片会进行下载操作,但是一张图片下载需要时间,如果图片还没下载下来,我就拖动了,将它移出屏幕,结果就是这张图片没有下载完,如果这时候我又拖动了,又要显示这张图片,因为上一次还没下载完,所以内存和磁盘里都没有,所以会重复下载。解决方式是定义一个操作缓存(字典),把之前的操作都保存起来,因为上面出现的原因本质就是因为blockOperation重复添加到queue中了。

 

   /** 定义操作缓存属性 */

    @property (nonatomic, strong) NSMutableDictionary *operations;

    // 操作缓存属性懒加载实现

    - (NSMutableDictionary *)operations{


        if (!_operations) {

            _operations = [NSMutableDictionary dictionary];

        }

    

        return _operations;

    }

    // 设置图标的位置修改如下:

    // 先去查看内存缓存中该图片有没有被下载过,如果有直接拿来用,如果没有就去检查磁盘缓存,如果没有磁盘缓存,就保存一份到内存,设置图片,否则就直接下载。

    UIImage *image = [self.images objectForKey:item.icon];

    // 如果有值直接拿来直接使用

    if(image){


        cell.imageView.image = image;

        NSLog(@"使用了内存缓存中的图片----%zd",indexPath.row);


    }else{

        

        // 沙盒缓存路径获取(磁盘缓存)

        NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];

        // 获得图片的名称

        NSString *imageName = [item.icon lastPathComponent];

        // 拼接全路径

        NSString *fullPath = [cachePath stringByAppendingPathComponent:imageName];

        // 检查磁盘缓存

        NSData *imageData = [NSData dataWithContentsOfFile:fullPath];

        if (imageData) {

            // 设置图标

            UIImage *image = [UIImage imageWithData:imageData];

            cell.imageView.image = image;

            NSLog(@"沙盒存储----%zd",indexPath.row);

            // 保存图片到内存缓存

            [self.images setObject:image forKey:item.icon];  


             }else{


                // 检查图片是否在操作缓存中进行下载,如果在下载就什么也不做,如果不在就添加下载任务

                NSBlockOperation *blockOperation = [self.operations objectForKey:item.icon];

                if (blockOperation) {

                

                }else{

                

                    // 创建下载操作

                    blockOperation = [NSBlockOperation blockOperationWithBlock:^{

                    NSURL *url = [NSURL URLWithString:item.icon];

                    NSData *imageData = [NSData dataWithContentsOfURL:url];

                    UIImage *image = [UIImage imageWithData:imageData];

                    NSLog(@"下载------%@",[NSThread currentThread]);

                    [[NSOperationQueue mainQueue] addOperationWithBlock:^{

                        cell.imageView.image = image;

                        NSLog(@"UI------%@",[NSThread currentThread]);

                        // 手动刷新 刷新UITableView指定的行

                        [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationRight];

                    }];

                    // 保存图片到内存缓存 用图片的URL作为图片的key 保证key唯一

                    [self.images setObject:image forKey:item.icon];

                     // 保存图片到沙盒缓存(磁盘缓存)

                     [imageData writeToFile:fullPath atomically:YES];

                    // 下载操作完成后进行移除操作

                    [self.operations removeObjectForKey:item.icon];

                     }];

                    // 添加下载操作到操作缓存中

                    [self.operations setObject:blockOperation forKey:item.icon];

                    // 添加下载操作到并发队列中

                    [self.queue addOperation:blockOperation];

            

        }   

    } 

}


  • cell的重用机制导致的cell图标展示数据错乱的问题,解决方案,如果要进行下载图片,先清空原来cell上的图片,但是一般不会直接设置为nil,会采用占位图片的方式来解决。


    /** 定义操作缓存属性 */

    @property (nonatomic, strong) NSMutableDictionary *operations;

    // 操作缓存属性懒加载实现

    - (NSMutableDictionary *)operations{


        if (!_operations) {

            _operations = [NSMutableDictionary dictionary];

        }

    

        return _operations;

    }

    // 设置图标的位置修改如下:

    // 先去查看内存缓存中该图片有没有被下载过,如果有直接拿来用,如果没有就去检查磁盘缓存,如果没有磁盘缓存,就保存一份到内存,设置图片,否则就直接下载。

    UIImage *image = [self.images objectForKey:item.icon];

    // 如果有值直接拿来直接使用

    if(image){


        cell.imageView.image = image;

        NSLog(@"使用了内存缓存中的图片----%zd",indexPath.row);


    }else{

        

        // 沙盒缓存路径获取(磁盘缓存)

        NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];

        // 获得图片的名称

        NSString *imageName = [item.icon lastPathComponent];

        // 拼接全路径

        NSString *fullPath = [cachePath stringByAppendingPathComponent:imageName];

        // 检查磁盘缓存

        NSData *imageData = [NSData dataWithContentsOfFile:fullPath];

        if (imageData) {

            // 设置图标

            UIImage *image = [UIImage imageWithData:imageData];

            cell.imageView.image = image;

            NSLog(@"沙盒存储----%zd",indexPath.row);

            // 保存图片到内存缓存

            [self.images setObject:image forKey:item.icon];  


             }else{


                // 检查图片是否在操作缓存中进行下载,如果在下载就什么也不做,如果不在就添加下载任务

                NSBlockOperation *blockOperation = [self.operations objectForKey:item.icon];

                if (blockOperation) {

                

                }else{


                    // 防止cell重用导致的数据错乱 先设置cell 的 image为空

                    cell.imageView.image = [UIImage imageNamed:@"placeHolder.png"];

                    // 创建下载操作

                    blockOperation = [NSBlockOperation blockOperationWithBlock:^{

                    NSURL *url = [NSURL URLWithString:item.icon];

                    NSData *imageData = [NSData dataWithContentsOfURL:url];

                    UIImage *image = [UIImage imageWithData:imageData];

                    NSLog(@"下载------%@",[NSThread currentThread]);

                    // 当url地址不正确 image为空 容错处理

                    if (!image) {

                        // 为了下一次进来的时候再次尝试进行图片下载

                        [self.operations removeObjectForKey:item.icon];

                        return ;

                    }

                      [[NSOperationQueue mainQueue] addOperationWithBlock:^{

                            cell.imageView.image = image;

                            NSLog(@"UI------%@",[NSThread currentThread]);

                            // 手动刷新 刷新UITableView指定的行

                            [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationRight];

                        }];

                    // 保存图片到内存缓存 用图片的URL作为图片的key 保证key唯一

                    [self.images setObject:image forKey:item.icon];

                     // 保存图片到沙盒缓存(磁盘缓存)

                     [imageData writeToFile:fullPath atomically:YES];

                     // 下载操作完成后进行移除操作

                    [self.operations removeObjectForKey:item.icon];

                     }];

                    // 添加下载操作到操作缓存中

                    [self.operations setObject:blockOperation forKey:item.icon];

                    // 添加下载操作到并发队列中

                    [self.queue addOperation:blockOperation];

            

        }   

    } 

}


内存问题:将图片保存在内存中是很方便的事,图片少的情况下肯定没问题,但是图片多了就会内存警告,要做一下处理。


- (void)didReceiveMemoryWarning{

    // 移除内存缓存,这里不会影响界面显示,因为有强引用的关系。

    [self.images removeAllObjects];

    // 移除队列中所有操作

    [self.queue cancelAllOperations];

}


写在最后的话:关于iOS图片加载策略的知识今天就分享到这里,关于iOS图片加载策略实现方面的问题欢迎大家和我交流,共同进步,谢谢各位。



  • 来自: 听海听心

  • 链接:https://www.jianshu.com/p/6edf6b9b2c1b

  • iOS开发整理发布,转载请联系作者授权

【点击成为Android大神】

关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接