iOS圖片記憶體優化

fgyong發表於2019-12-03

基於現在iOS11新生成的圖片都是HEIF,該圖片使用UIImage(named: name)已不在那麼優雅,圖片大小為1.8m大小的,讀進手機記憶體,直接飆升了45M,這是我們不想看到的結果,一個頁面有多個這樣子的圖的話,恐怕就是災難了。

既然原圖不能讀入,那麼如何可以用更少的記憶體和CPU來解決呢?

這就要先了解該圖片的編碼了。

HEIC HEIF

帶有後設資料的HEIF的另一種形式。HEIC檔案包含一個或多個以“高效影像格式”(HEIF)儲存的影像,該格式通常用於在移動裝置上儲存照片。它可能包含單個影像或影像序列以及描述每個影像的後設資料。最常使用副檔名“ .heic”,但HEIC檔案也可能顯示為.HEIF檔案

heicheif是廣色域圖片的格式,廣色域比sRGB表示範圍大25%,在廣色域裝置中能顯示更廣的色彩,sRGB 8bit/dept,廣色域達到16bit/dept。廣色域只是在硬體支援的情況下才能顯示的。 其實就是蘋果搞的一個更高效體積更小效率更高的壓縮方式。

載入

載入image,只是把檔案資訊載入到記憶體中,下一步就是解碼。在程式碼中體現就是

let image = UIImage(contentsOfFile: url.path)
或 載入圖片到記憶體 會常駐記憶體
let image = UIImage(named: name)!
複製程式碼

解碼

其實是發生在新增到要顯示的view上面才會解碼

let imageV = UIImageView.init(image: image)
imageV.frame = CGRect(x: 50, y: (250 * i) + 100, width: 200, height: 200)
self.view.addSubview(imageV)
複製程式碼

最後一行不寫,則不會解碼。

渲染

view顯示出來則是渲染。過程是解碼的data buffer 複製到frame buffer,硬體從幀緩衝區讀取資料顯示到螢幕上。

self.view.addSubview(imageV)
複製程式碼

記憶體暴漲原因

一部分圖片載入到記憶體,在解碼過程中出現了記憶體暴漲問題,今天探究一下原因和解決方案。

首先有請我們準備的素材和裝置(6s 64g版本)

A:jpg
20M 12000*12000

B:jpg
2.8M 3024*4032

C:HEIC
1.8M 3024*4032
複製程式碼

素材A

APP執行記憶體:13.8M
載入Image: 240.3M之後穩定到220M
CPU:峰值5%,隨後降低到0%
image佔記憶體:226.5M
複製程式碼

素材B

APP執行記憶體:13.7M
載入Image: 31.5
CPU:峰值5%,隨後降低到0%
image佔記憶體:17.8M
複製程式碼

素材C

APP執行記憶體:13.8M
載入Image: 32.3
CPU:峰值4%,隨後降低到0%
image佔記憶體:18.5M
複製程式碼

我們猜測是否是imageView的大小影響記憶體的呢? size改為原來的1/10結果執行記憶體還是和以前一樣。

為什麼呢?

記憶體大小不是取決於viewsize,而是原始檔案image size

iOS圖片記憶體優化

渲染格式

SRGB

每個畫素4位元組

display p3 寬色域

每個畫素8位元組,使用機型iphone7 、iphone8、iphone X及以後的裝置,不支援該格式的機型無法顯示該效果。

亮度和透明度

每個畫素2位元組,單一的色調和透明度,只能來顯示白色和黑色之間的色值,沒有其他顏色。

Alpha 8 Format

每個畫素1位元組,用來表示透明度,一般用作蒙版和文字。 相比sRGB容量小了75%,詳細 寬色域 容量小了87.5%

渲染圖片大小計算

圖片大小 = 圖片格式容量 * 畫素個數 當我們把大小是20*20使用Alpha 8 format渲染到20*20的view上面,和40*40的image使用p3渲染到20*20的view中,後著佔用記憶體是前者的8倍。

使用sRGB色域進行渲染所佔用的大小為

imageWidth*imageHeight*4 位元組
複製程式碼

每個畫素佔用了4位元組,每個位元組8位,

使用display p3則每個通道佔用16位,那麼佔用記憶體大小是

imageWidth*imageHeight*8 位元組
複製程式碼

如何選擇正確的圖片格式

不要主動選擇圖片格式,讓格式選擇你。

