篇1:SDWebImage源碼看圖片解碼

導語:這是SDWebImage源碼理解的第一篇,本篇先介紹圖片解碼相關的背景知識,然后介紹SDWebImage中解碼的源碼及其解碼相關的問題。

一、背景知識

在SDWebImage中處理圖片解碼的是SDWebImageDecoder

1、圖片加載
  • iOS 提供了兩種加載圖片方法,分別是UIIImage的imageNamed: 和 UIIImage的imageWithContentsOfFile:

  • 其中,imageNamed: 方法的特點在于可以緩存已經加載的圖片;使用時,先根據(jù)文件名在系統(tǒng)緩存中尋找圖片,如果找到了就返回;如果沒有,從Bundle內找到該文件,在渲染到屏幕時才解碼圖片,并將解碼結果保留到緩存中;當收到內存警告時,緩存會被清空。當頻繁加載同一張圖片時,使用imageNamed: 效果比較好。而imageWithContentsOfFile:僅加載圖片,不緩存圖像數(shù)據(jù)。

  • 雖然imageNamed: 方法利用緩存優(yōu)化了圖片的加載性能,但是第一次加載圖片時,只在渲染的時候才在主線程解碼,性能并不高效,尤其是在列表中加載多張高分辨率的圖片(大圖),可能會造成卡頓;

    說明:這里拋出圖片解碼的概念,SDWebImageDecoder這個類是為了優(yōu)化解碼效率存在的。

2、圖片解碼
  • 圖像可以分為矢量圖位圖,顯示到屏幕中的圖像是位圖圖像,位圖圖片格式有RGB、CMYK等顏色模式;其中RGB是最常用的顏色模式,它通過紅(R)、綠(G)、藍(B)三個顏色通道的數(shù)值表示顏色。手機顯示屏使用自帶Aphal通道(RGBA)的RGB32格式。

  • 在項目中,通常使用的圖片是JPG或PNG壓縮格式,它們是經過編碼壓縮后的圖片格式;而圖片顯示到屏幕之前,需要將JPG/PNG格式的圖片解碼位圖圖像;這個解碼工作是比較耗時的,而且不能使用GPU硬解碼,只能通過CPU軟解碼實現(xiàn)(硬解碼是通過解碼電路實現(xiàn),軟解碼是通過解碼算法、CPU的通用計算等方式實現(xiàn)軟件層面的解碼,效率不如GPU硬解碼)。

  • iOS默認會在UI主線程對圖像進行解碼,解碼后的圖像大小和圖片的寬高像素有關,寬高像素越大,位圖圖像就越大。假設一個3MB的圖片,其寬高像素為2048 * 2048的圖片,解碼后的位圖圖像大小是16MB(2048 * 2048 * 4)。

    //位圖大小的計算公式,其中bytesPerPixel = 4B
    bitmap_size = imageSize.width * imageSize.height * bytesPerPixel
    
  • 優(yōu)化解碼耗時的思路是:將耗時的解碼工作放在子線程中完成。SDWebImage和FastImageCache就是這么做的。具體的解碼工作就是SDWebImageDecoder負責的。

3、圖片重采樣
  • 在圖片顯示到屏幕前,除了要在主線程中解碼,還會在主線程中完成重采樣的工作。重采樣算法一般有: Nearest Neighbour Resampling (最鄰近重采樣)、 Bilinear Resampling(雙線性/兩次線性重采樣)、Bicubic Resampling (雙立方/兩次立方重采樣)等。

  • Nearest Neighbour Resampling比較簡單暴力,根據(jù)目標圖像的寬高 與源圖像的寬高比值,取源圖像相對位置的像素點的值作為目標像素點的值;而Bilinear Resampling參考源像素位置周圍4個點的值,按一定權重獲得目標圖像像素點的值;而Bicubic Resampling參考源像素點周圍4*4個點的值,按一定權重獲得目標圖像像素點的值。

  • 圖像的放大和縮小都會引起重采樣,放大圖像稱為上采樣/插值(upsamping),縮小圖像稱為小采樣(downsampling)。當圖片的size和imageView的size不同時,發(fā)生重采樣。

4、總結
  • 總結1SDWebImage 利用空間換時間的做法,在子線程中解碼圖片并緩存位圖結果,避免圖片的重復解碼,提升圖片展示性能。如果列表中需要展示很多網絡圖片,SDWebImage這種做法,有利于提高列表的流暢度。

  • 總結2:SDWebImage中下載的圖片,即使解碼縮放(decodedAndScaledDownImageWithImage:)后,圖片大小 未必 和imageView的大小相同,這會引發(fā)重采樣,我們可以在圖片顯示前,將圖片裁剪成和imageView的大小相同,提升性能(一個小的優(yōu)化點)。

二、源碼說明

SDWebImageDecoder的源碼有200多行,重要的函數(shù)兩個。其一是:(默認)解碼圖片辦法decodedImageWithImage: ;其二是:處理大圖縮放和解碼辦法decodedAndScaledDownImageWithImage:。

