一、內(nèi)存優(yōu)化的必要性
事實上,因為目前 iPhone 配備的內(nèi)存越來越高,當(dāng)內(nèi)存占用過高時,并不一定會超過系統(tǒng)設(shè)定的閾值而引發(fā)強(qiáng)殺進(jìn)程。
但這并不意味著減少內(nèi)存占用是沒有意義的,因為當(dāng)內(nèi)存占用過高時,很容易引起一系列的副作用。最直接的表現(xiàn)是 App Crash,當(dāng)然還有很多更為深遠(yuǎn)的副作用。
1. FOOM
FOOM 是最直接的影響了,當(dāng)內(nèi)存占用過多導(dǎo)致整個系統(tǒng)的可用內(nèi)存不足時,App所在的進(jìn)程容易被殺掉。而且相比于一般的 Crash 來說,F(xiàn)OOM 更難以檢測,并且也更難排查。
2. 限制并發(fā)數(shù)量
如果一個任務(wù)占用了過多的內(nèi)存,但總的內(nèi)存是有限的,那么任務(wù)的并發(fā)數(shù)將會受到直接限制。表現(xiàn)上就是 App 里某個功能可同時執(zhí)行的數(shù)量有限,或者可以同時顯示的內(nèi)容有數(shù)量限制。
同時,因為內(nèi)存是有限資源,當(dāng)占用內(nèi)存過多時,會容易導(dǎo)致操作系統(tǒng)殺掉其它 App 的進(jìn)程來給當(dāng)前的 App 提供足夠的內(nèi)存空間,這對用戶體驗是不利的。
3. 增加耗電
由于 iOS 系統(tǒng)的 Memory Compressor 的存在,當(dāng)可用內(nèi)存不足時,一部分 Dirty Page 會被壓縮存儲到磁盤中,當(dāng)用到這部分內(nèi)存時,再從磁盤里加載回來。這會造成 CPU 花費更多的時間來等待 IO, 間接提高 CPU 占用率,造成耗電。
二、原因分析
1. 圖片顯示原理
圖片其實是由很多個像素點組成的,每個像素點描述了該點的顏色信息。這樣的數(shù)據(jù)是可以被直接渲染在屏幕上的,稱之為 Image Buffer。
事實上,由于圖片源文件占用的存儲空間非常大,一般在存儲時候都會進(jìn)行壓縮,非常常見的就是 JPEG 和 PNG 算法壓縮的圖片。
因此當(dāng)圖片存儲在硬盤中的時候,它是經(jīng)過壓縮后的數(shù)據(jù)。經(jīng)過解碼后的數(shù)據(jù)才能用于渲染,因此需要將圖片顯示在屏幕上的話,需要先經(jīng)過解碼。解碼后的數(shù)據(jù)就是 Image Buffer 。
當(dāng)圖片顯示在屏幕上時,會復(fù)制顯示區(qū)域的Image Buffer去進(jìn)行渲染。
2. 圖片真實占用內(nèi)存
對于一張正在顯示在屏幕上的,尺寸為 1920*1080 的圖片來說,如果采用 SRGB 的格式(每個像素點的顏色由 red,green,blue,alpha 一個共 4 個 bytes 來決定)的話,那么它占用的內(nèi)存為:
也就是說,一張非常普通的圖片,解碼后占用的內(nèi)存就是 7.9 MB,這是非??鋸埖摹6鴪D片顯示時所占的內(nèi)存大小是與尺寸和顏色空間正相關(guān)的,與壓縮算法、圖片格式、圖片文件的大小沒有關(guān)聯(lián)。
三、解決方式
1. 避免將圖片放在內(nèi)存里
對于不顯示在屏幕上的圖片,在絕大部分時間里,其實是沒有必要放在內(nèi)存里的。解碼后的 UIImage 是非常大的,對于不需要顯示的圖片是不需要解碼的。而對于不顯示在屏幕上的圖片,一般也沒有必要繼續(xù)持有著 UIImage 對象。
2.圖片縮放
圖片縮放是很常見的處理方式,一般來說,常見的思想可能是重新畫一張小一點的圖片,往往是用 UIGraphicsBeginImageContextWithOptions的方式:
這種方式存在以下問題:
第一,默認(rèn)是 SRGB 的格式,也就是說每個像素需要占4個bytes的空間,對于一些黑白或者僅有alpha通道的數(shù)據(jù)來說是沒有必要的。
第二,需要將原圖片完全解碼后渲染出來,原圖片的解碼會造成內(nèi)存占用的高峰。
對于問題一的解決,可以使用新的 UIGraphicsImageRenderer 的方式,這種情況下框架會自動幫你選擇對應(yīng)的顏色格式,減少不必要的消耗。
這種方式在一定的場景有所優(yōu)化,但是沒有解決問題二中存在的內(nèi)存峰值的問題。由于處理前的圖片并不一定展示在屏幕上,解碼后的數(shù)據(jù)是冗余信息,因此應(yīng)該避免圖片的解碼。
對于峰值過高的問題,最直接的思想是采用流式的方式進(jìn)行處理。而底層的 ImageIO 的接口就采用了這種方式:
3. 降低峰值
通過 ARC 管理內(nèi)存的對象,注冊在某個 Autoreleasepool 中,Autoreleasepool 在 drain 的時候釋放已經(jīng)沒有使用的對象。
一般沒有進(jìn)行特殊處理的話,會在 Runloop 結(jié)束后,有一次 Autoreleasepool 的 drain 操作,而這次 Runloop 中生成的對象也是由這個 Autoreleasepool 來管理的。這部分的原理有很多的文章介紹,這里就不多贅述了。
在圖片批量處理的過程中,由于還在一個 Runloop 里,此時引用計數(shù)為 0 的對象是不會被釋放的。因此需要在每次循環(huán)后觸發(fā) Autoreleasepool 的 drain 操作:
4. 裁剪顯示的圖片
在很多場景下,圖片是不會完整的顯示出來的,例如下圖所示的情況:
在這種情況中,即使給 UIImageView 一張完整的圖片,最后渲染的時候也只會截取顯示區(qū)域的 Image Buffer 去進(jìn)行渲染。
這就意味著,區(qū)域外的數(shù)據(jù),其實是沒有必要的。因此在這種場景下,其實只需要裁減顯示區(qū)域的圖片即可。
舉個例子,以前面提到 1920 * 1080 的圖片為例, 顯示時需要占用的內(nèi)存為 829440 bytes。如果它是以 ScaleAspectFill 的方式放置在一個 300 x 300 的 UIImageView 中時,那么其實一張 300 x 300 的圖片就足以展示,而此時這張圖片占用的內(nèi)存為 360000 bytes, 僅為前者的 43% 。
上一篇:鍵盤上怎么打出頓號?
責(zé)任編輯: