圖片的編碼:在當前APP的開發中,圖片是常常會使用到的,關於圖片有不少種格式,例如JPEG
,PNG
等。其實這些各類各樣的圖片格式都對應了位圖(bitmap)通過不一樣算法編碼(壓縮)後的圖片。(編碼這裏就不過多介紹了)html
圖片的解碼:app從磁盤中讀入編碼後的圖片,須要通過解碼把圖片變成位圖(bitmap)讀入,這樣才能顯示在屏幕上。ios
位圖(bitmap):位圖又被叫作點陣圖像,也就是說位圖包含了一大堆的像素點信息,這些像素點就是該圖片中的點,有了圖片中每一個像素點的信息,就能夠在屏幕上渲染整張圖片了。算法
圖片本質上是位圖,一堆像素點組成的二維數組,其中每一個像素點都記錄該點位的顏色等信息。顯示出來就是一張圖了。數組
既然像素要存儲顏色數據,這裏就又引出一個顏色存儲格式的概念。咱們就以最簡單廣泛的32-bit RGBA 色彩存儲格式爲例子,他的意思是一個像素點存儲的色彩所需空間是32bits或是4bytes,1byte或8bit存儲是一個通道,對應下來就是:bash
- R = red (佔1byte或8bit)
- G = green (佔1byte或8bit)
- B = blue (佔1byte或8bit)
- A = alpha (佔1byte或8bit)
這樣你就知道 32-bit RGBA 格式可以顯示的顏色是 2^8 * 2^8* 2^8 (256 * 256 * 256),將近一千七百多萬個顏色。還有顏色空間(Color Spaces)的概念這裏就再也不擴展了。session
而位圖是裝載像素點的數組,這樣你大概能夠理解下一張普通位圖包含着多少數據!同時,這裏解釋顏色是爲了下面計算位圖大小,便於理解咱們爲何要進行圖片編碼。app
經過iOS - 圖形高級處理 (1、圖片顯示相關理論)的學習能夠知道,圖片的解壓縮是一個很是耗時的 CPU 操做,而且它默認是在主線程中執行的。那麼當須要加載的圖片比較多時,就會對咱們應用的響應性形成嚴重的影響,尤爲是在快速滑動的列表上,這個問題會表現得更加突出。
既然如此,圖片不編碼也就不用解碼,都使用位圖能夠嗎?這寫在這裏的確是明知故問的問題,下面就解釋下爲何必須對圖片進行編解碼操做。框架
一張位圖的寬和高分別都是100個像素,那這個位圖的大小是多少呢?ide
//計算一張位圖size的公式 //bytesPerPixel每一個像素點所需空間 //32-bit RGBA 格式圖片 bytesPerPixel = 4 (R,G,B,A各一個byte),理論看上面 size = width * height * bytesPerPixel 複製代碼
這樣把咱們100x100 的位圖代入該公式,能夠獲得其大小:函數
size = 100 * 100 * 4 = 40000B = 39KB 複製代碼
正常一張PNG或JPEG格式的100x100的圖片,大概只有幾KB。若是更大的圖,位圖所佔空間更大,因此位圖必須進行編碼進行存儲。
這裏不過多介紹了,蘋果提供2種圖片編碼格式,PNG和JPEG:
PNG 是無損壓損,JPEG能夠是有損壓縮(0-100% ),即損失部分信息來壓縮圖片,這樣壓縮以後的圖片大小將會更小。
// return image as PNG. May return nil if image has no CGImageRef or invalid bitmap format UIKIT_EXTERN NSData * __nullable UIImagePNGRepresentation(UIImage * __nonnull image); // return image as JPEG. May return nil if image has no CGImageRef or invalid bitmap format. compression is 0(most)..1(least) UIKIT_EXTERN NSData * __nullable UIImageJPEGRepresentation(UIImage * __nonnull image, CGFloat compressionQuality); 複製代碼
編碼後的圖片須要顯示在屏幕上,咱們須要得到圖片全部信息,也就是對應編碼前的位圖。因此編碼後的圖片必需要通過解碼才能正常顯示。
Buffer 表示一片連續的內存空間。在這裏,咱們說的 Buffer 是指一系列內部結構相同、大小相同的元素組成的內存區域。有三種Buffer:Data Buffer、Image Buffer、Frame Buffer。這個理論是2018WWDC蘋果上描述的概念,具體可看Image and Graphics Best Practices
- Data Buffer 是指存儲在內存中的原始數據,圖像可使用不一樣的格式保存,如 jpg、png。Data Buffer 的信息不能用來描述圖像的位圖像素信息。
- Image Buffer 是指圖像在內存中的存在方式,其中每一個元素描述了一個像素點。Image Buffer 的大小和位圖的大小相等。
- Frame Buffer 和 Image Buffer 內容相同,不過其存儲在 vRAM(video RAM)中,而 Image Buffer 存儲在 RAM 中。
圖片解碼過程:
一、假如在本地沙盒下有一張 JPEG 格式的圖片或項目資源中讀入通常都這麼作
UIImageView *imageView = ...; // UIImage *image = [UIImage imageNamed:@"xxx"]; UIImage *image = [UIImage imageWithContentsOfFile:@"xxx.JPG"]; imageView.image = image; 複製代碼
二、UIImage 是 iOS 中處理圖像的高級類。建立一個 UIImage 實例只會加載 Data Buffer;也就是說以上只是把圖片轉爲UIImage對象,該對象存儲在Data Buffer裏。此時並無對圖片進行解碼。
三、當將圖像顯示到屏幕上會觸發隱式解碼。(必須同時知足圖像被設置到 UIImageView 中、UIImageView 添加到視圖,纔會觸發圖像解碼。)也就是說你就算實例了一個UIImageView,可是沒有把他addSubview,顯示到視圖上,系統也是不會進行解碼的。
現實問題產生:
這個解碼過程默認是發生在主線程上面的,並且很是消耗 CPU,因此到若是在 tableView 或者 collectionView 中有至關多的圖片須要顯示的話,這些圖片在主線程的解碼操做必然會影響滑動的順暢度。因此咱們是否能夠在子線程強制將其解碼,而後在主線程讓系統渲染解碼以後的圖片呢?固然能夠,如今基本上全部的開源圖片庫都會實現這個操做。例如:YYImage\SDWebImage。
現實中解決方式:
本身手動解碼的原理就是對圖片進行從新繪製,獲得一張新的解碼後的位圖。其中,用到的最核心的函數是 CGBitmapContextCreate :
CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0); 複製代碼
這個方法是建立一個圖片處理的上下文 CGContext 對象,由於上面方法的返回值 CGContextRef 實際上就是 CGContext *。關於這個函數的詳細講解博文有不少,官方文檔CGBitmapContextCreate。博客文章,圖片解碼。
開源框架的解決方案基礎也是基於這個API:
一、YYImage 中解碼的代碼:
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 { ... } } 複製代碼
實際上, 這個方法的做用是建立一個圖像的拷貝,它接受一個原始的位圖參數 imageRef ,最終返回一個新的解碼後的位圖 newImage ,中間主要通過了如下三個步驟:
- 使用 CGBitmapContextCreate 函數建立一個位圖上下文;
- 使用 CGContextDrawImage 函數將原始位圖繪製到上下文中;
- 使用 CGBitmapContextCreateImage 函數建立一張新的解壓縮後的位圖。
事實上,SDWebImage 中對圖片的解壓縮過程與上述徹底一致,只是傳遞給 CGBitmapContextCreate 函數的部分參數存在細微的差異
二、SDWebImage的解碼實現
+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image { if (![UIImage 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 = [UIImage colorSpaceForImageRef:imageRef]; size_t width = CGImageGetWidth(imageRef); size_t height = CGImageGetHeight(imageRef); size_t bytesPerRow = kBytesPerPixel * width; // 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, bytesPerRow, 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; } } + (BOOL)shouldDecodeImage:(nullable UIImage *)image { // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error if (image == nil) { return NO; } // do not decode animated images if (image.images != nil) { return NO; } CGImageRef imageRef = image.CGImage; CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef); BOOL anyAlpha = (alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaPremultipliedLast); // do not decode images with alpha if (anyAlpha) { return NO; } return YES; } 複製代碼
SDWebImage 中和其餘不同的地方,就是若是一張圖片有 alpha 份量,那就直接返回原始圖片,再也不進行解碼操做。這麼作是由於alpha 份量不可知,爲了保證原圖完整信息故不作處理。
SDWebImage 在解碼操做外面包了 autoreleasepool,這樣在大量圖片須要解碼的時候,可使得局部變量儘早釋放掉,不會形成內存峯值太高。
大圖顯示這個問題,看似和圖片編碼解碼無關。可是大的圖片會佔用較多的內存資源,解碼和傳輸到 GPU 也會耗費較多時間。 所以,實際須要顯示的圖像尺寸可能並非很大,若是能將大圖縮小,便能達到優化的目的。如下是WWDC給的大圖顯示方案,功能是縮小圖像並解碼:
// 大圖縮小爲顯示尺寸的圖
- (UIImage *)downsampleImageAt:(NSURL *)imageURL to:(CGSize)pointSize scale:(CGFloat)scale {
// 利用圖像文件地址建立 image source
NSDictionary *imageSourceOptions =
@{
(__bridge NSString *)kCGImageSourceShouldCache: @NO // 原始圖像不要解碼
};
CGImageSourceRef imageSource =
CGImageSourceCreateWithURL((__bridge CFURLRef)imageURL, (__bridge CFDictionaryRef)imageSourceOptions);
// 下采樣
CGFloat maxDimensionInPixels = MAX(pointSize.width, pointSize.height) * scale;
NSDictionary *downsampleOptions =
@{
(__bridge NSString *)kCGImageSourceCreateThumbnailFromImageAlways: @YES,
(__bridge NSString *)kCGImageSourceShouldCacheImmediately: @YES, // 縮小圖像的同時進行解碼
(__bridge NSString *)kCGImageSourceCreateThumbnailWithTransform: @YES,
(__bridge NSString *)kCGImageSourceThumbnailMaxPixelSize: @(maxDimensionInPixels)
};
CGImageRef downsampledImage =
CGImageSourceCreateThumbnailAtIndex(imageSource, 0, (__bridge CFDictionaryRef)downsampleOptions);
UIImage *image = [[UIImage alloc] initWithCGImage:downsampledImage];
CGImageRelease(downsampledImage);
CFRelease(imageSource);
return image;
}
複製代碼
// Downsampling large images for display at smaller size
func downsample(imageAt imageURL: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage {
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOptions)!
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
let downsampleOptions =
[kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionary
let downsampledImage =
CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions)!
return UIImage(cgImage: downsampledImage)
}
複製代碼