iOS 下的圖片處理與性能優(yōu)化詳解
圖片在計(jì)算機(jī)世界中怎樣被存儲(chǔ)和表示?
圖片和其他所有資源一樣,在內(nèi)存中本質(zhì)上都是0和1的二進(jìn)制數(shù)據(jù),計(jì)算機(jī)需要將這些原始內(nèi)容渲染成人眼能觀察的圖片,反過(guò)來(lái),也需要將圖片以合適的形式保存在存儲(chǔ)器或者在網(wǎng)絡(luò)上傳送。
這種將圖片以某種規(guī)則進(jìn)行二進(jìn)制編碼的方式,就是圖片的格式。
常見(jiàn)的圖片格式
圖片的格式有很多種,除了我們熟知的 JPG、PNG、GIF,還有Webp,BMP,TIFF,CDR 等等幾十種,用于不同的場(chǎng)景或平臺(tái)。
這些格式可以分為兩大類(lèi):有損壓縮和無(wú)損壓縮。
有損壓縮:相較于顏色,人眼對(duì)光線亮度信息更為敏感,基于此,通過(guò)合并圖片中的顏色信息,保留亮度信息,可以在盡量不影響圖片觀感的前提下減少存儲(chǔ)體積。顧名思義,這樣壓縮后的圖片將會(huì)永久損失一些細(xì)節(jié)。最典型的有損壓縮格式是 jpg。
無(wú)損壓縮:和有損壓縮不同,無(wú)損壓縮不會(huì)損失圖片細(xì)節(jié)。它降低圖片體積的方式是通過(guò)索引,對(duì)圖片中不同的顏色特征建立索引表,減少了重復(fù)的顏色數(shù)據(jù),從而達(dá)到壓縮的效果。常見(jiàn)的無(wú)損壓縮格式是 png,gif。
除了上述提到的格式,有必要再簡(jiǎn)單介紹下 webp 和 bitmap這兩種格式:
Webp:jpg 作為主流的網(wǎng)絡(luò)圖片標(biāo)準(zhǔn)可以向上追溯到九十年代初期,已經(jīng)十分古老了。所以谷歌公司推出了Webp標(biāo)準(zhǔn)意圖替代陳舊的jpg,以加快網(wǎng)絡(luò)圖片的加載速度,提高圖片壓縮質(zhì)量。
webp 同時(shí)支持有損和無(wú)損兩種壓縮方式,壓縮率也很高,無(wú)損壓縮后的 webp 比 png 少了45%的體積,相同質(zhì)量的 webp 和 jpg,前者也能節(jié)省一半的流量。同時(shí) webp 還支持動(dòng)圖,可謂圖片壓縮格式的集大成者。
webp 的缺點(diǎn)是瀏覽器和移動(dòng)端支持還不是很完善,我們需要引入谷歌的 libwebp 框架,編解碼也會(huì)消耗相對(duì)更多的資源。
bitmap:bitmap 又叫位圖文件,它是一種*非壓縮*的圖片格式,所以體積非常大。所謂的非壓縮,就是圖片每個(gè)像素的原始信息在存儲(chǔ)器中依次排列,一張典型的1920*1080像素的 bitmap 圖片,每個(gè)像素由 RGBA 四個(gè)字節(jié)表示顏色,那么它的體積就是 1920 * 1080 * 4 = 1012.5kb。
由于 bitmap 簡(jiǎn)單順序存儲(chǔ)圖片的像素信息,它可以不經(jīng)過(guò)解碼就直接被渲染到 UI 上。實(shí)際上,其它格式的圖片一般都需要先被首先解碼為 bitmap,然后才能渲染到界面上。
如何判斷圖片的格式?
在一些場(chǎng)景中,我們需要手動(dòng)去判斷圖片數(shù)據(jù)的格式,以進(jìn)行不同的處理。一般來(lái)說(shuō),只要拿到原始的二進(jìn)制數(shù)據(jù),根據(jù)不同壓縮格式的編碼特征,就可以進(jìn)行簡(jiǎn)單的分類(lèi)了。以下是一些圖片框架的常用實(shí)現(xiàn),可以復(fù)制使用:
+ (XRImageFormat)imageFormatForImageData:(nullable NSData *)data { if (!data) { return XRImageFormatUndefined; } uint8_t c; [data getBytes:&c length:1]; switch (c) { case 0xFF: return XRImageFormatJPEG; case 0x89: return XRImageFormatPNG; case 0x47: return XRImageFormatGIF; case 0x49: case 0x4D: return XRImageFormatTIFF; case 0x52: if (data.length < 12) { return XRImageFormatUndefined; } NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding]; if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) { return XRImageFormatWebP; } } return XRImageFormatUndefined; }
UIImageView 的性能瓶頸
如上文所說(shuō),大部分格式的圖片,都需要被首先解碼為bitmap,然后才能渲染到UI上。
UIImageView 顯示圖片,也有類(lèi)似的過(guò)程。實(shí)際上,一張圖片從在文件系統(tǒng)中,到被顯示到 UIImageView,會(huì)經(jīng)歷以下幾個(gè)步驟:
- 分配內(nèi)存緩沖區(qū)和其它資源。
- 從磁盤(pán)拷貝數(shù)據(jù)到內(nèi)核緩沖區(qū)
- 從內(nèi)核緩沖區(qū)復(fù)制數(shù)據(jù)到用戶(hù)空間
- 生成UIImageView,把圖像數(shù)據(jù)賦值給UIImageView
- 將壓縮的圖片數(shù)據(jù),解碼為位圖數(shù)據(jù)(bitmap),如果數(shù)據(jù)沒(méi)有字節(jié)對(duì)齊,Core Animation會(huì)再拷貝一份數(shù)據(jù),進(jìn)行字節(jié)對(duì)齊。
- CATransaction捕獲到UIImageView layer樹(shù)的變化,主線程Runloop提交CATransaction,開(kāi)始進(jìn)行圖像渲染
- GPU處理位圖數(shù)據(jù),進(jìn)行渲染。
由于 UIKit 的封裝性,這些細(xì)節(jié)不會(huì)直接對(duì)開(kāi)發(fā)者展示。實(shí)際上,當(dāng)我們調(diào)用[UIImage imageNamed:@"xxx"]后,UIImage 中存儲(chǔ)的是未解碼的圖片,而調(diào)用 [UIImageView setImage:image]后,會(huì)在主線程進(jìn)行圖片的解碼工作并且將圖片顯示到 UI 上,這時(shí)候,UIImage 中存儲(chǔ)的是解碼后的 bitmap 數(shù)據(jù)。
而圖片的解壓縮是一個(gè)非常消耗 CPU 資源的工作,如果我們有大量的圖片需要展示到列表中,將會(huì)大大拖慢系統(tǒng)的響應(yīng)速度,降低運(yùn)行幀率。這就是 UIImageView 的一個(gè)性能瓶頸。
解決性能瓶頸:強(qiáng)制解碼
如果 UIImage 中存儲(chǔ)的是已經(jīng)解碼后的數(shù)據(jù),速度就會(huì)快很多,所以?xún)?yōu)化的思路就是:在子線程中對(duì)圖片原始數(shù)據(jù)進(jìn)行強(qiáng)制解碼,再將解碼后的圖片拋回主線程繼續(xù)使用,從而提高主線程的響應(yīng)速度。
我們需要使用的工具是 Core Graphics 框架的 CGBitmapContextCreate 方法和相關(guān)的繪制函數(shù)??傮w的步驟是:
A. 創(chuàng)建一個(gè)指定大小和格式的 bitmap context。
B. 將未解碼圖片寫(xiě)入到這個(gè) context 中,這個(gè)過(guò)程包含了*強(qiáng)制解碼*。
C. 從這個(gè) context 中創(chuàng)建新的 UIImage 對(duì)象,返回。
下面是 SDWebImage 實(shí)現(xiàn)的核心代碼,編號(hào)對(duì)應(yīng)的解析在下文中:
// 1. CGImageRef imageRef = image.CGImage; // 2. CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef]; size_t width = CGImageGetWidth(imageRef); size_t height = CGImageGetHeight(imageRef); // 3. size_t bytesPerRow = 4 * width; // 4. CGContextRef context = CGBitmapContextCreate(NULL, width, height, kBitsPerComponent, bytesPerRow, colorspaceRef, kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast); if (context == NULL) { return image; } // 5. CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // 6. CGImageRef newImageRef = CGBitmapContextCreateImage(context); // 7. UIImage *newImage = [UIImage imageWithCGImage:newImageRef scale:image.scale orientation:image.imageOrientation]; CGContextRelease(context); CGImageRelease(newImageRef); return newImage;
對(duì)上述代碼的解析:
1、從 UIImage 對(duì)象中獲取 CGImageRef 的引用。這兩個(gè)結(jié)構(gòu)是蘋(píng)果在不同層級(jí)上對(duì)圖片的表示方式,UIImage 屬于 UIKit,是 UI 層級(jí)圖片的抽象,用于圖片的展示;CGImageRef 是 QuartzCore 中的一個(gè)結(jié)構(gòu)體指針,用C語(yǔ)言編寫(xiě),用來(lái)創(chuàng)建像素位圖,可以通過(guò)操作存儲(chǔ)的像素位來(lái)編輯圖片。這兩種結(jié)構(gòu)可以方便的互轉(zhuǎn):
// CGImageRef 轉(zhuǎn)換成 UIImage CGImageRef imageRef = CGBitmapContextCreateImage(context); UIImage *image = [UIImage imageWithCGImage:imageRef]; // UIImage 轉(zhuǎn)換成 CGImageRef UIImage *image=[UIImage imageNamed:@"xxx"]; CGImageRef imageRef=loadImage.CGImage;
2、調(diào)用 UIImage 的 +colorSpaceForImageRef: 方法來(lái)獲取原始圖片的顏色空間參數(shù)。
什么叫顏色空間呢,就是對(duì)相同顏色數(shù)值的解釋方式,比如說(shuō)一個(gè)像素的數(shù)據(jù)是(FF0000FF),在 RGBA 顏色空間中,會(huì)被解釋為紅色,而在 BGRA 顏色空間中,則會(huì)被解釋為藍(lán)色。所以我們需要提取出這個(gè)參數(shù),保證解碼前后的圖片顏色空間一致。
3、計(jì)算圖片解碼后每行需要的比特?cái)?shù),由兩個(gè)參數(shù)相乘得到:每行的像素?cái)?shù) width,和存儲(chǔ)一個(gè)像素需要的比特?cái)?shù)4。
這里的4,其實(shí)是由每張圖片的像素格式和像素組合來(lái)決定的,下表是蘋(píng)果平臺(tái)支持的像素組合方式。
我們解碼后的圖片,默認(rèn)采用 kCGImageAlphaNoneSkipLast RGB 的像素組合,沒(méi)有 alpha 通道,每個(gè)像素32位4個(gè)字節(jié),前三個(gè)字節(jié)代表紅綠藍(lán)三個(gè)通道,最后一個(gè)字節(jié)廢棄不被解釋。
4、最關(guān)鍵的函數(shù):調(diào)用 CGBitmapContextCreate() 方法,生成一個(gè)空白的圖片繪制上下文,我們傳入了上述的一些參數(shù),指定了圖片的大小、顏色空間、像素排列等等屬性。
5、調(diào)用 CGContextDrawImage() 方法,將未解碼的 imageRef 指針內(nèi)容,寫(xiě)入到我們創(chuàng)建的上下文中,這個(gè)步驟,完成了隱式的解碼工作。
6、從 context 上下文中創(chuàng)建一個(gè)新的 imageRef,這是解碼后的圖片了。
7、從 imageRef 生成供UI層使用的 UIImage 對(duì)象,同時(shí)指定圖片的 scale 和orientation 兩個(gè)參數(shù)。
scale 指的是圖片被渲染時(shí)需要被壓縮的倍數(shù),為什么會(huì)存在這個(gè)參數(shù)呢,因?yàn)樘O(píng)果為了節(jié)省安裝包體積,允許開(kāi)發(fā)者為同一張圖片上傳不同分辨率的版本,也就是我們熟悉的@2x,@3x后綴圖片。不同屏幕素質(zhì)的設(shè)備,會(huì)獲取到對(duì)應(yīng)的資源。為了繪制圖片時(shí)統(tǒng)一,這些圖片會(huì)被set自己的scale屬性,比如@2x圖片,scale 值就是2,雖然和1x圖片的繪制寬高一樣,但是實(shí)際的長(zhǎng)是width * scale。
orientation 很好理解,就是圖片的旋轉(zhuǎn)屬性,告訴設(shè)備,以哪個(gè)方向作為圖片的默認(rèn)方向來(lái)渲染。
通過(guò)以上的步驟,我們成功在子線程中對(duì)圖片進(jìn)行了強(qiáng)制轉(zhuǎn)碼,回調(diào)給主線程使用,從而大大提高了圖片的渲染效率。這也是現(xiàn)在主流 App 和大量三方庫(kù)的最佳實(shí)踐。
總結(jié)
總結(jié)一下本文內(nèi)容:
- 圖片在計(jì)算機(jī)世界中被按照不同的封裝格式進(jìn)行壓縮,以便存儲(chǔ)和傳輸。
- 手機(jī)會(huì)在主線程中將壓縮的圖片解壓為可以進(jìn)行渲染的位圖格式,這個(gè)過(guò)程會(huì)消耗大量資源,影響App性能。
- 我們使用 Core Graphics 的繪制方法,強(qiáng)制在子線程中先對(duì) UIImage 進(jìn)行轉(zhuǎn)碼工作,減少主線程的負(fù)擔(dān),從而提升App的響應(yīng)速度。
和 UIImageView 類(lèi)似,UIKit 隱藏了很多技術(shù)細(xì)節(jié),降低開(kāi)發(fā)者的學(xué)習(xí)門(mén)檻,但另一方面,卻也限制了我們對(duì)一些底層技術(shù)的探究。文中提到的強(qiáng)制解碼方法,其實(shí)也是CGBitmapContextCreate 方法的一個(gè)『副作用』,屬于比較hack方式,這也是iOS平臺(tái)的一個(gè)局限:蘋(píng)果過(guò)于封閉了。
用戶(hù)對(duì)軟件性能(幀率、響應(yīng)速度、閃退率等等)其實(shí)非常敏感,作為開(kāi)發(fā)者,必須不斷探究性能瓶頸背后的原理,并且嘗試解決,移動(dòng)端開(kāi)發(fā)的性能優(yōu)化永無(wú)止境。
以上就是iOS 下的圖片處理與性能優(yōu)化詳解的詳細(xì)內(nèi)容,更多關(guān)于ios 圖片處理與性能優(yōu)化的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解使用Xcode進(jìn)行iOS設(shè)備無(wú)線調(diào)試
這篇文章主要介紹了詳解使用Xcode進(jìn)行iOS設(shè)備無(wú)線調(diào)試,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-12-12IOS 開(kāi)發(fā)中畫(huà)扇形圖實(shí)例詳解
這篇文章主要介紹了IOS 開(kāi)發(fā)中畫(huà)扇形圖實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2017-04-04IOS程序開(kāi)發(fā)之跳轉(zhuǎn)短信發(fā)送界面實(shí)現(xiàn)發(fā)送短信功能
在程序開(kāi)發(fā)中,我們經(jīng)常遇到這樣一功能:某個(gè)程序里面發(fā)送一些短信驗(yàn)證,那么基于代碼是如何實(shí)現(xiàn)的呢?下面小編通過(guò)本文給大家介紹IOS程序開(kāi)發(fā)之跳轉(zhuǎn)短信發(fā)送界面實(shí)現(xiàn)發(fā)送短信功能,有需要的朋友拿去用2016-01-01IOS實(shí)現(xiàn)上滑隱藏NvaigtionBar而下拉則顯示效果
這篇文章給大家介紹了如何實(shí)現(xiàn)APP上滑時(shí)隱藏navigationBar而下拉則又會(huì)顯示,雖然也是隱藏但是效果和其他完全不一樣,因?yàn)橐郧皼](méi)做過(guò)所以試著去實(shí)現(xiàn)一下,現(xiàn)在分享給大家,有需要的可以參考借鑒。2016-09-09IOS開(kāi)發(fā)之路--C語(yǔ)言存儲(chǔ)方式和作用域
只有你完全了解每個(gè)變量或函數(shù)存儲(chǔ)方式、作用范圍和銷(xiāo)毀時(shí)間才可能正確的使用這門(mén)語(yǔ)言。今天將著重介紹C語(yǔ)言中變量作用范圍、存儲(chǔ)方式、生命周期、作用域和可訪問(wèn)性。2014-08-08如何用IOS調(diào)用WebService(SOAP接口)
這篇文章主要介紹了如何用IOS調(diào)用WebService(SOAP接口),需要的朋友可以參考下2015-07-07ios NSNotificationCenter通知的簡(jiǎn)單使用
這篇文章主要介紹了ios NSNotificationCenter通知的簡(jiǎn)單使用,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-06-06iOS開(kāi)發(fā)中Subview的事件響應(yīng)以及獲取subview的方法
這篇文章主要介紹了iOS開(kāi)發(fā)中Subview的事件響應(yīng)以及獲取subview的方法,代碼基于傳統(tǒng)的Objective-C,需要的朋友可以參考下2015-09-09