在日常开发中,加载网络图片场景很多,一般来说UIImage加载网路图片采用的方法是:
+ (nullable UIImage *)imageWithData:(NSData *)data;
这个方法会先请求图片数据,然后进行加载渲染,在一般的尺寸的情况下,并没有表现出什么大的问题,但是如果是加载一个超清大图时,由于图片太大,请求数据耗时长,造成图片加载延迟,出现空白的情况,这是一种很不友好的表现。面对这种情况,我这边想的可以解决的两种方案:
- 先显示缩略图,图片下载完毕后,使用原图更新UIImage;
- 采用渐进显示图片的方法。
使用缩略图显示,数据请求完毕后,更新UIImage
- 创建缩略图
CGImageRef __nullable CGImageSourceCreateThumbnailAtIndex(CGImageSourceRef __nonnull isrc, size_t index, CFDictionaryRef __nullable options)
在ImageI/O这个框架中有这样一个方法创建缩略图。其中这个方法有三个参数:
isrc
:表示的是图片数据源的容器
index
:执行加载数据源中具体哪张图片,jpg,png都只有一张图片,所以直接填写0,但是如果是多图的,比如gif,这是就需要填写相应的下标
CFDictionaryRef
:中配置图片的一些选项。
以下是官方文档的解释:
Parameters:
isrc: An image source.
index: The index that specifies the location of the image. The index is zero-based.
options:A dictionary that specifies additional creation options. See Image Source Option Dictionary Keys for the keys you can supply.
- (void)CreateThumbnailImageFromData:(NSURL *)url imageSize:(int)imageSize image:(UIImageView *)imageView
{
//先判断数据是否存在
CGImageSourceRef imageSource = CGImageSourceCreateWithURL((CFURLRef)url, NULL);
if (imageSource == NULL) {
NSLog(@"failure");
return;
}
//创建缩略图等比缩放大小,会根据长宽值比较大的作为imageSize进行缩放
NSDictionary *thumbnailInfo = @{
(NSString *)kCGImageSourceCreateThumbnailFromImageAlways : @YES,
(NSString *)kCGImageSourceThumbnailMaxPixelSize : [NSNumber numberWithInt:imageSize],
(NSString *)kCGImageSourceCreateThumbnailWithTransform : @YES,
};
//获取缩略图,由于图片是jpg格式的,所以index直接0就可以
CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, (__bridge CFDictionaryRef)thumbnailInfo );
//手动释放imageSource
CFRelease(imageSource);
if (imageRef == NULL) {
return;
}
imageView.image = [UIImage imageWithCGImage:imageRef];
//手动释放imageRef,注意是CGImageRelease(),之前写成CFRelease(),直接crash,看了好久才发现。。。
CGImageRelease(imageRef);
}
从图片中可以看出UIImageView是先加载了一张模糊的缩略图,随后使用了原图进行了更新。
内存使用对比:
由于对图片大小有限制,gif图不是很标准,通过对比,可以看出这两种加载图片的方式,在内存和CPU的使用上并无太大差异。所以我认为采用缩略图还是比较好的解决方案。
采用渐进显示图片的方法
先看效果图
可以看出图片是渐进显示,直到图片完整展现。
在ImageI/O框架中,有一个函数方法:
void CGImageSourceUpdateData(CGImageSourceRef __nonnull isrc, CFDataRef __nonnull data, bool final)
这个函数的作用是持续填充图片数据源容器,直到图片下载完毕。
参数:
isrc
:表示的是图片数据源的容器
data
:下载的图片数据
final
:标记图片是否下载完毕
实在这个功能需要使用NSURLSession
的配合,
使用NSURLSessionDataDelegate
中的代理方法:
// 1.接收到服务器的响应
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
//图片的总长度
self.expectLength = dataTask.response.expectedContentLength;
//标记图片是否下载完毕
self.isFinish = NO;
// 允许处理服务器的响应,才会继续接收服务器返回的数据
completionHandler(NSURLSessionResponseAllow);
}
// 2.接收到服务器的数据(可能调用多次)
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
//saveImageData中存储每次服务器响应返回的图片数据
[self.saveImgData appendData:data];
self.isFinish = NO;
//判断图片是否下载完毕
if (self.expectLength == self.saveImgData.length) {
self.isFinish = YES;
}
//填充数据
CGImageSourceUpdateData(self.source, (__bridge CFDataRef)self.saveImgData, self.isFinish);
CGImageRef oneRef = CGImageSourceCreateImageAtIndex(self.source, 0, nil);
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = [UIImage imageWithCGImage:oneRef];
//一定要记得释放,否则内存暴涨的那叫一个厉害
CGImageRelease(oneRef);
});
}
// 3.请求成功或者失败(如果失败,error有值)
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (self.expectLength == self.saveImgData.length) {
//释放
CFRelease(self.source);
}
}
使用以上代码,就可以实现图片渐进显示的效果,但是使用渐进效果导致一个问题,就是CPU使用占比相较另外两种方式会高出很多,毕竟每次获取到数据后都重新渲染了UIImage,目前还没找到比较好的方法,想到了再来补充吧。