淺談iOS中圖片解壓縮從文件渲染到屏幕的過程

將一張圖片從磁盤中加載出來,並最終顯示到屏幕上,中間其實通過了一系列複雜的處理過程,從文件到屏幕,其中還包括了對圖片的解壓縮操做。數組

渲染圖片到屏幕的過程

圖片渲染過程

如上圖所示,圖片渲染到屏幕上,是CPU和GPU協做完成的。緩存

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

  • CPU:計算視圖 frame、圖片解碼、須要繪製紋理圖片經過數據總線交給 GPU
  • GPU:紋理混合、頂點變換與計算、像素點的填充計算、渲染到幀緩衝區
  • 時鐘信號:垂直同步信號 V-Sync / 水平同步信號 H-Sync
  • iOS 設備雙緩衝機制:顯示系統一般會引入兩個幀緩衝區,雙緩衝機制

什麼叫垂直同步信號和水平同步信號?框架

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

從上圖看,每一行從左到右就叫水平刷新,從上到下就叫垂直信號。整個屏幕刷新完畢,就會發出V-Sync信號。 能夠簡單的用超市買單的掃碼槍來理解,掃一個商品二維碼後就會出現一個H-Sync信號,掃完全部的商品二維碼後,計算商品總價後就至關於發送一個V-Sync信號。 從CRT顯示器的顯示原理來看,單個像素組成了水平掃描線,水平掃描線在垂直方向的堆積造成了完整的畫面。顯示器的刷新率受顯卡DAC控制,顯卡DAC完成一幀的掃描後就會產生一個垂直同步信號。ide

圖片加載的工做流程

iOS從磁盤加載一張圖片,使用UIImageView顯示在屏幕上,加載流程以下:函數

  1. 咱們使用 +imageWithContentsOfFile:(使用Image I/O建立CGImageRef內存映射數據)方法從磁盤中加載一張圖片,此時,圖像還沒有解碼。;在這個過程當中先從磁盤拷貝數據到內核緩衝區,再從內核緩衝區複製數據到用戶空間oop

  2. 生成UIImageView,把圖像數據賦值給UIImageView,若是圖像數據未解碼(PNG/JPG),解碼爲位圖數據性能

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

  4. 在主線程的下一個 runloop 到來時,Core Animation 提交了這個隱式的 transaction ,這個過程可能會對圖片進行 copy 操做,若是數據沒有字節對齊,Core Animation會再拷貝一份數據,進行字節對齊,可能會涉及到如下操做:ui

    • 分配內存緩衝區用於管理文件 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…

相關文章
相關標籤/搜索