简化SDWebImage
2018-12-06 本文已影响6人
简_爱SimpleLove
优化,我们可以理解为两种:
- 减少计算量(或者任务量)
- 减轻主线程上的压力(一种是自己在主线程上操作的任务, 一种是系统自身在主线程操作的任务,需要我们了解一些底层的操作,才能知道哪些是在主线程上操作的),都可以转到子线程上面操作
比如针对第一种方法,我们可以从业务上,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是第一个线程走的,不是后面的线程。同步下载也是,也只是第一个线程里面走了。