依稀記得好久之前被問到過這麼一個問題。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];
}
複製代碼
但願能對你們有一點點的幫助。