本文已授權陌陌公眾號(紅陽)獨家發布
最近封裝了一個高斯模糊組件,正好整理了與圖片相關的理論基礎。 那么,這次我就來說說如何估算一張圖片在顯存中的大小。 如果你想優化的話,可以從什么方向入手。
問問題
在閱讀本文之前,我們先思考幾個問題:
Q1:一張png格式的圖片,圖片文件大小為55.8KB,那么加載到顯存中時占用的大小是多少?
Q2:為什么有時同一個應用程序,應用程序中相同的界面,界面上相同的圖片,但在不同設備上的內存消耗不同?
Q3:圖片占用顯存大小的公式:圖片幀率*每像素像素大小,這些說法是否正確或嚴謹?
Q4:優化圖片顯存大小可以從哪些方向入手?
文本
在開發中,經常需要對圖片進行優化,因為圖片很容易耗盡顯存。 這樣,你就需要知道一張圖片的大小是如何估算的,以及加載到顯存中時占用了多少空間?
先看一張圖:
這是一張普通的png圖片,我們來看看它的具體信息:
圖片的幀率為1080*452,而我們在筆記本上聽到的png圖片大小只有55.8KB,那么問題來了:
我們看到一張大小為55.8KB的png圖片,它在顯存中是否也占用了55.8KB的大小呢?
澄清這一點非常重要,因為我遇到過有人說我的一張圖片只有幾KB,雖然界面上顯示了幾百張圖片,但為什么顯存占用這么高?
因此,我們需要明確一個概念:我們在筆記本上看到的png格式或者jpg格式的圖片,png(jpg)只是這張圖片的容器,它們通過相應的壓縮算法對原圖的每張圖片進行壓縮。 將像素信息轉換為另一種數據格式表示,從而達到壓縮的目的,減小圖像文件的大小。
而當我們通過代碼將這張圖片加載到顯存中時,會先解析圖片文件本身的數據格式,然后還原為位圖,即對象,其大小取決于圖片文件的數據格式像素和幀速率提高。
因此,png或jpg格式的圖片大小與加載到顯存中的這張圖片所占用的大小完全不同。 你不能說我的jpg圖片只有10KB,所以它只占用10KB的顯存空間,這是錯誤的。
那么,如何估算一張圖片占用的顯存空間呢?
最后附上一篇大師文章很詳細,有興趣可以看一下。 我在這里不會說得那么專業,但我會根據我的粗略了解告訴你。
圖像內存大小
網上很多文章都會介紹估算一張圖片占用顯存大小的公式:碼率*每個像素的大小。
這句話是對是錯,我只是覺得不結合場景直接表達是不嚴謹的。
在原生操作中,有些場景下,圖片加載到顯存時的幀率會經過一層換算,所以雖然最終的圖片大小估算公式仍然是幀率*像素大小,但此時的幀率它不再是圖片本身的幀率。
我們來做一個實驗,從以下幾個考慮因素相互結合的場景中加載同一張圖片,看看它占用了多少顯存空間:
測試代碼模板如下:
private void loadResImage(ImageView imageView) {
BitmapFactory.Options options = new BitmapFactory.Options();
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.weixin, options);
//Bitmap bitmap = BitmapFactory.decodeFile("mnt/sdcard/weixin.png", options);
imageView.setImageBitmap(bitmap);
Log.i("!!!!!!", "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
Log.i("!!!!!!", "width:" + bitmap.getWidth() + ":::height:" + bitmap.getHeight());
Log.i("!!!!!!", "inDensity:" + options.inDensity + ":::inTargetDensity:" + options.inTargetDensity);
Log.i("!!!!!!", "imageview.width:" + imageView.getWidth() + ":::imageview.height:" + imageView.getHeight());
}
ps:這里提一下,可以使用()方法獲取當前圖像占用的顯存大小。 其實api19之后還有另外一種方法,只不過復用時獲取的大小含義也發生了變化。 這個特殊場景就不詳細說了。 說,有興趣的話,自己去看看吧。 總之,我們知道,在大多數場景下,我們可以通過()復制圖片所占用的顯存大小來驗證我們的實驗。
圖片為上圖:一張png格式的圖片,幀率為1080*452,圖片文件本身大小為56KB
序列號前提內存大小
圖片位于res/,設備dpi=240,設備1dp=1.5px,控制寬高=50dp
(4.19MB)
圖片位于res/,設備dpi=240,設備1dp=1.5px,控制寬高=500dp
(4.19MB)
圖片位于res/-hdpi,設備dpi=240,設備1dp=1.5px
(1.86MB)
圖片位于res/-xhdpi,設備dpi=240,設備1dp=1.5px
(1.05MB)
圖片位于res/-xhdpi,設備dpi=160,設備1dp=1px
(476.7KB)
圖片位于res/-hdpi,設備dpi=160,設備1dp=1px
(846.5KB)
圖片位于res/,設備dpi=160,設備1dp=1px
(1.86MB)
圖片位于c盤,設備dpi=160,設備1dp=1px
(1.86MB)
圖片位于c盤,設備dpi=240,設備1dp=1.5px
(1.86MB)
看看是否是同一張圖片,但是在不同的場景下,占用的顯存大小可能會不一樣,這個后面會分析。 上述場景中,列出了不同的圖片來源、不同的設備、不同的顯示控件大小。 我們繼續看一個場景:同一張圖片,保存為不同格式的文件(不是重命名,可以用ps);
圖片:jpg格式的圖片,碼率為1080*452,圖片文件本身大小為85.2KB
ps:還是和上一張圖一樣,只是保存為jpg格式
序號前提顯存大小比較對象
10
圖片位于res/,設備dpi=240,設備1dp=1.5px
(4.19MB)
序列號1
11
圖片位于res/-hdpi,設備dpi=240,設備1dp=1.5px
(1.86MB)
序列號3
12
圖片位于res/-xhdpi,設備dpi=240,設備1dp=1.5px
(1.05MB)
序列號4
13
圖片位于c盤,設備dpi=240,設備1dp=1.5px
(1.86MB)
9號
對于這里列出的幾個場景,每行的末尾還寫有每個場景比較的實驗對象的序號。 你可以對比確認一下,發現數據是否一樣,所以這里可以得到一點推論:
圖片的不同格式:png或jpg似乎對圖片占用顯存的大小沒有影響
好,我們開始分析這類實驗數據:
首先如果根據圖片大小的估算公式為:幀率*像素大小
所以,這張圖片的大小應該按照這個公式:1080*452*4B=≈1.86MB
ps:這里像素大小是按4B估算的,因為不指定時系統默認為像素數據格式,其他格式如下:
上面的實驗中應該是這個大小,那為什么會出現一些其他大小的數據呢? 那么,讓我們一一分解:
分析點1
我們先看一下數字1和2的實驗。 三者之間的區別僅在于圖片中顯示的空間大小。 之所以做這個測試,是因為有人認為圖片占用顯存的大小與界面上顯示的圖片大小有關。 顯示控件越大,占用的視頻內存就越多。 事實上,這些認識都是錯誤的。
想想看,圖片在繪制到控件上之前必須先加載到顯存中,所以當圖片需要申請顯存空間時,它不知道此時要顯示的控件的大小,怎么可能控件大小影響圖片占用 至于顯存空間,除非提前通知,否則會自動參與圖片加載過程。
分析點2
我們看一下序號2、3、4的實驗nc10 內存,這三個的區別在于圖片在res中的資源目錄不同。 當圖片放在res中不同的目錄下時,為什么最終圖片加載到顯存中所占用的大小不同呢?
如果你看一下.()的源碼,你會發現,系統在加載res目錄下的資源圖片時,會根據圖片存放的目錄不同,進行碼率轉換,轉換后的碼率會根據圖片的大小進行轉換。規則是:
新圖片的高度=原圖片的高度*(設備的dpi/目錄對應的dpi)
新圖片的長度=原圖片的長度*(設備的dpi/目錄對應的dpi)
目錄名與dpi的對應關系如下,無后綴對應:
那么,我們來看看實驗號2,根據上面的理論,我們來估算一下這張圖片的顯存大?。?/p>
轉換后幀率:1080*(240/160)*452*(240/160)=1620*678
事實上,此時的幀率并不是原始圖像的幀率。 經過一層轉換,最終估算出圖像大?。?/p>
1620*678*4B=≈4.19MB
現在你知道序列號2的實驗結果是怎么來的了吧。 同理,序號3資源的目標是240對應的hdpi,而設備的dpi也是240,所以轉換后的幀率依然是原圖本身,結果也是1.86MB。
總結:
位于res中不同資源目錄的圖像,在加載到顯存時,會先進行碼率轉換,然后估算大小。 轉換的影響是由設備的dpi和不同的資源目錄引起的。
分析點3
根據分析點2的理論,看序號5、6、7的實驗,這三個實驗似乎是用來和序號2、3、4的實驗進行比較的,這是我們可以從這6個實驗中得到。 推論是:
因此,可能會出現這樣的情況,同一個app,但是運行在不同dpi的設備上,同樣的界面,但是內存消耗可能會不同。
為什么我們仍然說可能會有所不同? 根據前面的理論,如果圖像相同,目錄相同,但dpi設備不同,那么幀率轉換可能不同,顯存消耗也一定不同。 為什么要用“可能性”這些詞呢?
emmm,繼續看下面的分析點。
分析點4
序號8和9的實驗,雖然我想驗證只有圖片來源為res時是否會有碼率轉換,但結果也證明了當圖片在C盤、SD卡或者目錄下時、網上還是網上(雖然網上的圖片最終都是下載到c盤的),只要不在res目錄下,此類圖片占用顯存大小的估算公式是原圖碼率*像素大小。
當然,如果有時間看一下源碼,確實只有()方法會根據里面的dpi來轉換碼率,其他()則不會。
那么,為什么在上一節中,非常重要的是要解釋一下,雖然同一個應用程序運行在不同 dpi 設備上,但相同的界面可能會消耗不同的顯存。 為什么這里用了這么多“可能”這個詞?
對啊,大家想想吧。 顯然,根據我們整理的理論,估算圖像內存大小的公式為:碼率*像素大小。 之后,如果圖片的來源在res中,則需要注意圖片放置的資源目錄,以及設備本身的dpi值,因為系統在res中取資源圖片,所以會根據這兩點進行幀率轉換。 這樣的話,圖片的顯存大小一定不一樣吧?
emmm,這個就看你自己的動機了。 如果你開發一個app,圖片的相關操作都是經過操作的,所以上面的問題可以用肯定的說法來代替。 但現在還是有那么多自己寫的強大的圖片開源庫,而且不同的圖片開源庫內部的圖片加載處理、緩存策略、復用策略也不同。
因此,如果使用某個圖像開源庫,加載一張圖片到顯存會占用多少空間,就需要深入該圖像開源庫分析其處理。
由于基本上所有的圖像開源庫都會對圖像操作進行優化,所以下面我們繼續講圖像優化。
圖像優化
有了上面的理論基礎,現在我們來思考一下,如果圖片占用顯存空間太大,需要優化,可以入手一些方向,就更有趣了。
圖像搶占顯存大小的公式為:幀率*像素大小,但在某些場景下,例如圖像來源為res,最終圖像的碼率可能不是原始圖像的幀率,但歸根結底,對于計算機而言,確實是按照這個公式來估算的。
因此,如果只從圖像本身考慮優化,只有兩個方向:
不僅僅考慮圖片本身,其他方面可以是顯存警告時自動清除、圖片弱引用等操作。
減小像素大小
第二個方向非常容易操作。 雖然系統默認采用該格式進行處理,但每個像素都會占用4B大小。 改變這種格式自然會增加圖像占用的顯存大小。
一般都會換成某種格式,但是前者不支持透明度,所以這個方案不通用,要看你app中圖片的透明度要求,其實也可以緩存,但是會增加質量。
因為基本都是使用圖像開源庫,所以這里介紹一下圖像開源庫的一些處理方法:
//fresco,默認使用ARGB_8888
Fresco.initialize(context, ImagePipelineConfig.newBuilder(context).setBitmapsConfig(Bitmap.Config.RGB_565).build());
//Glide,不同版本nc10 內存,不同像素格式
班級 {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888);
}
@Override
public void registerComponents(Context context, Glide glide) {
}
//定義為.xml上的元數據
"com..lab..":value=""/>
//,默認
.with(.()).load(url).(..).into();
上面的代碼摘自網上,其正確性應該是可信的。 尚未得到證實。 如果有興趣可以去相關源碼確認一下。
增加比特率
如果能夠讓系統在加載圖片時不以原始碼率為標準,而是增加一定的比例,那么自然就可以達到減少圖片顯存的效果了。
同樣,系統提供了相關的API:
BitmapFactory.Options.inSampleSize
設置后,寬度和高度會減少兩倍。 例如:一張寬高為 的圖片,設置為 4 后,實際加載到顯存的圖片寬高為 。占用顯存為 0.75M,而不是 12M,節省 15 倍
其中的段落摘自最后鏈接的文章。 網上也有很多關于如何操作的講解文章,這里不再贅述。 我沒有看過這些開源圖像庫的內部處理,但我推測它們對圖像的優化處理應該也是通過這個API來操作的。
雖然,無論哪種圖像開源庫,在加載圖像時,內部都必須對圖像進行優化,盡管我們并沒有自動表明需要圖像壓縮過程。 這就是我在里面說的,為什么當你使用開源圖像庫時,你不能再根據圖像內存大小部分提到的理論來估計圖像內存的大小了。
我們可以做一個實驗,先看下面的實驗:
開源庫必備顯存大小
圖片位于res/,設備dpi=240,設備1dp=1.5px
(1.86MB)
圖片位于res/-hdpi,設備dpi=240,設備1dp=1.5px
(1.86MB)
圖片位于res/-xhdpi,設備dpi=240,設備1dp=1.5px
(1.86MB)
圖片位于c盤,設備dpi=240,設備1dp=1.5px
(1.86MB)
如果使用的話,無論圖片來源在哪里,都會根據原始圖片的比特率來估計幀率,從中得到的數據也可以得到確認。 像素大小按默認格式處理。
我推測,內部加載res圖像時,應該先通過自己的方法獲取圖像文件對象,最后通過()或()等形式加載圖像,總之不是通過()加載圖片,這就可以解釋為什么無論放在哪個res目錄下,圖片的大小都是根據原圖的幀率來估計的。有時間的話可以去看看源碼代碼來驗證它。
我們來看看Glide的實驗:
開源庫必備顯存大小
滑行
圖片位于res/,設備dpi=240,設備1dp=1.5px,顯示到寬高為500dp的控件
(91.99KB)
滑行
圖片位于res/-hdpi,設備dpi=240,設備1dp=1.5px,顯示到寬高為500dp的控件
(91.99KB)
滑行
圖片位于res/-hdpi,設備dpi=240,設備1dp=1.5px,不顯示到控件,只獲取對象
(1.86MB)
滑行
圖片位于c盤,設備dpi=240,設備1dp=1.5px,不顯示到控件,只獲取對象
(1.86MB)
滑行
圖片位于c盤,設備dpi=240,設備1dp=1.5px,全屏控制顯示(1920*984)
(7.21MB)
可以看到,Glide的處理方式與以下有很大不同:
如果只獲取物體,則根據原始圖片的碼率估算圖片占用的顯存大小。 但如果通過into()將圖像加載到控件中,則幀速率將根據控件的大小進行壓縮。
例如第一個,顯示控件的寬度和高度都是500dp=750px,原始圖像碼率是1080*452,最終轉換后的碼率是:750*314,所以圖像內存大?。?50*314*4B=;
比如最后一張,顯示的控件的寬高為1920*984,原圖的幀率換算為:1920*984,所以圖像內存大小:1920*984*4B=;
至于這個轉換的規則,我不知道。 有時間的話可以去源碼看看,不過也就是說Glide會根據顯示控件的大小手動轉換碼率,然后加載到顯存中。
但無論是Glide,無論圖片來源是否在res中,也無論設備的dpi是多少,是否需要與源res目錄進行幀率轉換。
因此,在關于圖像內存大小的章節中,我會說,如果使用開源庫圖像,那么,這個理論就不適用,因為系統已經開放了設置,允許我們根據需要加載中的圖片首先將顯存按一定比例進行壓縮,以減少顯存占用。
而這種圖像開源庫自然會利用系統的支持,內部做一些顯存優化,還可能涉及到圖像裁剪等其他優化,但不管怎樣,這個時候系統原生的估計圖像內存大小的理論基礎自然不適用。
為了提高碼率,不僅僅是圖像開源庫內部默認的優化處理,他們自然會提供相關的供我們使用,例如:
//fresco
ImageRequestBuilder.newBuilderWithSource(uri)
.setResizeOptions(new ResizeOptions(500, 500)).build()
比如可以用這些方法來自動提高碼率,這樣圖片占用的顯存大小也會減少,但是我不知道這個內部是如何處理傳入的(500,500)的,因為我們要知道,系統開放的API只支持一定速率的壓縮,所以內部肯定會進行一層處理和轉換。
需要注意的是,我使用的是0.14.1版本。 不知道高版本怎么樣。 該版本的()套接字僅支持jpg格式的圖片。 如果要處理png圖片,網上有很多,可以自己查一下。
對于Glide來說,它已經根據控件的大小做了一個處理。 如果想自動處理,可以使用它的()方法。
總結
最后,稍微總結一下:
本文整理的理論基本上都是總結別人博客顯存并做相關實驗得到的推論。 推論的正確性自然弱于閱讀源碼本身。 因此,如果有錯誤的地方,歡迎賜教。 如果有時間,也可以看一下相關源碼來確認一下。
186信息網原創文章,轉載請注明本文來自:www.yjdjwpb.cn