简化SDWebImage

2018-12-06  本文已影响6人  简_爱SimpleLove

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

  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是第一个线程走的,不是后面的线程。同步下载也是,也只是第一个线程里面走了。

上一篇下一篇

猜你喜欢

热点阅读