1、decodedImageWithImage:函數(shù)
  • decodedImageWithImage實現(xiàn)了PNG、GIF、TIFF三類圖片解碼的問題,這是SDWebImage默認的解碼操作;當它解碼高分辨率圖片,會導致內存暴增,甚至Crash。它造成的問題就是網上很多博客說的,“SDWebImage加載大圖(高分辨率圖),內存暴漲,導致加載失敗的問題”。

  • decodedImageWithImage函數(shù)很簡單,主要步驟可以看成:先過濾掉不符合解碼條件;再獲得圖片的信息;最后繪制出位圖圖片。

    1)shouldDecodeImage:過濾點不適合解碼的image,分別是:image為nil、animated images 或 有透明度的圖片
    2)獲取圖片的像素寬高(width、height)、顏色空間(colorspaceRef) 、行字節(jié)數(shù)(bytesPerRow,4 * width)等數(shù)據(jù)。
    3)使用創(chuàng)建CGBitmapContextCreate沒有透明度的位圖上下文,然后在該上下文中繪制出圖像。
    

說明:解碼操作在@autoreleasepool中,可以使得局部變量能盡早釋放掉,避免內存峰值過高。

2、decodedAndScaledDownImageWithImage:函數(shù)處理流程
  • decodedAndScaledDownImageWithImage解決了PNG、GIF、TIFF三類高分辨率圖片因解碼導致內存暴增的問題,采用辦法是:將大的原圖縮放成指定大小的圖片(個人感覺,思路應該來自蘋果的LargeImageDownsizing)。

  • decodedAndScaledDownImageWithImage: 函數(shù)中主要步驟可以看成:先過濾掉不符合解碼條件,位圖大小不達標的(小于60MB);再將原圖按照固定大小分割,然后依次繪制到目標畫布上(這部分最關鍵)。

  • 在裁剪繪制過程中,主要步驟如下:

    1)根據(jù)sourceTotalPixels(原圖像素大小)和kDestTotalPixels(60MB對應的像素大小)獲取imageScale;
    2)根據(jù)imageScale和原圖的像素寬高獲取 目標圖的大小destResolution.size, 并創(chuàng)建目標位圖上下文;
    3)獲得原圖分割圖的size(sourceTile.size),寬度和原圖寬一樣,高度是 (int)(kTileTotalPixels / sourceTile.size.width ),其中 kTileTotalPixels為20MB
    4)獲取目標分割圖的size(destTile.size),寬度和目標圖寬一樣,高度是sourceTile.size.height * imageScale
    5)根據(jù)原圖高(sourceResolution.height)除以原圖分割圖的高(sourceTile.size.height)獲得獲取分割塊的個數(shù)iterations,如果還有余數(shù),分割塊個數(shù)(iterations)載累加1。
    6)從原圖中裁剪出指定大小的分割圖,然后繪制到目標上下文的指定位置。
    

    注意:Core Graphics的坐標系則是y軸向上的,UIKit框架坐標系是y軸向下的;使用CGContextDrawImage將sourceTileImageRef繪制到destContext中,為了避免圖片上下文顛倒,注意destTile.origin.y和sourceTile.origin.y的計算方式

    for( int y = 0; y < iterations; ++y ) {
        @autoreleasepool {
            //注意sourceTile.origin.y和destTile.origin.y的計算
            sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
            destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
            sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, 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 );
        }
    }
    
3、CGBitmapContextCreate創(chuàng)建位圖上下文

函數(shù)原型CGBitmapContextCreate(void *data,size_t width,size_t height,size_t bitsPerComponent,size_t bytesPerRow,CGColorSpaceRef colorspace,CGBitmapInfo bitmapInfo)

  • 參數(shù)data:渲染目標的內存地址,內存塊大小至少是(bytesPerRow * height) 個字節(jié);一般傳遞NULL,讓系統(tǒng)去分配和釋放內存空間,避免內存泄漏問題。
  • 參數(shù)width和height分別是:位圖的寬高像素;
  • 參數(shù)bitsPerComponent是:位圖像素中每個組件的位數(shù)(number of bits)。對于32位像素格式和RGB 顏色空間,這個值是8;
  • 參數(shù)bytesPerRow:在內存中,位圖每一行所占的字節(jié)數(shù)。
  • 參數(shù) colorspace:位圖的顏色空間。
  • 參數(shù) bitmapInfo:指出該位圖是否包含 alpha 通道和它是如何產生的(RGB/RGBA/RGBX…),還有每個通道應該用整數(shù)標識還是浮點數(shù)。值為kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast,表示著新的位圖圖像不使用后面8位的 alpha 通道的。

說明:一個新的位圖上下文的像素格式由三個參數(shù)決定:每個組件的位數(shù)(bitsPerComponent),顏色空間(colorspace),alpha選項(bitmapInfo),alpha值決定了繪制像素的透明性。

三、解碼高分辨率圖的擔憂

