iOS-圖片高級處理(2、圖片的編碼解碼)

前言

    圖片的編碼:在當前APP的開發中,圖片是常常會使用到的,關於圖片有不少種格式,例如JPEG,PNG等。其實這些各類各樣的圖片格式都對應了位圖(bitmap)通過不一樣算法編碼(壓縮)後的圖片。(編碼這裏就不過多介紹了)html

    圖片的解碼:app從磁盤中讀入編碼後的圖片,須要通過解碼把圖片變成位圖(bitmap)讀入,這樣才能顯示在屏幕上。ios

    位圖(bitmap):位圖又被叫作點陣圖像,也就是說位圖包含了一大堆的像素點信息,這些像素點就是該圖片中的點,有了圖片中每一個像素點的信息,就能夠在屏幕上渲染整張圖片了。算法

1、圖片的本質

    圖片本質上是位圖,一堆像素點組成的二維數組,其中每一個像素點都記錄該點位的顏色等信息。顯示出來就是一張圖了。數組

    既然像素要存儲顏色數據,這裏就又引出一個顏色存儲格式的概念。咱們就以最簡單廣泛的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

2、位圖須要編碼本質

    經過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);

複製代碼

3、圖片須要解碼的本質及項目中實際應用

一、編碼後的圖片須要解碼的緣由:

編碼後的圖片須要顯示在屏幕上,咱們須要得到圖片全部信息,也就是對應編碼前的位圖。因此編碼後的圖片必需要通過解碼才能正常顯示。

二、2018WWDC 中說的三種 Buffer 理念:

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,這樣在大量圖片須要解碼的時候,可使得局部變量儘早釋放掉,不會形成內存峯值太高。

4、最後關於大圖顯示的痛點,蘋果對於大圖顯示的解決方案:

大圖顯示這個問題,看似和圖片編碼解碼無關。可是大的圖片會佔用較多的內存資源,解碼和傳輸到 GPU 也會耗費較多時間。 所以,實際須要顯示的圖像尺寸可能並非很大,若是能將大圖縮小,便能達到優化的目的。如下是WWDC給的大圖顯示方案,功能是縮小圖像並解碼:

一、Objective-C:

// 大圖縮小爲顯示尺寸的圖
- (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;
}
複製代碼

二、Swift

// 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)
}
複製代碼

參考文檔

相關文章
相關標籤/搜索