圖片在計算機中如何存儲和表示?
常見的圖片格式
JPEG 是目前最常見的圖片格式捐晶,它誕生于1992年菲语,是一個很古老的格式。它只支持有損壓縮
惑灵,其壓縮算法可以精確控制壓縮比山上,以圖像質(zhì)量換得存儲空間。由于它太過常見英支,以至于許多移動設(shè)備的 CPU 都支持針對它的硬編碼與硬解碼佩憾。
PNG 誕生在 1995 年,比 JPEG 晚幾年干花。它本身的設(shè)計目的是替代 GIF 格式妄帘,所以它與 GIF 有更多相似的地方。PNG 只支持無損壓縮
池凄,所以它的壓縮比是有上限的抡驼。相對于 JPEG 和 GIF 來說,它最大的優(yōu)勢在于支持完整的透明通道
肿仑。
GIF 誕生于 1987 年致盟,隨著初代互聯(lián)網(wǎng)流行開來碎税。它有很多缺點,比如通常情況下只支持 256 種顏色馏锡、透明通道只有 1 bit雷蹂、文件壓縮比不高。它唯一的優(yōu)勢就是支持多幀動畫
眷篇,憑借這個特性,它得以從 Windows 1.0 時代流行至今荔泳,而且仍然大受歡迎蕉饼。
格式 | 優(yōu)點 | 缺點 | 用途 |
---|---|---|---|
jpg | 色彩豐富,文件小 | 有損壓縮 | 顏色豐富的圖 |
png | 透明玛歌、無損壓縮昧港、簡單圖文件小 | 若顏色較多復(fù)雜,則圖片生成后的文件很大 | 小圖標支子、透明背景 |
gif | 動態(tài)创肥、透明、文件小 | 色域不廣值朋、只有256種顏色 | 動態(tài)圖片 |
除了以上面常見的格式叹侄,也有一些新型的格式:
APNG 是 Mozilla 在 2008 年發(fā)布的一種圖片格式,旨在替換掉畫質(zhì)低劣的 GIF 動畫
昨登。它實際上只是相當于 PNG 格式的一個擴展趾代,所以 Mozilla 一直想把它合并到 PNG 標準里面去。然而 PNG 開發(fā)組并沒有接受 APNG 這個擴展
丰辣,而是一直在推進它自己的 MNG 動圖格式撒强。MNG 格式過于復(fù)雜以至于并沒有什么系統(tǒng)或瀏覽器支持,而 APNG 格式由于簡單容易實現(xiàn)笙什,目前已經(jīng)漸漸流行開來飘哨。Mozilla 自己的 Firefox 首先支持了 APNG,隨后蘋果的 Safari 也開始有了支持琐凭, Chrome 目前也已經(jīng)嘗試開始支持 芽隆,可以說未來前景很好
。
WebP 是 Google 在 2010 年發(fā)布的圖片格式统屈,希望以更高的壓縮比
替代 JPEG摆马。它用 VP8 視頻幀內(nèi)編碼作為其算法基礎(chǔ),取得了不錯的壓縮效果鸿吆。它支持有損和無損壓縮
囤采、支持完整的透明通道
、也支持多幀動畫
惩淳,并且沒有版權(quán)問題蕉毯,是一種非常理想的圖片格式(美中不足的是乓搬,WebP格式圖像的編碼時間“比JPEG格式圖像長8倍)
。借由 Google 在網(wǎng)絡(luò)世界的影響力代虾,WebP 在幾年的時間內(nèi)已經(jīng)得到了廣泛的應(yīng)用进肯。看看你手機里的 App:微博棉磨、微信江掩、QQ、淘寶乘瓤、網(wǎng)易新聞等等环形,每個 App 里都有 WebP 的身影。Facebook 則更進一步衙傀,用 WebP 來顯示聊天界面的貼紙動畫抬吟。
關(guān)于以上幾種圖片格式在移動端的解碼和性能對比參見:移動端圖片格式調(diào)研
iOS中圖片加載過程和性能瓶頸
如上文所說,大部分格式的圖片都是被壓縮的统抬,都需要被首先解碼為bitmap(未壓縮的位圖)火本,然后才能渲染到UI上。
UIImageView
顯示圖片聪建,也有類似的過程钙畔。實際上,一張圖片從在文件系統(tǒng)中金麸,到被顯示到 UIImageView
刃鳄,會經(jīng)歷以下幾個步驟:
- 假設(shè)我們使用
+imageWithContentsOfFile:
方法從磁盤中加載一張圖片,這個時候的圖片并沒有解壓縮钱骂; - 然后將生成的
UIImage
賦值給UIImageView
叔锐; - 接著一個隱式的
CATransaction
捕獲到了UIImageView
圖層樹的變化; - 在主線程的下一個
run loop
到來時见秽,Core Animation
提交了這個隱式的transaction
愉烙,這個過程可能會對圖片進行copy
操作,而受圖片是否字節(jié)對齊等因素的影響解取,這個 copy 操作可能會涉及以下部分或全部步驟:- 分配內(nèi)存緩沖區(qū)用于管理文件 IO 和解壓縮操作敞葛;
- 將文件數(shù)據(jù)從磁盤讀到內(nèi)存中袜瞬;
- 將壓縮的圖片數(shù)據(jù)解碼成未壓縮的位圖形式,這是一個非常耗時的 CPU 操作;
- 最后
Core Animation
使用未壓縮的位圖數(shù)據(jù)渲染UIImageView
的圖層吱殉。
在上面的步驟中肝集,我們提到了圖片的解壓縮是一個非常耗時的 CPU 操作恰聘,并且它默認是在主線程中執(zhí)行的魂挂。那么當需要加載的圖片比較多時,就會對我們應(yīng)用的響應(yīng)性造成嚴重的影響慧邮,尤其是在快速滑動的列表上调限,這個問題會表現(xiàn)得更加突出舟陆。這就是 UIImageView 的一個性能瓶頸。
實際上耻矮,當我們調(diào)用
[UIImage imageNamed:@"xxx"]
后秦躯,UIImage 中存儲的是未解碼的圖片,而調(diào)用[UIImageView setImage:image]
后裆装,會在主線程進行圖片的解碼工作并且將圖片顯示到 UI 上踱承,這時候,UIImage 中存儲的是解碼后的 bitmap 數(shù)據(jù)哨免。
為什么需要解壓縮
既然圖片的解壓縮需要消耗大量的 CPU 時間茎活,那么我們?yōu)槭裁催€要對圖片進行解壓縮呢?是否可以不經(jīng)過解壓縮铁瞒,而直接將圖片顯示到屏幕上呢妙色?答案是否定的桅滋。要想弄明白這個問題慧耍,我們首先需要知道什么是位圖
bitmap:bitmap 又叫位圖文件,它是一種非壓縮的圖片格式丐谋,所以體積非常大芍碧。所謂的非壓縮,就是圖片每個像素的原始信息在存儲器中依次排列号俐,一張典型的1920*1080像素的 bitmap 圖片泌豆,每個像素由 RGBA 四個字節(jié)表示顏色,那么它的體積就是 1920 * 1080 * 4 = 1012.5kb吏饿。
由于 bitmap 簡單順序存儲圖片的像素信息踪危,它可以不經(jīng)過解碼就直接被渲染到 UI 上。實際上猪落,其它格式的圖片都需要先被首先解碼為 bitmap贞远,然后才能渲染到界面上。
不管是 JPEG 還是 PNG 圖片笨忌,都是一種壓縮的位圖圖形格式蓝仲。只不過 PNG 圖片是無損壓縮,并且支持 alpha 通道官疲,而 JPEG 圖片則是有損壓縮袱结,可以指定 0-100% 的壓縮比。值得一提的是途凫,在蘋果的 SDK 中專門提供了兩個函數(shù)用來生成 PNG 和 JPEG 圖片:
// return image as PNG. May return nil if image has no CGImageRef or invalid bitmap format
UIKIT_EXTERN NSData * __nullable UIImagePNGRepresentation(UIImage * __nonnull image);
// return image as JPEG. May return nil if image has no CGImageRef or invalid bitmap format. compression is 0(most)..1(least)
UIKIT_EXTERN NSData * __nullable UIImageJPEGRepresentation(UIImage * __nonnull image, CGFloat compressionQuality);
因此垢夹,在將磁盤中的圖片渲染到屏幕之前,必須先要得到圖片的原始像素數(shù)據(jù)维费,才能執(zhí)行后續(xù)的繪制操作棚饵,這就是為什么需要對圖片解壓縮的原因煤裙。
圖片解壓縮的過程其實就是將圖片的二進制數(shù)據(jù)轉(zhuǎn)換成像素數(shù)據(jù)的過程
圖片的編碼和解碼
iOS 底層是用 ImageIO.framework 實現(xiàn)的圖片編解碼。目前 iOS 原生支持的格式有:JPEG噪漾、JPEG2000硼砰、PNG、GIF欣硼、BMP题翰、ICO、TIFF诈胜、PICT豹障,自 iOS 8.0 起,ImageIO 又加入了 APNG焦匈、SVG血公、RAW 格式的支持。在上層缓熟,開發(fā)者可以直接調(diào)用 ImageIO 對上面這些圖片格式進行編碼和解碼累魔。對于動圖來說,開發(fā)者可以解碼動畫 GIF 和 APNG够滑、可以編碼動畫 GIF垦写。
注意:圖片所占內(nèi)存的大小與圖片的尺寸有關(guān),而不是圖片的文件大小
色彩空間和像素格式
計算圖片解碼后每行需要的比特數(shù)彰触,由兩個參數(shù)相乘得到:每行的像素數(shù) width
梯投,和存儲一個像素需要的比特數(shù)4
這里的4,其實是由每張圖片的像素格式和像素組合來決定的况毅,下表是蘋果平臺支持的像素組合方式
)
表中的bpp分蓖,表示每個像素需要多少位;bpc表示顏色的每個分量尔许,需要多少位么鹤。具體的解釋方式,可以看下面這張圖:
我們解碼后的圖片母债,默認采用 kCGImageAlphaNoneSkipLast RGB 的像素組合午磁,沒有 alpha 通道,每個像素32位4個字節(jié)毡们,前三個字節(jié)代表紅綠藍三個通道, 但是有時候迅皇,如果我們只是繪制一個蒙版,是不需要這么字節(jié)表示的比如 Alpha 8 format衙熔,每個像素只需要占用 1 個字節(jié)登颓,這之間的差距就造成了內(nèi)存浪費。
UIGraphicsImageRenderer 和 UIGraphicsBeginImageContextWithOptions
當我們?yōu)榱穗x屏渲染红氯,要創(chuàng)建 image buffer 時框咙,我們通常會使用 UIGraphicsBeginImageContext咕痛,但是最好還是用 UIGraphicsImageRenderer,它的性能更好喇嘱、更高效茉贡,并且支持廣色域。這里有一個中間地帶者铜,如果你主要將圖像渲染到圖形圖像渲染器(graphic image render)中腔丧,該圖像可能使用超出 SRGB 色域的色彩空間值,但實際上并不需要更大的元素來存儲這些信息作烟。所以 UIImage 有一個可以用來獲取預(yù)構(gòu)建的 UIGraphicsImageRendererFormat 對象的 image renderer format 屬性愉粤,該對象用于重新渲染圖像時進行最優(yōu)化存儲。
所以蘋果官方建議使用 UIGraphicsImageRenderer拿撩,這個方法是從 iOS 10 引入衣厘,在 iOS 12 上會自動選擇最佳的圖像格式,可以減少很多內(nèi)存压恒。系統(tǒng)可以根據(jù)圖片分辨率選擇創(chuàng)建解碼圖片的格式影暴,如選用SRGB format 格式,每個像素占用 4 字節(jié)涎显,而Alpha 8 format坤检,每像素只占用 1 字節(jié)兴猩,可以減少大量的解碼內(nèi)存占用期吓。
擴展閱讀色彩空間與像素格式
imageWithContentsOfFile 和 imageNamed 對比
imgeNamed
用這個方法加載圖片分為兩種情況:
- 系統(tǒng)緩存有這個圖片,直接從緩存中取得
- 系統(tǒng)緩存沒有這個圖片
通過傳入的文件名對整個工程進行遍歷 (在application bundle
的頂層文件夾尋找名字的圖象 ), 如果如果找到對應(yīng)的圖片,iOS 系統(tǒng)首先要做的是將這個圖片放到系統(tǒng)緩存中去,以備下次使用的時候直接從系統(tǒng)緩存中取, 接下來重復(fù)第一步,即直接從緩存中取
由于系統(tǒng)會緩存圖片倾芝,所以如果要加載的這個圖片的文件量很多,文件大小很大,內(nèi)存不足,內(nèi)存泄露,甚至是程序的崩潰都是很容易發(fā)生的事.
imageWithContentsOfFile
用這個方法只有一種情況,那就是僅僅加載圖片, 圖像數(shù)據(jù)不會被緩存. 因此在加載較大圖片的時候, 以及圖片使用情況很少的時候可以使用這兩個方法 , 降低內(nèi)存消耗.
加載本地圖片讨勤,要比從Assets Catalogs耗時要長,具體見Assets Catalogs 與 I/O 優(yōu)化
圖片內(nèi)存優(yōu)化
到此我們可知晨另,圖片經(jīng)過解壓之后潭千,在內(nèi)存中實際是根據(jù)圖片的分辨率和圖片渲染所用的像素格式而定的
一、對不常用的大圖片借尿,使用 imageWithContentsOfFile
代替 imageNamed
方法刨晴,避免內(nèi)存緩存(相應(yīng)的使用imageNamed
要避免載入大量的圖片造成內(nèi)存暴增)
二、使用 ImageIO 方法路翻,對大圖片進行縮放狈癞,減少圖片解碼占用內(nèi)存大小。
UIImage 在設(shè)置和調(diào)整大小的時候茂契,需要將原始圖像加壓到內(nèi)存中蝶桶,然后對內(nèi)部坐標空間做一系列轉(zhuǎn)換,整個過程會消耗很多資源掉冶。我們可以使用 ImageIO真竖,它可以直接讀取圖像大小和元數(shù)據(jù)信息脐雪,不會帶來額外的內(nèi)存開銷。
三恢共、繪制圖片战秋,用 UIGraphicsImageRenderer 代替 UIGraphicsBeginImageContextWithOptions,自動管理顏色格式
四讨韭、超大圖片處理
-
加載使用蘋果推薦的DownSampling方案(縮略圖方式)
// DownSampling(降低采樣) // 在視圖比較小获询,圖片比較大的場景下,直接展示原圖片會造成不必要的內(nèi)存和CPU消耗拐袜,這里就可以使用ImageIO的接口吉嚣,DownSampling,也就是生成縮略圖 func downsample(imageAt imageURL: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage { let sourceOpt = [kCGImageSourceShouldCache : false] as CFDictionary /**< 這里有兩個注意事項 設(shè)置kCGImageSourceShouldCache為false蹬铺,避免緩存解碼后的數(shù)據(jù)尝哆,64位設(shè)置上默認是開啟緩存的,(很好理解甜攀,因為下次使用該圖片的時候秋泄,可能場景不同,需要生成的縮略圖大小是不同的规阀,顯然不能做緩存處理) 設(shè)置kCGImageSourceShouldCacheImmediately為true恒序,避免在需要渲染的時候才做解碼,默認選項是false */ // 其他場景可以用createwithdata (data并未decode,所占內(nèi)存沒那么大), let source = CGImageSourceCreateWithURL(imageURL as CFURL, sourceOpt)! let maxDimension = max(pointSize.width, pointSize.height) * scale let downsampleOpt = [kCGImageSourceCreateThumbnailFromImageAlways : true, kCGImageSourceShouldCacheImmediately : true , kCGImageSourceCreateThumbnailWithTransform : true, kCGImageSourceThumbnailMaxPixelSize : maxDimension] as CFDictionary let downsampleImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOpt)! return UIImage(cgImage: downsampleImage) }
使用蘋果的CATiledLayer去加載谁撼。原理是分片渲染歧胁,滑動時通過指定目標位置,通過映射原圖指定位置的部分圖片數(shù)據(jù)解碼渲染厉碟。這里不再累述喊巍,有興趣的小伙伴可以自行了解下官方API。
五箍鼓、網(wǎng)絡(luò)圖片加載方式:使用SDwebImage等三方庫
解決UIImageView的性能瓶頸
我們在討論UIImageView的性能瓶頸中發(fā)現(xiàn)崭参,問題在于主線程進行圖片解壓縮占用了大量的CPU,解決問題的辦法就是:在子線程提前對圖片進行強制解壓縮
而強制解壓縮的原理就是對圖片進行重新繪制款咖,得到一張新的解壓縮后的位圖何暮。其中,用到的最核心的函數(shù)是 CGBitmapContextCreate :
/* Create a bitmap context. The context draws into a bitmap which is `width'
pixels wide and `height' pixels high. The number of components for each
pixel is specified by `space', which may also specify a destination color
profile. The number of bits for each component of a pixel is specified by
`bitsPerComponent'. The number of bytes per pixel is equal to
`(bitsPerComponent * number of components + 7)/8'. Each row of the bitmap
consists of `bytesPerRow' bytes, which must be at least `width * bytes
per pixel' bytes; in addition, `bytesPerRow' must be an integer multiple
of the number of bytes per pixel. `data', if non-NULL, points to a block
of memory at least `bytesPerRow * height' bytes. If `data' is NULL, the
data for context is allocated automatically and freed when the context is
deallocated. `bitmapInfo' specifies whether the bitmap should contain an
alpha channel and how it's to be generated, along with whether the
components are floating-point or integer. */
CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,
size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)
CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
如果 UIImage
中存儲的是已經(jīng)解碼后的數(shù)據(jù)铐殃,速度就會快很多海洼,所以優(yōu)化的思路就是:在子線程中對圖片原始數(shù)據(jù)進行強制解碼,再將解碼后的圖片拋回主線程繼續(xù)使用背稼,從而提高主線程的響應(yīng)速度贰军。
我們需要使用的工具是 Core Graphics
框架的 CGBitmapContextCreate
方法和相關(guān)的繪制函數(shù)。總體的步驟是:
- 創(chuàng)建一個指定大小和格式的
bitmap context
词疼。 - 將未解碼圖片寫入到這個
context
中俯树,這個過程包含了強制解碼。 - 從這個
context
中創(chuàng)建新的UIImage
對象贰盗,返回许饿。
SDWebImage 實現(xiàn)
下面是SDWebImage的核心代碼:
// 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;
通過以上的步驟搅幅,我們成功在子線程中對圖片進行了強制轉(zhuǎn)碼,回調(diào)給主線程使用呼胚,從而大大提高了圖片的渲染效率茄唐。這也是現(xiàn)在主流 App 和大量三方庫的最佳實踐。
SDWebImage配置優(yōu)化,減小CG-raster-data內(nèi)存占用
在使用SDWebImage的時候沪编,會默認保存圖片解碼后的內(nèi)存呼盆,以便提高頁面的渲染速度,但是這會導(dǎo)致內(nèi)存的急速增加蚁廓,所以可以在不影響體驗的情況下访圃,選擇機型和系統(tǒng),進行優(yōu)化相嵌,避免大量的內(nèi)存占用腿时,引起OOM問題。關(guān)閉解碼內(nèi)存緩存的方法如下:
[[SDImageCache sharedImageCache] setShouldDecompressImages:NO];
[[SDWebImageDownloader sharedDownloader] setShouldDecompressImages:NO];
附錄
WWDC2018 圖像和圖形的最佳實踐
WWDC2018 深入iOS內(nèi)存
移動端圖片格式調(diào)研
談?wù)?iOS 中圖片的解壓縮
iOS開發(fā):圖片格式與性能優(yōu)化