不要再使用UIGraphicsBeginImageContextWithOptions,該方法總是使用sRGB格式,你想節約記憶體是不行的,在支援p3的裝置上想繪製出來p3色域的圖片也是不行的。那麼使用UIGraphicsImageRenderer系統可以自動為你選擇格式,如果繪製image,自己再新增單色蒙版,是不需要另外單獨分配記憶體的。

if let im = imageV {
//第二次新增蒙版
	im.tintColor = UIColor.black
}else{
//繪製一個紅色矩形
	let bounds = CGRect(x: 0, y: 0, width: width, height: height)
	let renderer = UIGraphicsImageRenderer(bounds: bounds)
	 let image = renderer.image { (coxt) in
		UIColor.red.setFill()
		let path = UIBezierPath(roundedRect: bounds,
								cornerRadius: 20)
		path.addClip()
		UIRectFill(bounds)
	}
	imageV = UIImageView(image: image)
	imageV?.frame = bounds
	self.view.addSubview(imageV!)
}
複製程式碼

UIImage 直接讀出來需要將所有UIImagedata全部解碼到記憶體,很耗費記憶體和效能。為了節省記憶體和降低CPU使用率,可以採用下采樣

下采樣

image素材大小是1000*1000,但是在手機上顯示出來只有200*200,我們其實是沒必要將1000*1000的資料都解碼的,只需要縮小成200*200的大小即可,這樣子節省了記憶體和CPU,使用者感官也沒有任何影響。 在UIKit中使用UIGraphicsImageRenderer會有瞬間很高的記憶體和CPU峰值,那麼

1.UIKit UIGraphicsImageRenderer

使用素材A下采樣技術,使用UIKit中的UIGraphicsImageRenderer

Memory 
High:16.4M
normal:14.8M
CPU:
Hight:29%
normal:0%
複製程式碼
func resizedImage(at url: URL, for size: CGSize) -> UIImage? {
	guard let image = UIImage(contentsOfFile: url.path) else {
		return nil
	}
	if #available(iOS 10.0, *) {
		let renderer = UIGraphicsImageRenderer(size: size)
	
		return renderer.image { (context) in
			image.draw(in: CGRect(origin: .zero, size: size))
		}
	}else{
		UIGraphicsBeginImageContext(size)
		image.draw(in: CGRect(origin: .zero, size: size))
		let image = UIGraphicsGetImageFromCurrentImageContext()
		UIGraphicsEndImageContext()
		return image
	}
}
複製程式碼

用子執行緒繪製,會出現CPU略微升高,當image size大很多的時候會出現記憶體飆升然後慢慢恢復到normal

2.CoreGraphics CGContext上下文繪製縮圖

使用上下文繪製 cpu 和記憶體變化如下,CPU和記憶體沒有大的變動解決了該問題,也做到省電、順滑。

Memory 
High:42.3M
normal:14.1M
CPU:
Hight:6%
normal:0%
複製程式碼
func resizedImage2(at url: URL, for size: CGSize) -> UIImage?{
	guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
		let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
	else{
		return nil;
	}
	let cxt = CGContext(data: nil,
						width: Int(size.width),
						height: Int(size.height),
						bitsPerComponent: image.bitsPerComponent,
						bytesPerRow: image.bytesPerRow,
						space: image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!
		,
						bitmapInfo: image.bitmapInfo.rawValue)
	cxt?.interpolationQuality = .high
	cxt?.draw(image, in: CGRect(origin: .zero, size: size))
	guard let scaledImage = cxt?.makeImage() else {
		return nil
	}
	let ima = UIImage(cgImage: scaledImage)
	return ima
	
}
複製程式碼

3.ImageIO 建立縮圖

使用ImageIO 中建立影像,CPU和記憶體記錄反而更高了,記憶體也居高不下,時間上基本2s才將影像繪製出來。

Memory 
High:320M
normal:221M
CPU:
Hight:73%
normal:0%
複製程式碼
func resizedImage3(at url: URL, for size: CGSize) -> UIImage?{
	
	let ops:[CFString:Any] = [kCGImageSourceCreateThumbnailFromImageIfAbsent:true,
							  kCGImageSourceCreateThumbnailWithTransform:true,
							  kCGImageSourceShouldCacheImmediately:true,
							  kCGImageSourceThumbnailMaxPixelSize:max(size.width, size.height)]
	guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
		let image = CGImageSourceCreateImageAtIndex(imageSource, 0, ops as CFDictionary) else {
			return nil;
	}
	let ima = UIImage(cgImage: image)
	printImageCost(image: ima)
	return ima
}
複製程式碼

4.CoreImage 濾鏡

