美文网首页
简化SDWebImage

简化SDWebImage

作者: 简_爱SimpleLove | 来源:发表于2018-12-06 17:29 被阅读6次

    优化,我们可以理解为两种:

    1. 减少计算量(或者任务量)
    2. 减轻主线程上的压力(一种是自己在主线程上操作的任务, 一种是系统自身在主线程操作的任务,需要我们了解一些底层的操作,才能知道哪些是在主线程上操作的),都可以转到子线程上面操作

    比如针对第一种方法,我们可以从业务上,1、缓存cell的高度 2、优化cell的内容。从而避免过多的计算
    第二种而言,比如我们自己实现一个ImageView,将bitmap的操作我们可以提前做了,放在子线程上面。因为 系统默认把bitmap的操作是放在主线程的。

    图片渲染,是要先经过CPU把图片转换成bitmap,然后放到缓存,通过GPU来进行渲染
    视频加载的第一帧也是放在主线程的

    我们实现一个简化的SD,首先要大体上明白的几个步骤:
    1. 先从缓存中获取,如果没有再下载,有就直接加载缓存中的图片
    2. 缓存中没有图片就下载
    3. 下载完成后,将数据转化为bitmap图片,再生成UIImage
    4. 存储图片和步骤1对应起来
    5. 加载图片
    然后就是慢慢实现,并优化。

    通过对系统的UIImageView增加分类实现的简单SD。

    #import <UIKit/UIKit.h>
    NS_ASSUME_NONNULL_BEGIN
    @interface UIImageView (LoadImage)
    - (void)loadImageWithURL:(NSString*)url;
    @end
    NS_ASSUME_NONNULL_END
    
    #import "UIImageView+LoadImage.h"
    #import "ImageViewOperation.h"
    
    static NSOperationQueue *__operationQueue;
    
    @implementation UIImageView (LoadImage)
    
    + (void)initialize {
        
        // 初始化的时候,通过单例创建一个线程队列
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            __operationQueue = [NSOperationQueue new];
        });
    }
    
    - (void)loadImageWithURL:(NSString *)url {
        
        ImageViewOperation *operation = [ImageViewOperation new];
        operation.url = url;
        operation.imageView = self;
        [__operationQueue addOperation:operation];
    }
    @end
    
    #import <Foundation/Foundation.h>
    #import <UIKit/UIKit.h>
    NS_ASSUME_NONNULL_BEGIN
    @interface ImageViewOperation : NSOperation
    @property (nonatomic, strong)NSString *url;
    @property (nonatomic, weak)UIImageView *imageView;
    @end
    NS_ASSUME_NONNULL_END
    
    #import "ImageViewOperation.h"
    
    @implementation ImageViewOperation
    
    /*
     重写main方法,不能重写start方法,因为:
     只是重写start方法,并不会调用dealloc方法,因为start方法中没有对方法做一个结束的通知,_finished = NO,所以dealloc 没被执行。
     只是重写main方法,会调用dealloc方法。
     */
    - (void)main {
        
        // 1. 先从缓存中获取,如果没有再下载
        NSData *imageData = [self imageDataFromCache];
        
        
        if (!imageData) {
            
            // 2. 缓存中没有图片就下载
            imageData = [self imageDataFromSynNet]; // 同步下载
            
            // 3. 下载完成后,将数据转化为bitmap
            UIImage *bitmapImage = [self bitmapFromImageData:imageData];
            
            // 4. 存储图片和步骤1对应起来
            [self saveBitmapToCache:bitmapImage];
            
            // 5. 加载图片
            [self loadContentImage:bitmapImage];
        } else {
            
            // 缓存中有数据就直接加载缓存中的图片数据
            [self loadContentImage:[UIImage imageWithData:imageData]];
            
        }
        
    }
    
    
    // 1 先从缓存中获取,读文件
    - (NSData *)imageDataFromCache {
        
        NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
        // 去掉字符串 /
        NSString *fileName = [self.url stringByReplacingOccurrencesOfString:@"/" withString:@""];
        NSString *filePath = [documentPath stringByAppendingPathComponent:fileName];
        NSData *imageData = [NSData dataWithContentsOfFile:filePath];
        
        return imageData;
    }
    
    // 2 下载
    - (NSData *)imageDataFromSynNet {
        
        NSURLSession *session = [NSURLSession sharedSession];
        
        __block NSData *imageData = nil;
        
        // 同步(用信号量操作)
        dispatch_semaphore_t sem = dispatch_semaphore_create(0);
        
        NSURLSessionTask *task = [session dataTaskWithURL:[NSURL URLWithString:self.url] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            
            if (!error) {  // 更多网络异常处理,未处理,实际上还需要g处理
                imageData = data;
            }
            
            dispatch_semaphore_signal(sem); // 发送信号量
            
        }];
        
        [task resume];  // 开启网络任务
        
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // 等待信号(阻塞),网络请求没有完成,不会执行后面的代码
        
        return imageData;
    }
    
    
    // 3 生成一个bitmap图片
    - (UIImage *)bitmapFromImageData:(NSData *)imageData {
        
        UIImage *netImage = [UIImage imageWithData:imageData];
        
        CGImageRef imageRef = netImage.CGImage;
        
        size_t width = CGImageGetWidth(imageRef);
        size_t height = CGImageGetHeight(imageRef);
        
        // 3.1 获取一个bitmap上下文
        CGContextRef contextRef = CGBitmapContextCreate(NULL, width, height, CGImageGetBitsPerComponent(imageRef), CGImageGetBytesPerRow(imageRef), CGImageGetColorSpace(imageRef), CGImageGetBitmapInfo(imageRef));
        
        // 3.2 在bitmap上下文上绘制图片
        CGContextDrawImage(contextRef, CGRectMake(0, 0, width, height), imageRef);
        
        // 3.3 把bitmap上下文转化成CGImageRef
        CGImageRef backImageRef = CGBitmapContextCreateImage(contextRef);
        
        
        // 3.4 把CGImageRef 转化成UIImage对象
        UIImage *bitmapImage = [UIImage imageWithCGImage:backImageRef];
        
        CFRelease(backImageRef);
        
        UIGraphicsEndImageContext(); // 结束上下文  (入栈出栈)
        
        return bitmapImage;
    }
    
    // 4 存储 写文件 根据图片网址的地址进行存储
    - (void)saveBitmapToCache:(UIImage *)bitmap {
        
        NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
        // 去掉字符串 /
        NSString *fileName = [self.url stringByReplacingOccurrencesOfString:@"/" withString:@""];
        NSString *filePath = [documentPath stringByAppendingPathComponent:fileName];
        
        NSData *imageData = UIImagePNGRepresentation(bitmap);
        [imageData writeToFile:filePath atomically:YES];
        
    }
    
    
    // 5 将图片加载到UI上
    
    - (void)loadContentImage:(UIImage *)bitmap {
        
        dispatch_async(dispatch_get_main_queue(), ^{
            
            //下面两种加载图片的方法都可以
            
            // 因为imageView是自定义的EOCImageView,继承自UIView,所以没有image属性,所以用下面的方式赋值图片
    //        self.imageView.layer.contents = (__bridge id)bitmap.CGImage;
            self.imageView.image = bitmap;
            
        });
        
    }
    @end
    

    下面是自定义的ImageView,并进行了一些优化。

    #import <UIKit/UIKit.h>
    NS_ASSUME_NONNULL_BEGIN
    @interface SJImageView : UIView
    @property (nonatomic, strong) NSString *imageUrl;
    - (void)loadImageWithURL:(NSString *)url;
    @end
    NS_ASSUME_NONNULL_END
    
    #import "SJImageView.h"
    #import "LoadImageOperation.h"
    
    static NSOperationQueue *__operationQueue;
    
    @implementation SJImageView
    
    + (void)initialize {
        
        // 初始化的时候,通过单例创建一个线程队列
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            __operationQueue = [NSOperationQueue new];
        });
    }
    
    - (void)loadImageWithURL:(NSString *)url {
        
        self.imageUrl = url;
        LoadImageOperation *operation = [LoadImageOperation new];
        operation.url = url;
        operation.imageView = self;
        [__operationQueue addOperation:operation];
    }
    @end
    
    #import <Foundation/Foundation.h>
    #import "SJImageView.h"
    NS_ASSUME_NONNULL_BEGIN
    // 自定义一个线程,异步加载图片
    @interface LoadImageOperation : NSOperation
    @property (nonatomic, strong) NSString *url;
    /*
    为什么用 weak(最好用weak,因为线程最好不要对View有强引用,让它可以由控制器来控制生命周期,虽然我用strong,
    最后ImageView也走了delloc方法,但那是线程已经结束了的时候,如果网络比较卡,线程还没有结束,
    当控制器不在的时候,会因为线程对view有强引用,而释放不了,导致内存泄漏)
     */
    @property (nonatomic, weak) SJImageView *imageView;  
    @end
    NS_ASSUME_NONNULL_END
    
    #import "LoadImageOperation.h"
    
    /*
     1 缓存
     2 bitmap
     
     3 问题
     
     3.1 取消任务怎么处理 (加载过程中, imageview换了一个url地址/或者imageview释放掉了, 这怎么处理)
     
     3.2 任务重复怎么处理 (两个imageview,加载同一个图片)
     
     // url没变,服务的图片变了, 这种情况必须每次都去下载图片
     
     1 缓存  2 下载  3 bitmap 4 保存 5 加载
     
     
     */
    
    static NSMutableDictionary *__taskLoadInfoDict; // 存放任务信息,一个url对应一个任务
    static NSMutableArray *__sameTaskArr; // 存放相同的任务对象
    static NSLock *__dataLock;
    
    @implementation LoadImageOperation
    
    //初始化时,使用单例都初始化一遍
    + (void)initialize
    {
    
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            __taskLoadInfoDict = [NSMutableDictionary new];
            __sameTaskArr = [NSMutableArray new];
            __dataLock = [NSLock new];
        });
    }
    
    /*
     重写main方法,不能重写start方法,因为:
     只是重写start方法,并不会调用dealloc方法,因为start方法中没有对方法做一个结束的通知,_finished = NO,所以dealloc 没被执行。
     只是重写main方法,会调用dealloc方法。
     */
    - (void)main {
        
        // 1. 先从缓存中获取,如果没有再下载
        NSData *imageData = [self imageDataFromCache];
        
        // 取消操作
        BOOL (^cancelOperation)(void) = ^{
            
            // 如果imageview释放掉了,也返回YES,取消下载
            if (!self.imageView) {
                return YES;
            }
            
            // 如果imageview换了一个url地址,返回YES,取消下载
            if (![self.url isEqual:self.imageView.imageUrl]) {
                return YES;
            }
            return NO;
        };
        
        // 下面这是一个取消的节点,并不是马上能够取消,把节点放到关键的地方越多,取消的准确率也就越高
        if (cancelOperation()) {
            return;
        }
        
        if (!imageData) {
            
            // 取消节点
            if (cancelOperation()) {
                return;
            }
            
            // 2. 缓存中没有图片就下载
            
            // 2.1 防止相同的任务请求
            
            // 上锁,避免别的地方操作,同时添加任务什么的
            [__dataLock lock];
            
            //如果任务已经开始了,那么直接返回,并且在相同的任务中,添加第二次任务的对象,即需要展示图片的ImageView
            if ([__taskLoadInfoDict objectForKey:self.url]) {
                // 能进到这个方法里面,就说明已经是重复任务了
                NSMutableArray *sameTaskArr = [__taskLoadInfoDict objectForKey:self.url];
                [sameTaskArr addObject:self.imageView];
                [__dataLock unlock];
                // 这个return不能少,是跳出整个大的方法,后面还走的,是接到第一次线程走的
                return;
            }
            
            NSMutableArray *sameTaskArr = [NSMutableArray new];
            [__taskLoadInfoDict setObject:sameTaskArr forKey:self.url];
            [__dataLock unlock];
            
            // 2.2 同步下载
            imageData = [self imageDataFromSynNet];
            
            // 3. 下载完成后,将数据转化为bitmap
            UIImage *bitmapImage = [self bitmapFromImageData:imageData];
            
            // 4. 存储图片和步骤1对应起来
            [self saveBitmapToCache:bitmapImage];
            
            // 5.1 重复任务的加载
            [self sameTaskLoadHandleTwo:bitmapImage];
            
            // 取消节点
            if (cancelOperation()) {
                return;
            }
            
            // 5.2 加载图片
            [self loadContentImage:bitmapImage];
        } else {
            
            // 取消节点
            if (cancelOperation()) {
                return;
            }
            
            // 5. 缓存中有数据就直接加载缓存中的图片数据
            [self loadContentImage:[UIImage imageWithData:imageData]];
            
        }
        
    }
    
    
    - (void)sameTaskLoadHandleTwo:(UIImage *)bitmap {
        
        [__dataLock lock];
        
        // 取出url对应的相同的任务对象
        NSMutableArray *sameTaskArr = [__taskLoadInfoDict objectForKey:self.url];
        // 移除url对应的任务,因为这次请求已经结束了
        [__taskLoadInfoDict removeObjectForKey:self.url];
        
        for (int i = 0; i < sameTaskArr.count; i++) {
            
            SJImageView *imageView = sameTaskArr[i];
            // 如果两个url相同,就在主线程中给imageView添加图片
            if ([imageView.imageUrl isEqual:self.url]) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    imageView.layer.contents = (__bridge id)bitmap.CGImage;
                });
            }
        }
        [__dataLock unlock];
    }
    
    
    // 1 先从缓存中获取,读文件
    - (NSData *)imageDataFromCache {
        
        NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
        // 去掉字符串 /
        NSString *fileName = [self.url stringByReplacingOccurrencesOfString:@"/" withString:@""];
        NSString *filePath = [documentPath stringByAppendingPathComponent:fileName];
        NSData *imageData = [NSData dataWithContentsOfFile:filePath];
        
        return imageData;
    }
    
    // 2 下载
    - (NSData *)imageDataFromSynNet {
        
        NSLog(@"下载开始");
        NSURLSession *session = [NSURLSession sharedSession];
        
        __block NSData *imageData = nil;
        
        // 同步(用信号量操作)
        dispatch_semaphore_t sem = dispatch_semaphore_create(0);
        
        NSURLSessionTask *task = [session dataTaskWithURL:[NSURL URLWithString:self.url] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            
            if (!error) {  // 更多网络异常处理,未处理,实际上还需要g处理
                imageData = data;
            }
            
            dispatch_semaphore_signal(sem); // 发送信号量
            
        }];
        
        [task resume];  // 开启网络任务
        
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // 等待信号(阻塞),网络请求没有完成,不会执行后面的代码
        
        return imageData;
    }
    
    
    // 3 生成一个bitmap图片
    - (UIImage *)bitmapFromImageData:(NSData *)imageData {
        
        UIImage *netImage = [UIImage imageWithData:imageData];
        
        // 安全处理,如果没有图片就返回
        if (!netImage) {
            return nil;
        }
        
        CGImageRef imageRef = netImage.CGImage;
        
        size_t width = CGImageGetWidth(imageRef);
        size_t height = CGImageGetHeight(imageRef);
        
        // 3.1 获取一个bitmap上下文
        CGContextRef contextRef = CGBitmapContextCreate(NULL, width, height, CGImageGetBitsPerComponent(imageRef), CGImageGetBytesPerRow(imageRef), CGImageGetColorSpace(imageRef), CGImageGetBitmapInfo(imageRef));
        
        // 3.2 在bitmap上下文上绘制图片
        CGContextDrawImage(contextRef, CGRectMake(0, 0, width, height), imageRef);
        
        // 3.3 把bitmap上下文转化成CGImageRef
        CGImageRef backImageRef = CGBitmapContextCreateImage(contextRef);
        
        // 安全处理,如果没有图片就返回
        if (!netImage) {
            return nil;
        }
        
        // 3.4 把CGImageRef 转化成UIImage对象
        UIImage *bitmapImage = [UIImage imageWithCGImage:backImageRef];
        
        CFRelease(backImageRef);
        
        UIGraphicsEndImageContext(); // 结束上下文  (入栈出栈)
        
        return bitmapImage;
    }
    
    // 4 存储 写文件 根据图片网址的地址进行存储
    - (void)saveBitmapToCache:(UIImage *)bitmap {
        
        NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
        // 去掉字符串 /
        NSString *fileName = [self.url stringByReplacingOccurrencesOfString:@"/" withString:@""];
        NSString *filePath = [documentPath stringByAppendingPathComponent:fileName];
        
        NSData *imageData = UIImagePNGRepresentation(bitmap);
        [imageData writeToFile:filePath atomically:YES];
    
    }
    
    
    // 5 将图片加载到UI上
    
    - (void)loadContentImage:(UIImage *)bitmap {
        
        dispatch_async(dispatch_get_main_queue(), ^{
            
            // 因为imageView是自定义的EOCImageView,继承自UIView,所以没有image属性,所以用下面的方式赋值图片
            self.imageView.layer.contents = (__bridge id)bitmap.CGImage;
            
        });
    }
    
    - (void)dealloc {
        NSLog(@"%s", __func__);
    }
    @end
    

    使用方法就比较简单了,如下:

        SJImageView *imageView = [[SJImageView alloc] initWithFrame:CGRectMake(87, 0, 200, 200)];
        [imageView loadImageWithURL:@"http://img.hb.aicdn.com/0f608994c82c2efce030741f233b29b9ba243db81ddac-RSdX35_fw658"];
        [self.view addSubview:imageView];
    
        SJImageView *imageViewTwo = [[SJImageView alloc] initWithFrame:CGRectMake(87, 250, 200, 200)];
        [imageViewTwo loadImageWithURL:@"http://img.hb.aicdn.com/0f608994c82c2efce030741f233b29b9ba243db81ddac-RSdX35_fw658"];
        [self.view addSubview:imageViewTwo];
    
        SJImageView *imageView3 = [[SJImageView alloc] initWithFrame:CGRectMake(87, 470, 200, 200)];
        [imageView3 loadImageWithURL:@"http://img.hb.aicdn.com/0f608994c82c2efce030741f233b29b9ba243db81ddac-RSdX35_fw658"];
        [self.view addSubview:imageView3];
    

    需要注意的是:


    image.png

    上面图片中,return过后是直接跳出了整个大方法的。后面又走的2和3是因为,第一个线程因为同步下载,卡在了那里,所以等下载完成过后,才走的2和3,也就是说2和3是第一个线程走的,不是后面的线程。同步下载也是,也只是第一个线程里面走了。

    相关文章

      网友评论

          本文标题:简化SDWebImage

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