1、“被嫌棄”的解碼
  • 網絡上,很多博客都說到了使用SDWebImage加載(高分辨率)圖片,發(fā)生內存暴漲,甚至導致Crash的問題;提出的解決的辦法是,關閉解碼操作。

    // 關閉解碼操作
    [[SDImageCache sharedImageCache] setShouldDecompressImages:NO];
    [[SDWebImageDownloader sharedDownloader] setShouldDecompressImages:NO];
    
  • 關閉解碼操作,將decodedImageWithImage解碼高分辨率圖的內存問題避開了,但是這意味著:如果大圖多次加載顯示,意味著在主線程要多次重復解碼(這好像不是什么好事);此外顯示大圖時,App依然會占用大量的內存,還可能造成卡頓;放棄SDWebImage解碼并不能保證能應對所有高分辨圖。所以說,關閉解碼操作并不是一個很好的選擇

2、加載高分辨率圖問題
  • 項目中,加載高分辨率圖不可避免,如后臺下發(fā)給我們一張大圖(高分辨率圖);主線程解碼(默認)可能導致卡頓;子線程解碼可能因內存暴漲而Crash。解決辦法可以參考 LargeImageDownsizing。該Demo展示加載顯示一個高分辨率(7033 × 10110 像素,位圖大小271MB)圖片的做法;其主要優(yōu)化思路是:將大的原圖縮放成指定大小的圖片。decodedAndScaledDownImageWithImage就是采用這種思路。

  • decodedAndScaledDownImageWithImage:函數(shù)中,為了避免內存暴增,將原圖裁剪成多個小圖,然后依次繪制到目標位圖context中。項目中,我更愿意decodedAndScaledDownImageWithImage方法去解碼高清晰圖片,而不愿意禁止解碼操作。

    //SDWebImageOptions選擇SDWebImageScaleDownLargeImages,處理網絡高分辨率圖
    [self.imageView sd_setImageWithURL:url placeholderImage:nil options:SDWebImageScaleDownLargeImages];
    
  • Apple提供了一個異步繪制內容的圖層CATiledLayer,不需要加載全部圖片,可以將大圖分解成小圖片,然后再載入顯示,具體參考下CATiledLayer

3、需要考慮的問題

憑心而論,后臺不經處理,任意下發(fā)高分辨率大圖這類事發(fā)生可能性很少;絕大部分場景下,iOS設備上不需要分辨率過高的圖(iPhone X的屏幕尺寸也不過是1125px × 2436px),那我們應該考慮什么呢。

  • 考慮1:因為SDWebImage支持并發(fā)的解碼操作,同時解碼多張分辨率中等圖片,占用的內存空間比較大,可能會給內存帶來壓力(小圖不必擔心)。可行的處理辦法是,限制并發(fā)解碼的個數(shù)

  • 考慮2:如果后臺下發(fā)的圖片是帶透明度的圖片,SDWebImage并不會去做解碼,這樣的圖片只能讓iOS系統(tǒng)去解碼。我能想到的辦法:盡可能讓后臺下發(fā)不透明的圖片

四、解碼中的小問題

SDWebImageDecoder的解碼工作中,有兩個小問題值得留意一下。

1、顏色空間的問題
  • 在創(chuàng)建位圖,選用顏色空間時,如果圖片的顏色空間模式是kCGColorSpaceModelUnknown(未知)、kCGColorSpaceModelMonochrome、kCGColorSpaceModelCMYK和kCGColorSpaceModelIndexed,默認使用設備RGB顏色空間(通過CGColorSpaceCreateDeviceRGB獲得),詳見代碼:

    + (CGColorSpaceRef)colorSpaceForImageRef:(CGImageRef)imageRef {
      // current
      CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
      CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);  
      BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown ||
                                    imageColorSpaceModel == kCGColorSpaceModelMonochrome ||
                                    imageColorSpaceModel == kCGColorSpaceModelCMYK ||
                                    imageColorSpaceModel == kCGColorSpaceModelIndexed);
      if (unsupportedColorSpace) {
          colorspaceRef = CGColorSpaceCreateDeviceRGB();
          CFAutorelease(colorspaceRef);
      }
      return colorspaceRef;
    }
    

這么做的原因,我認為只要有兩點

  • RGB顏色模式幾乎包括了人類視力所能感知的所有顏色,而SDWebImageDecoder中主要支持PNG、JPG、TIFF常見圖片格式的解碼,它們大部分采用RGB色彩模式;目前手機屏、電腦顯示屏大都采用了RGB顏色模式。

  • 對應Monochrome、CMYK和Indexed這樣的模式,使用設備RGB顏色空間(device color space),其結果是可以接受的。一張灰度圖片,顏色空間模式是kCGColorSpaceModelMonochrome,使用設備灰度顏色空間設備RGB顏色空間都可以,只是使用設備灰度顏色空間內存上效果會好一些。

2、解碼不透明圖片的問題
  • SDWebImage中不解碼透明的圖片,猜測原因是:在UI渲染視圖時,如果某個layer透明時,需要疊加計算下方多層的像素;如果某個layer不透明,可以忽略掉下方的圖層,減少了GPU像素混合計算。使用CGBitmapContextCreate 時 bitmapInfo 參數(shù)設置為忽略掉 alpha 通道,繪制出不透明的位圖圖片,在渲染視圖,能減少GPU的計算,提高性能。

End

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容