使用濾鏡處理反而有點麻煩,在iOS不是專業處理影像的APP中略微臃腫,而且效能不是很好。在重複刪除新增操作,第二次出現了APP閃退問題。

Memory 
High:1.04G
normal:566M
CPU:
Hight:73%
normal:0%
複製程式碼
	func resizedImage4(at url: URL, for size: CGSize) -> UIImage?{
		let shareContext = CIContext(options: [.useSoftwareRenderer:false])
		
		 guard let image = CIImage(contentsOf: url) else { return nil }
		let fillter = CIFilter(name: "CILanczosScaleTransform")
		fillter?.setValue(image, forKey: kCIInputImageKey)
		fillter?.setValue(1, forKey: kCIInputScaleKey)
		guard let outPutCIImage = fillter?.outputImage,let outputCGImage = shareContext.createCGImage(outPutCIImage, from: outPutCIImage.extent) else { return nil }
		
		return UIImage(cgImage: outputCGImage)
	}
複製程式碼

5.使用 vImage 優化圖片渲染

使用vImage建立影像效能略低,記憶體使用較多,步驟麻煩,是我們該捨棄的。在記憶體只有1G的手機上恐怕要crash了。

Memory 
High:998.7M
normal:566M
CPU:
Hight:78%
normal:0%
複製程式碼
func resizedImage5(at url: URL, for size: CGSize) -> UIImage? {
    // 解碼源影像
    guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
        let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil),
        let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
        let imageWidth = properties[kCGImagePropertyPixelWidth] as? vImagePixelCount,
        let imageHeight = properties[kCGImagePropertyPixelHeight] as? vImagePixelCount
    else {
        return nil
    }

    // 定義影像格式
    var format = vImage_CGImageFormat(bitsPerComponent: 8,
                                      bitsPerPixel: 32,
                                      colorSpace: nil,
                                      bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.first.rawValue),
                                      version: 0,
                                      decode: nil,
                                      renderingIntent: .defaultIntent)

    var error: vImage_Error

    // 建立並初始化源緩衝區
    var sourceBuffer = vImage_Buffer()
    defer { sourceBuffer.data.deallocate() }
    error = vImageBuffer_InitWithCGImage(&sourceBuffer,
                                         &format,
                                         nil,
                                         image,
                                         vImage_Flags(kvImageNoFlags))
    guard error == kvImageNoError else { return nil }

    // 建立並初始化目標緩衝區
    var destinationBuffer = vImage_Buffer()
    error = vImageBuffer_Init(&destinationBuffer,
                              vImagePixelCount(size.height),
                              vImagePixelCount(size.width),
                              format.bitsPerPixel,
                              vImage_Flags(kvImageNoFlags))
    guard error == kvImageNoError else { return nil }

    // 優化縮放影像
    error = vImageScale_ARGB8888(&sourceBuffer,
                                 &destinationBuffer,
                                 nil,
                                 vImage_Flags(kvImageHighQualityResampling))
    guard error == kvImageNoError else { return nil }

    // 從目標緩衝區建立一個 CGImage 物件
    guard let resizedImage =
        vImageCreateCGImageFromBuffer(&destinationBuffer,
                                      &format,
                                      nil,
                                      nil,
                                      vImage_Flags(kvImageNoAllocate),
                                      &error)?.takeRetainedValue(),
        error == kvImageNoError
    else {
        return nil
    }

    return UIImage(cgImage: resizedImage)
}
複製程式碼

記憶體優化

圖片解碼後載入在記憶體中的資料需要在恰當的時機刪除掉,在合適的時機新增上,也是保持低記憶體使用率的手段。

在使用者撥打電話或者進入到其他APP中可以先刪除掉大圖片,等回來的時候再次新增也是不錯的選擇。

# 1
NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification,
									   object: nil,
									   queue: .main)
{[weak self] (note) in
	self?.unloadImage()
}
NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification,
									   object: nil,
									   queue: .main)
{[weak self] (note) in
	self?.loadImage()
}
# 2

override func viewWillAppear(_ animated: Bool) {
	super.viewWillAppear(animated)
	self.loadImage()
}
override func viewWillDisappear(_ animated: Bool) {
	super.viewWillDisappear(animated)
	self.unloadImage()
}
複製程式碼

DemoCode download from github

總結

  • 基於效能綜合考慮方法1是最簡單最合適的
  • 使用濾鏡和vImage略微複雜點,平時開發過程中可以不用考慮了。
  • 圖片解碼快取和圖片大小有關,適當的下采樣是不錯的選擇。

參考


廣告時間

iOS圖片記憶體優化

相關文章