關於圖片的一些知識點

如果說之前的項目中哪個 bug 讓我記憶猶新,我會毫不猶豫的說是內存溢出(OOM),因爲當時無論從 dSYM 還是第三方的報錯信息中我都找不出問題是所在,而且開發過程中也極少遇到,現在知道當時遇到的是高分辨率的圖片集中渲染導致的 OOM。

內存溢出從字面上就很好理解,傳統意義上的 OOM 就是當前使用的 App 達到了 “high water mark”,也就是達到了系統對單個 App 的內存限制,系統會把這個應用殺掉(由 Jetsam 執行)。簡單的說完關於 OOM 的知識點,接下來是本文要探討一些關於圖片渲染的一些知識點。

現在大家都應該知道圖片從讀取到最終渲染都會經歷解壓的過程,大致過程如下(圖片來自於Image and Graphics Best Practices

AboutImage-8

Decode 過程簡單說就是把圖片轉化成 Bitmap,那麼 Bitmap 具體是什麼?

Bitmap

Wikipedia 有這麼一段解釋

In computing, a bitmap is a mapping from some domain (for example, a range of integers) to bits. It is also called a bit array or bitmap index.The more general term pix-map refers to a map of pixels,

通俗點講 bitmap 就是像素圖,通過以下方法我們可以得到一張圖片的 bitmap 信息

extension UIImage {
    
    var decodeData: Data? {
        guard let cgimage = cgImage
            , let dataProvider = cgimage.dataProvider
            , let rawData = dataProvider.data as Data? else {
                return nil
        }
        return rawData
    }
}

拿下面這張 48 * 48 的圖片爲例

image-1

通過上面提供的方法最終獲取到的 bitmap 信息前一小段是這樣的(這樣子分割開來的十六進制數據一共有 48 * 48 個),事實上這些數據對應的就是各個像素上要顯示的顏色信息

ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff fffbf9ff ffd5c8ff ffb19bff ff9b7eff ff8765ff ff7e59ff ff7750ff ff7750ff ff7e59ff ff8765ff ff9b7eff ffb19bff ffd5c8ff fffbf9ff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff fff6f3ff ffcbbcff ff9f84ff ff7953ff ff734bff ff734bff ff734bff ff734bff ff734bff ff734bff ff734bff ff734bff ff734bff ff734bff ff734bff ff734bff ff7953ff ff9f84ff ffcbbcff fff6f3ff ffffffff ffffffff ffffffff

驗證一下,前十七個像素點都是白色,這符合預期,因爲左上角都是白色區域,到了第十八個像素點的時候變成了 fffbf9ff,我們把圖片放大後對比下

AboutImage-2

第十八個像素點是fffbf9ff,具體的,這個色值就是就是上面所顯示的淺紅色,最後兩位 ff 代表的 alpha 值,到現在算是搞清楚了 bitmap 到底是什麼。

內存佔用

回到文章開頭提到的高分辨率圖片的解壓導致 OOM 問題,首先一個問題是:一張圖片解壓需要消耗多少內存?答案是 水平象素 × 垂直象素 × 4 單位是 byte。我們來驗證一下,這次換一張稍大的彩色 PNG 圖片,它的分辨率是 400 350,所以理論上解壓需要的內存大小是 400 350 * 4 = 547k,測試設備是 iPhone X 12.3.0,通過 Instruments 的 Allocations 檢測到結果是 560k 這非常接近於理論值

AboutImage-3

爲什麼要強調彩色圖片呢,或許你已經猜到了解壓一張彩色圖片和解壓一張只有黑白組成的照片是所消耗的性能是不一樣的,對的,比如下面這張圖片同樣是 400 * 350 的 PNG 圖片

AboutImage-4

解壓消耗的內存只要288k,計算方法 400 350 2 = 273k

AboutImage-5

這時候可能有些小夥伴會想,這樣子也可以的話,那是不是純紅,純綠或者純藍消耗的內存大小也一樣的,抱歉,不是的,要解釋這個問題需要引入顏色空間的概念,蘋果目前支持的顏色空間有下面這種方式

  • Gray spaces, used for grayscale display and printing; see Gray Spaces
  • RGB-based color spaces, used mainly for displays and scanners; see RGB-Based Color Spaces
  • CMYK-based color spaces, used mainly for color printing; see CMY-Based Color Spaces
  • Device-independent color spaces, such as Lab, used mainly for color comparisons, color differences, and color conversion; see Device-Independent Color Spaces
  • Named color spaces, used mainly for printing and graphic design; see Named Color Spaces
  • Heterogeneous HiFi color spaces, also referred to as multichannel color spaces, primarily used in new printing processes involving the use of red-orange, green and blue, and also for spot coloring, such as gold and silver metallics; see Color-Component Values, Color Values, and Color

具體到 iOS 有這幾個方式

CS Pixel format and bitmap information constant Availability
Null 8 bpp, 8 bpc, kCGImageAlphaOnly Mac OS X, iOS
Gray 8 bpp, 8 bpc, kCGImageAlphaNone Mac OS X, iOS
Gray 8 bpp, 8 bpc, kCGImageAlphaOnly Mac OS X, iOS
RGB 16 bpp, 5 bpc, kCGImageAlphaNoneSkipFirst Mac OS X, iOS
RGB 32 bpp, 8 bpc, kCGImageAlphaNoneSkipFirst Mac OS X, iOS
RGB 32 bpp, 8 bpc, kCGImageAlphaNoneSkipLast Mac OS X, iOS
RGB 32 bpp, 8 bpc, kCGImageAlphaPremultipliedFirst Mac OS X, iOS
RGB 32 bpp, 8 bpc, kCGImageAlphaPremultipliedLast Mac OS X, iOS

回到前面的計算方式,在表格中可以看到每個顏色空間(CS)對應的像素格式(Pixel format)和一些常量(bitmap information constant),包括每種顏色空間對應的每像素總 bit 數(bpp)等 ,對於彩色圖片來說,解壓它所需要用到的必然是 RGB ,對應的就是 32 bpp(關於 16 bpp 後面會提到), 32 bits / 8 = 4 bytes,所以計算方式就是 水平象素 × 垂直象素 × 4

那麼對於只有黑白組成的圖片,對應的就是灰度顏色空間(Gray),官方解釋

Gray spaces typically have a single component, ranging from black to white, as shown in Figure 2-1. Gray spaces are used for black-and-white and grayscale display and printing. A properly plotted gray space should have a fifty percent value as its midpoint.

image

所以內存佔用計算方式就是 水平象素 × 垂直象素 × 2 (爲什麼表格給的是 8 bpp),這種解釋方式同樣適用於 UILabel 的渲染,不信可以試試紅色文字的顯示和黑白文字的顯示需要佔用的內存。

另外,或許你會想到 UIColor 的有個通過 HSB 顏色空間初始化的方法,貌似違背以上的說法

public init(hue: CGFloat, saturation: CGFloat, brightness: CGFloat, alpha: CGFloat)

HSB 確實是一種顏色空間,但是也是基於 RGB,在維基百科有相應的解釋,以及官方文檔也提到最終還是 RGB。

RGB

前面提到的 RGB 顏色空間,每像素擁有的總 bit 數並不一定都是 32 bits,也有一種特殊情況是 16 bits。首先 32 bpp 的意思就是,在 R G B 三個顏色上都用 8 bits 去表示,比如 Red 顏色有 2^8 個數去表示,也就是 0 - 255 個數值,當然剩下的 8 bits 留給了 alpha。那麼 16 bpp 就是在 R G B 上分別使用 5 6 5 位去去表示,至於爲什麼 G 分到 6 位而不是其他的,據說是因爲人類的眼睛對綠色比較敏感。所以它們還有另外一種表述形式叫做 RGB888 和 RGB565。

RGB888 示意圖

AboutImage-6

RGB565 示意圖

AboutImage-7

比如我們需要創建一個 bitmap 來表示 RGB565,以十六進制 0x001f 表示其中的一個像素,轉化爲二進制就是 11111,這時候它表示並不是紅色,而是藍色,因爲如果高位不夠就會用 0 來補,左邊 -> 右邊 就是高位 -> 低位,所以最終其實是 0000000000011111 來表示一個像素的顏色,也就是藍色。知道這些後我們可以動手創建一個RGB565 的 bitmap

    static func makeData() -> UnsafeMutablePointer<UInt16> {
        let capacity = 200 * 200 * 2
        let imageBuffer = UnsafeMutablePointer<UInt16>.allocate(capacity: capacity)
        for row in 0..<200 {
            let color: UInt16 = row >= 100 ? 0x001f : 0x7e0
            for col in 0..<200 {
                imageBuffer[row * 200 + col] = color
            }
        }
        free(imageBuffer)
        return imageBuffer
    }

這裏設置 200 * 200 分辨率的圖片上半部分是 0x7e0 也就是 11111100000 綠色,下半部分是 0x001f 就是剛纔說的藍色,然後通過 CGImage 生成圖片。

static var RGB565Image: UIImage? {
        let width = 200
        let height = 200
        let rawData = makeData()
        guard let data = Data(bytes: rawData, count: width * height * 2) as CFData?
            , let provider = CGDataProvider(data: data) else {
                return nil
        }
        
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo =  CGBitmapInfo(rawValue: CGImageAlphaInfo.noneSkipFirst.rawValue | CGBitmapInfo.byteOrder16Little.rawValue)
        let imageRef = CGImage(width: width,
                               height: height,
                               bitsPerComponent: 5,
                               bitsPerPixel: 16,
                               bytesPerRow: width * 2,
                               space: colorSpace,
                               bitmapInfo: bitmapInfo,
                               provider: provider,
                               decode: nil,
                               shouldInterpolate: false,
                               intent: CGColorRenderingIntent.defaultIntent
        )
        
        guard let cgImage = imageRef else {
            return nil
        }
        
        let finalImage = UIImage(cgImage: cgImage)
        return finalImage
    }

注意 CGBitmapInfo 的參數,不需要 alpha 通道 noneSkipFirst,以及 16 位小端模式 byteOrder16Little,這裏不做過多介紹,有興趣的可以搜一搜。

未完待續

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章