淺談iOS中圖片解壓縮從檔案渲染到螢幕的過程

傷心的Easyman發表於2019-09-04

將一張圖片從磁碟中載入出來,並最終顯示到螢幕上,中間其實經過了一系列複雜的處理過程,從檔案到螢幕,其中還包括了對圖片的解壓縮操作。

渲染圖片到螢幕的過程

圖片渲染過程

如上圖所示,圖片渲染到螢幕上,是CPU和GPU協作完成的。

CPU/GPU 等在這樣一次渲染過程中的具體分工:

  • CPU:計算檢視 frame、圖片解碼、需要繪製紋理圖片通過資料匯流排交給 GPU
  • GPU:紋理混合、頂點變換與計算、畫素點的填充計算、渲染到幀緩衝區
  • 時鐘訊號:垂直同步訊號 V-Sync / 水平同步訊號 H-Sync
  • iOS 裝置雙緩衝機制:顯示系統通常會引入兩個幀緩衝區,雙緩衝機制

什麼叫垂直同步訊號和水平同步訊號?

垂直同步訊號和水平同步訊號圖解

從上圖看,每一行從左到右就叫水平重新整理,從上到下就叫垂直訊號。整個螢幕重新整理完畢,就會發出V-Sync訊號。 可以簡單的用超市買單的掃碼槍來理解,掃一個商品二維碼後就會出現一個H-Sync訊號,掃完所有的商品二維碼後,計算商品總價後就相當於傳送一個V-Sync訊號。 從CRT顯示器的顯示原理來看,單個畫素組成了水平掃描線,水平掃描線在垂直方向的堆積形成了完整的畫面。顯示器的重新整理率受顯示卡DAC控制,顯示卡DAC完成一幀的掃描後就會產生一個垂直同步訊號。

圖片載入的工作流程

iOS從磁碟載入一張圖片,使用UIImageView顯示在螢幕上,載入流程如下:

  1. 我們使用 +imageWithContentsOfFile:(使用Image I/O建立CGImageRef記憶體對映資料)方法從磁碟中載入一張圖片,此時,影像尚未解碼。;在這個過程中先從磁碟拷貝資料到核心緩衝區,再從核心緩衝區複製資料到使用者空間

  2. 生成UIImageView,把影像資料賦值給UIImageView,如果影像資料未解碼(PNG/JPG),解碼為點陣圖資料

  3. 隱式CATransaction 捕獲到UIImageView layer樹的變化。

  4. 在主執行緒的下一個 runloop 到來時,Core Animation 提交了這個隱式的 transaction ,這個過程可能會對圖片進行 copy 操作,如果資料沒有位元組對齊,Core Animation會再拷貝一份資料,進行位元組對齊,可能會涉及到以下操作:

    • 分配記憶體緩衝區用於管理檔案 IO 和解壓縮操作

    • 將檔案資料從磁碟讀到記憶體中

    • 將壓縮的圖片資料解碼成未壓縮的點陣圖形式,這是一個非常耗時的 CPU 操作,並且解碼出來的圖片體積與圖片的寬高有關係,而與圖片原來的體積無關

    • 最後 Core Animation 中CALayer使用未壓縮的點陣圖資料渲染 UIImageView 的圖層

    • CPU計算好圖片的Frame,對圖片解壓之後.就會交給GPU,GPU處理點陣圖資料,進行渲染

  5. 渲染流程

    • GPU獲取圖片的座標
    • 將座標交給頂點著色器VertexShader (頂點計算)
    • 將圖片光柵化(獲取圖片對應螢幕上的畫素點,實際繪製或填充每個頂點之間的畫素)
    • 片元著色器FragmentShader計算(計算每個畫素點的最終顯示的顏色值)
    • 從幀快取區中渲染到螢幕上

可以看到在上面這個工作流程中體現了CPU和GPU的互相配合

CPU和GPU協作

為什麼需要解壓縮?

圖片的解壓縮是需要消耗大量CPU時間的,那為什麼還需要對圖片進行解壓縮操作呢? 首先需要了解到什麼是點陣圖:

點陣圖(Bitmap),又稱柵格圖(英語:Raster graphics)或點陣圖,是使用畫素陣列(Pixel-array/Dot-matrix點陣)來表示的影像。
複製程式碼

其實,點陣圖就是一個畫素陣列,陣列中的每個畫素就代表著圖片中的一個點。我們在應用中經常用到的 JPEG 和 PNG 圖片就是點陣圖。

