圖片的存儲方式
圖片和其他所有資源一樣循头,在內(nèi)存中本質(zhì)上都是0和1的二進(jìn)制數(shù)據(jù)尖飞,用戶無法接觸到這些二進(jìn)制數(shù)據(jù)筛圆,他們看到的都是經(jīng)過某種二進(jìn)制編碼之后的圖片篙顺。這種將圖片以某種規(guī)則進(jìn)行二進(jìn)制編碼的方式面褐,就是圖片的格式拌禾。
常見的格式有:
JPEG
只支持有損壓縮,其壓縮算法可以精確控制壓縮比展哭,以圖像質(zhì)量換得存儲空間湃窍。
PNG
只支持無損壓縮,最大的優(yōu)勢在于支持完整的透明通道匪傍。
GIF
支持多幀動畫您市。
WebP
支持有損和無損壓縮、支持完整的透明通道役衡、無損壓縮后的 webp 比 png 少了45%的體積茵休,相同質(zhì)量的 webp 和 jpg,前者也能節(jié)省一半的流量。同時 webp 還支持動圖榕莺,可謂圖片壓縮格式的集大成者俐芯。缺點:WebP格式圖像的編碼時間很長,是JPEG的8倍钉鸯;瀏覽器和移動端支持還不是很完善泼各。
這里簡單介紹下有損壓縮和無損壓縮:
有損壓縮:相較于顏色,人眼對光線亮度信息更為敏感亏拉,基于此扣蜻,通過合并圖片中的顏色信息,保留亮度信息及塘,可以在盡量不影響圖片觀感的前提下減少存儲體積莽使。顧名思義,這樣壓縮后的圖片將會永久損失一些細(xì)節(jié)笙僚。最典型的有損壓縮格式是 jpg芳肌。
無損壓縮:和有損壓縮不同,無損壓縮不會損失圖片細(xì)節(jié)肋层。它降低圖片體積的方式是通過索引亿笤,對圖片中不同的顏色特征建立索引表,減少了重復(fù)的顏色數(shù)據(jù)栋猖,從而達(dá)到壓縮的效果净薛。常見的無損壓縮格式是 png,gif蒲拉。
如何判斷圖片的格式:
由原始的二進(jìn)制數(shù)據(jù)肃拜,根據(jù)不同壓縮方式的編碼特征,就可以拿到,以SDWebImage為例
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
if (!data) {
return SDImageFormatUndefined;
}
// File signatures table: http://www.garykessler.net/library/file_sigs.html
uint8_t c;
[data getBytes:&c length:1];
switch (c) {
case 0xFF:
return SDImageFormatJPEG;
case 0x89:
return SDImageFormatPNG;
case 0x47:
return SDImageFormatGIF;
case 0x49:
case 0x4D:
return SDImageFormatTIFF;
case 0x52: {
if (data.length >= 12) {
//RIFF....WEBP
NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
return SDImageFormatWebP;
}
}
break;
}
case 0x00: {
if (data.length >= 12) {
//....ftypheic ....ftypheix ....ftyphevc ....ftyphevx
NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(4, 8)] encoding:NSASCIIStringEncoding];
if ([testString isEqualToString:@"ftypheic"]
|| [testString isEqualToString:@"ftypheix"]
|| [testString isEqualToString:@"ftyphevc"]
|| [testString isEqualToString:@"ftyphevx"]) {
return SDImageFormatHEIC;
}
if ([testString isEqualToString:@"ftypmif1"] || [testString isEqualToString:@"ftypmsf1"]) {
return SDImageFormatHEIF;
}
}
break;
}
}
return SDImageFormatUndefined;
}
iOS中的圖片加載過程
圖片顯示到屏幕主要是依靠CPU和GPU協(xié)同合作完成的雌团。分工具體如下:
- CPU: 計算視圖frame燃领,圖片解碼,需要繪制紋理圖片通過數(shù)據(jù)總線交給GPU
- GPU: 紋理混合锦援,頂點變換與計算,像素點的填充計算猛蔽,渲染到幀緩沖區(qū)。
- 時鐘信號:垂直同步信號V-Sync / 水平同步信號H-Sync灵寺。
- iOS設(shè)備雙緩沖機制:顯示系統(tǒng)通常會引入兩個幀緩沖區(qū)曼库,雙緩沖機制。
開發(fā)中拿到的圖片大部分都是jpg替久,png凉泄,gif等經(jīng)過格式化的文件躏尉,這些圖片都是被壓縮過的蚯根,在渲染到屏幕之前,都需要先解碼成bitmap(未壓縮的位圖)
以UIImageView顯示一張圖片為例,會經(jīng)過以下步驟:
1.使用 +imageWithContentsOfFile:
方法從磁盤中加載一張圖片颅拦,這個時候的圖片并沒有解壓縮蒂誉。
2.將生成的image賦值給UIImageView。
3.接著一個隱式的 CATransaction 捕獲到了 UIImageView 圖層樹的變化距帅。
4.在主線程的下一個 runloop 到來時右锨,Core Animation 提交了這個隱式的 transaction ,這個過程可能會對圖片進(jìn)行 copy 操作碌秸,而受圖片是否字節(jié)對齊等因素的影響绍移,這個 copy 操作可能會涉及以下部分或全部步驟:
* 分配內(nèi)存緩沖區(qū)用于管理文件 IO 和解壓縮操作;
* 將文件數(shù)據(jù)從磁盤讀到內(nèi)存中讥电;
* 將壓縮的圖片數(shù)據(jù)解碼成未壓縮的位圖形式蹂窖,這是一個非常耗時的 CPU 操作;
* 最后 `Core Animation` 中`CALayer`使用未壓縮的位圖數(shù)據(jù)渲染 `UIImageView` 的圖層恩敌。
* CPU計算好圖片的Frame,對圖片解壓之后.就會交給GPU來做圖片渲染渲染流程
* GPU獲取獲取圖片的坐標(biāo)
* 將坐標(biāo)交給頂點著色器(頂點計算)
* 將圖片光柵化(獲取圖片對應(yīng)屏幕上的像素點)
* 片元著色器計算(計算每個像素點的最終顯示的顏色值)
* 從幀緩存區(qū)中渲染到屏幕上
圖片渲染到屏幕的過程: 讀取文件->cpu計算Frame->cpu圖片解碼->解碼后紋理圖片位圖數(shù)據(jù)通過數(shù)據(jù)總線交給GPU->GPU獲取圖片F(xiàn)rame->頂點變換計算->光柵化->根據(jù)紋理坐標(biāo)獲取每個像素點的顏色值(如果出現(xiàn)透明值需要將每個像素點的顏色*透明度值)->渲染到幀緩存區(qū)->渲染到屏幕
圖片的解壓縮是非常耗時的cpu操作瞬测,且默認(rèn)是在主線程執(zhí)行。當(dāng)在快速滑動的列表上有多張圖片顯示時纠炮,應(yīng)用的響應(yīng)性就會比較差月趟。
解壓縮是否必須
是必須的。因為將圖片渲染到屏幕之前恢口,必須拿到圖片的原始像素數(shù)據(jù)bitmap(位圖)孝宗。之前提到的jpg,png耕肩,gif碳褒,webp都是一種壓縮的位圖圖形格式。只不過png是無損壓縮看疗,jpg是有損壓縮沙峻。
位圖:
就是bitmap文件,它是一種非壓縮的圖片格式两芳。所謂的非壓縮摔寨,就是圖片每個像素的原始信息在存儲器中依次排列,比如1920*1080像素的 bitmap 圖片怖辆,每個像素由 RGBA 四個字節(jié)表示顏色是复,那么它的體積就是 1920 * 1080 * 4 / 8 = 1012.5kb。
由于 bitmap 簡單順序存儲圖片的像素信息竖螃,它可以不經(jīng)過解碼就直接被渲染到 UI 上淑廊。實際上,其它格式的圖片都需要先被首先解碼為 bitmap特咆,然后才能渲染到界面上季惩。
圖片解壓縮的過程其實就是將圖片的二進(jìn)制數(shù)據(jù)轉(zhuǎn)換成像素數(shù)據(jù)的過程
如何做到性能優(yōu)化:
未解壓縮的圖片將要渲染到屏幕時,cpu會在在主線程解壓縮,而解壓縮過的圖片画拾,就不會再去解壓縮了啥繁。
常見的解決方案是在子線程提前對圖片進(jìn)行解壓縮。強制解壓縮的原理就是對圖片進(jìn)行重新繪制青抛,得到一張新的解壓縮后的位圖旗闽。
YYImage\SDWebImage主流框架的也大致是這么實現(xiàn)的。
// 1. 從 UIImage 對象中獲取 CGImageRef 的引用蜜另。這兩個結(jié)構(gòu)是蘋果在不同層級上對圖片的表示方式适室,UIImage 屬于 UIKit,是 UI 層級圖片的抽象举瑰,用于圖片的展示亭病;CGImageRef 是 QuartzCore 中的一個結(jié)構(gòu)體指針,用C語言編寫嘶居,用來創(chuàng)建像素位圖罪帖,可以通過操作存儲的像素位來編輯圖片。這兩種結(jié)構(gòu)可以方便的互轉(zhuǎn):
CGImageRef imageRef = image.CGImage;
// 2. 調(diào)用 UIImage 的 +colorSpaceForImageRef: 方法來獲取原始圖片的顏色空間參數(shù)邮屁。
CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef];
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
// 3. 計算圖片解碼后每行需要的比特數(shù)整袁,由兩個參數(shù)相乘得到:每行的像素數(shù) width,和存儲一個像素需要的比特數(shù)4(這里的4佑吝,其實是由每張圖片的像素格式和像素組合來決定的)
size_t bytesPerRow = 4 * width;
// 4. 最關(guān)鍵的函數(shù):調(diào)用 CGBitmapContextCreate() 方法坐昙,生成一個空白的圖片繪制上下文,我們傳入了上述的一些參數(shù)芋忿,指定了圖片的大小炸客、顏色空間、像素排列等等屬性戈钢。
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
kBitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
if (context == NULL) {
return image;
}
// 5. 調(diào)用 CGContextDrawImage() 方法痹仙,將未解碼的 imageRef 指針內(nèi)容,寫入到我們創(chuàng)建的上下文中殉了,這個步驟开仰,完成了隱式的解碼工作。
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
// 6. 從 context 上下文中創(chuàng)建一個新的 imageRef薪铜,這是解碼后的圖片了众弓。
CGImageRef newImageRef = CGBitmapContextCreateImage(context);
// 7. 從 imageRef 生成供UI層使用的 UIImage 對象,同時指定圖片的 scale 和 orientation 兩個參數(shù)隔箍。
UIImage *newImage = [UIImage imageWithCGImage:newImageRef
scale:image.scale
orientation:image.imageOrientation];
CGContextRelease(context);
CGImageRelease(newImageRef);
return newImage;
YYImage
中對圖片的解壓縮過程與上述完全一致谓娃,只是傳遞給 CGBitmapContextCreate
函數(shù)的部分參數(shù)存在細(xì)微的差別
性能對比:
- 在解壓PNG圖片,SDWebImage>YYImage
- 在解壓JPEG圖片,SDWebImage<YYImage
這樣就在子線程中對圖片進(jìn)行了強制解碼,回調(diào)給主線程使用蜒滩,從而大大提高了圖片的渲染效率滨达。這也是現(xiàn)在主流三方庫圖片解碼的最佳實踐奶稠。