依稀记得好久之前被问到过这么一个问题。ios
若是网络下载下来的图片很大的状况下要怎么处理。缓存
那时候对这块内容不是特别了解,大体只知道内存确定会爆掉。而后回答的是超大图就不显示了吧😂😂😂。后面也尝试去Google了,可是可能那时候比较急躁,没有很深刻的去理解这个问题。今天我在回味YY
大佬的iOS 处理图片的一些小 Tip的时候看到了下面的评论里面有人也提了相同的问题,大佬的回答是bash
能够参考苹果官方例子: https://developer.apple.com/library/ios/samplecode/LargeImageDownsizing/ 另外,在用图片库时,用参数禁用解码。网络
鉴于我最近高涨的学习兴趣,决定去一探究竟。app
我这边尝试写了个demo来看看具体会发生什么。(空的初始化工程,只是首页展现了这张图片)ide
具体结果:oop
当用[UIImage imageNamed:name]
的方式加载本地大图的时候,内存的变化是 45 MB —> 318.5MB。能够说是内存暴增了,这样的暴增带来的结果颇有可能就是闪退。性能
当用SDWebImage
或者YYWebImage
加载的时候结果相似,有细微的几MB的差异。差很少都是 45MB -> 240MB -> 47Mb。能够看到仍是有段时间是内存暴增的状况,仍是存在闪退的风险。学习
搞清楚这个问题以前,咱们先来看一下图片加载的具体流程。方便后面理解。ui
假设咱们用的是
imageNamed
的方式来加载图片
1.先在bundle里查找图片的文件名返回给image。
2.加载图片前,经过文件名加载image赋值给imageView。这个时候图片并不会直接解压缩。
3.一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
4.在主线程的下一个 run loop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操做,而受图片是否字节对齐等因素的影响,这个 copy 操做可能会涉及如下部分或所有步骤:
a.分配内存缓冲区用于管理文件 IO 和解压缩操做;
b.将文件数据从磁盘读到内存中;
c.将压缩的图片数据解码成未压缩的位图形式,这是一个很是耗时的 CPU 操做;
d.最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层。
复制代码
咱们能够看到,图片并非一赋值给imageView就显示的。图片须要在显示以前解压缩成未压缩的位图形式才能显示。可是这样的一个操做是很是耗时的CPU操做,而且这个操做是在主线程当中进行的。因此若是没有特殊处理的状况下,在图片不少的列表里快速滑动的状况下会有性能问题。
在接下去讲内容以前先来解释下这个问题。不论是 JPEG 仍是 PNG 图片,都是一种压缩的位图图形格式。按照个人理解就是把原始的位图数据压缩一下,按照特定的格式删掉一些内容,这样一来数据就变少了。图片也就变小了。可是咱们展现的时候这样压缩过的格式是没法直接显示的,咱们须要拿到图片的原始数据,因此咱们就须要在展现前解压缩一下。
上面提到,若是咱们不作特殊处理的话,解压缩会带来一些性能问题。可是若是咱们给imageView提供的是解压缩后的位图那么系统就不会再进行解压缩操做。这种方式也是SDWebImage
和YYWebImage
的实现方式。具体解压缩的原理就是CGBitmapContextCreate
方法从新生产一张位图而后把图片绘制当这个位图上,最后拿到的图片就是解压缩以后的图片。
SDWebImage
里面的这部分代码
- (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image {
if (![[self class] shouldDecodeImage:image]) {
return image;
}
// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
@autoreleasepool{
CGImageRef imageRef = image.CGImage;
CGColorSpaceRef colorspaceRef = [[self class] colorSpaceForImageRef:imageRef];
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
kBitsPerComponent,
0,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
if (context == NULL) {
return image;
}
// Draw the image into the context and retrieve the new bitmap image without alpha
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
scale:image.scale
orientation:image.imageOrientation];
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);
return imageWithoutAlpha;
}
}
复制代码
YYWebImage
里的这部分代码
CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
if (!imageRef) return NULL;
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
if (width == 0 || height == 0) return NULL;
if (decodeForDisplay) { //decode with redraw (may lose some precision)
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
// BGRA8888 (premultiplied) or BGRX8888
// same as UIGraphicsBeginImageContext() and -[UIView drawRect:]
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo);
if (!context) return NULL;
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode
CGImageRef newImage = CGBitmapContextCreateImage(context);
CFRelease(context);
return newImage;
} else {
CGColorSpaceRef space = CGImageGetColorSpace(imageRef);
size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
size_t bitsPerPixel = CGImageGetBitsPerPixel(imageRef);
size_t bytesPerRow = CGImageGetBytesPerRow(imageRef);
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
if (bytesPerRow == 0 || width == 0 || height == 0) return NULL;
CGDataProviderRef dataProvider = CGImageGetDataProvider(imageRef);
if (!dataProvider) return NULL;
CFDataRef data = CGDataProviderCopyData(dataProvider); // decode
if (!data) return NULL;
CGDataProviderRef newProvider = CGDataProviderCreateWithCFData(data);
CFRelease(data);
if (!newProvider) return NULL;
CGImageRef newImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, space, bitmapInfo, newProvider, NULL, false, kCGRenderingIntentDefault);
CFRelease(newProvider);
return newImage;
}
}
复制代码
如今来分析一下为何会出现内存暴增的问题,上面的内容看似和这个问题没有上面联系。其实否则,上面咱们都知道,用系统的方法和用SDWebImage
或者YYWebImage
加载图片的时候都涉及到了一个解压缩的操做,而解压缩的操做有涉及到了一个位图的建立。
先看看SDWebImage
或者YYWebImage
,它们用的是CGBitmapContextCreate
这个方法。我在文档里发现咱们须要传递一个data参数,文档里的解释是以下。
data A pointer to the destination in memory where the drawing is to be rendered. The size of this memory block should be at least (bytesPerRow*height) bytes.
Pass NULL if you want this function to allocate memory for the bitmap. This frees you from managing your own memory, which reduces memory leak issues.
也就是说咱们须要去生成一个一块大小为bytesPerRow*height的内存,经过查阅其它博客发现最后的计算内存大小的逻辑是(宽度 * 高度 * 4 (一个像素4个字节)),固然你也能够传NULL,这样的话系统就会帮你去建立。咱们上面用的图片是7033 × 10110计算出来的尺寸是271MB。这里和上面看见的大小有细微出入,由于不是用的instruments因此可能不是很准,并且可能其它的东西会影响到这个结果。这里暂且不论。咱们看到上面的demo里内存最后会有一个回落的过程,结合上面两个库里的代码能够看到,拿到图片以后这两个库都把位图释放掉了,内存得以释放。因此内存也就回落了。
看不到系统具体的建立方法,可是建立位图的过程应该相似。而且咱们都知道imageNamed
加载图片的方式最后会把位图存到一个全局缓存里面去,因此用系统的方式咱们看不到内存的回落。
内存暴增就是由于,解压缩展现大图的时候咱们建立的位图太大了,占用了很大的内存空间。
经过上面的分析咱们已经知道具体的缘由了。
以前YY
大佬的解释里的用参数禁用解码
也就很好理解了。因为用第三方库的时候都是提早作了一步解压缩操做,因此当图片很大的状况下这一步建立的位图会占用很大的内存。因此咱们须要禁止解压缩。
若是图片不是解压缩完的位图,那么想要显示在屏幕上不管如何都是要作解压缩的,以前第三方只是提早作了这步的操做。居然有现成的方案了,咱们来看一下具体是须要怎么处理的。
核心的方法逻辑:
CGBitmapContextCreate
方法生成一张比例缩放的位图。CGImageCreateWithImageInRect
根据计算的rect获取到图片数据。CGContextDrawImage
根据计算的rect把获取到的图片数据绘制到位图上。CGBitmapContextCreateImage
绘制完毕获取到图片显示。画了个好像没什么用的图:
具体代码:
-(void)downsize:(id)arg {
// 建立NSAutoreleasePool
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
// 获取图片,这个时候是不会绘制
sourceImage = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:kImageFilename ofType:nil]];
if( sourceImage == nil ) NSLog(@"input image not found!");
// 拿到当前图片的宽高
sourceResolution.width = CGImageGetWidth(sourceImage.CGImage);
sourceResolution.height = CGImageGetHeight(sourceImage.CGImage);
// 当前图片的像素
sourceTotalPixels = sourceResolution.width * sourceResolution.height;
// 当前图片渲染到界面上的大小
sourceTotalMB = sourceTotalPixels / pixelsPerMB;
// 获取当前最合适的图片渲染大小,计算图片的缩放比例
imageScale = destTotalPixels / sourceTotalPixels;
// 拿到缩放后的宽高
destResolution.width = (int)( sourceResolution.width * imageScale );
destResolution.height = (int)( sourceResolution.height * imageScale );
// 生成一个rgb的颜色空间
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
// 缩放状况下的每一行的字节数
int bytesPerRow = bytesPerPixel * destResolution.width;
// 计算缩放状况下的位图大小,申请一块内存
void* destBitmapData = malloc( bytesPerRow * destResolution.height );
if( destBitmapData == NULL ) NSLog(@"failed to allocate space for the output image!");
// 根据计算的参数生成一个合适尺寸的位图
destContext = CGBitmapContextCreate( destBitmapData, destResolution.width, destResolution.height, 8, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast );
// 若是生成失败了释放掉以前申请的内存
if( destContext == NULL ) {
free( destBitmapData );
NSLog(@"failed to create the output bitmap context!");
}
// 释放掉颜色空间
CGColorSpaceRelease( colorSpace );
// 坐标系转换
CGContextTranslateCTM( destContext, 0.0f, destResolution.height );
CGContextScaleCTM( destContext, 1.0f, -1.0f );
// 分块绘制的宽度(原始宽度)
sourceTile.size.width = sourceResolution.width;
// 分块绘制的高度
sourceTile.size.height = (int)( tileTotalPixels / sourceTile.size.width );
NSLog(@"source tile size: %f x %f",sourceTile.size.width, sourceTile.size.height);
sourceTile.origin.x = 0.0f;
// 绘制到位图上的宽高
destTile.size.width = destResolution.width;
destTile.size.height = sourceTile.size.height * imageScale;
destTile.origin.x = 0.0f;
NSLog(@"dest tile size: %f x %f",destTile.size.width, destTile.size.height);
// 重合的像素
sourceSeemOverlap = (int)( ( destSeemOverlap / destResolution.height ) * sourceResolution.height );
NSLog(@"dest seem overlap: %f, source seem overlap: %f",destSeemOverlap, sourceSeemOverlap);
CGImageRef sourceTileImageRef;
// 分块绘制须要多少次才能绘制完成
int iterations = (int)( sourceResolution.height / sourceTile.size.height );
int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
if( remainder ) iterations++;
// 添加剧合线条
float sourceTileHeightMinusOverlap = sourceTile.size.height;
sourceTile.size.height += sourceSeemOverlap;
destTile.size.height += destSeemOverlap;
// 分块绘制
for( int y = 0; y < iterations; ++y ) {
// create an autorelease pool to catch calls to -autorelease made within the downsize loop.
NSAutoreleasePool* pool2 = [[NSAutoreleasePool alloc] init];
NSLog(@"iteration %d of %d",y+1,iterations);
sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
destTile.origin.y = ( destResolution.height ) - ( ( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + destSeemOverlap );
// 分块拿到图片数据
sourceTileImageRef = CGImageCreateWithImageInRect( sourceImage.CGImage, sourceTile );
// 计算绘制的位置
if( y == iterations - 1 && remainder ) {
float dify = destTile.size.height;
destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
dify -= destTile.size.height;
destTile.origin.y += dify;
}
// 绘制到位图上
CGContextDrawImage( destContext, destTile, sourceTileImageRef );
// 释放内存
CGImageRelease( sourceTileImageRef );
[sourceImage release];
[pool2 drain];
// 更新图片显示
if( y < iterations - 1 ) {
sourceImage = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:kImageFilename ofType:nil]];
[self performSelectorOnMainThread:@selector(updateScrollView:) withObject:nil waitUntilDone:YES];
}
}
// 显示图片,释放内存
[self performSelectorOnMainThread:@selector(initializeScrollView:) withObject:nil waitUntilDone:YES];
CGContextRelease( destContext );
[pool drain];
}
复制代码
但愿能对你们有一点点的帮助。