獲取圖片的原始畫素資料,用以下程式碼:

UIImage *image = [UIImage imageNamed:@"text.png"];
CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));
複製程式碼

解壓縮後的圖片大小與原始檔案大小之間沒有任何關係,而只與圖片的畫素有關: 解壓縮後的圖片大小 = 圖片的畫素寬 * 圖片的畫素高 * 每個畫素所佔的位元組數

事實上,不管是 JPEG 還是 PNG 圖片,都是一種壓縮的點陣圖圖形格式。只不過 PNG 圖片是無失真壓縮,並且支援 alpha 通道,而 JPEG 圖片則是有失真壓縮,可以指定 0-100% 的壓縮比。值得一提的是,在蘋果的 SDK 中專門提供了兩個函式用來生成 PNG 和 JPEG 圖片:

// 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);   
複製程式碼

總結:在將磁碟中的圖片渲染到螢幕之前,必須先要得到圖片的原始畫素資料,才能執行後續的繪製操作,這就是為什麼需要對圖片解壓縮的原因。

解壓縮原理

當未解壓縮的圖片將要渲染到螢幕時,系統會在主執行緒對圖片進行解壓縮,而如果圖片已經解壓縮了,系統就不會再對圖片進行解壓縮。因此,也就有了業內的解決方案,在子執行緒提前對圖片進行強制解壓縮。

而強制解壓縮的原理就是對圖片進行重新繪製,得到一張新的解壓縮後的點陣圖。其中,用到的最核心的函式是 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);
複製程式碼

這個函式用於建立一個點陣圖上下文,用來繪製一張寬 width 畫素,高 height 畫素的點陣圖。

  • data :如果不為 NULL ,那麼它應該指向一塊大小至少為 bytesPerRow * height 位元組的記憶體;如果 為 NULL ,那麼系統就會為我們自動分配和釋放所需的記憶體,所以一般指定 NULL 即可;
  • width 和 height :點陣圖的寬度和高度,分別賦值為圖片的畫素寬度和畫素高度即可;
  • bitsPerComponent :畫素的每個顏色分量使用的 bit 數,在 RGB 顏色空間下指定 8 即可;
  • bytesPerRow :點陣圖的每一行使用的位元組數,大小至少為 width * bytes per pixel 位元組。有意思的是,當我們指定 0 時,系統不僅會為我們自動計算,而且還會進行 cache line alignment 的優化,更多資訊可以檢視為什麼CoreAnimation要位元組對齊? 為什麼我的影像的每行位元組數超過其每畫素的位元組數乘以其寬度?

YYImage\SDWebImage開源框架實現

用於解壓縮圖片的函式 YYCGImageCreateDecodedCopy 存在於 YYImageCoder 類中,核心程式碼如下

CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
    ...

    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 {
        ...
    }
}
複製程式碼

YYImage載入流程:

  • 獲取圖片二進位制資料
  • 建立一個CGImageRef物件
  • 使用CGBitmapContextCreate()方法建立一個上下文物件
  • 使用CGContextDrawImage()方法繪製到上下文
  • 使用CGBitmapContextCreateImage()生成CGImageRef物件。
  • 最後使用imageWithCGImage()方法將CGImage轉化為UIImage。

SDWebImage 中對圖片的解壓縮過程與上述完全一致,只是傳遞給 CGBitmapContextCreate 函式的部分引數存在細微的差別。

效能對比:

解壓PNG圖片,SDWebImage>YYImage 解壓JPEG圖片,SDWebImage<YYImage

總結

  1. 圖片檔案只有在確認要顯示時,CPU才會對齊進行解壓縮.因為解壓是非常消耗效能的事情.解壓過的圖片就不會重複解壓,會快取起來.
  2. UIImage有兩種快取,一種是UIImage類的快取,這種快取保證imageNamed初始化的UIImage只會被解碼一次。另一種是UIImage物件的快取,這種快取保證只要UIImage沒有被釋放,就不會再解碼。
  3. 圖片渲染到螢幕的過程: 讀取檔案->計算Frame->圖片解碼->解碼後紋理圖片點陣圖資料通過資料匯流排交給GPU->GPU獲取圖片Frame->頂點變換計算->光柵化->根據紋理座標獲取每個畫素點的顏色值(如果出現透明值需要將每個畫素點的顏色*透明度值)->渲染到幀快取區->渲染到螢幕

參考連結:

www.jianshu.com/p/72dd07472…

相關文章