很久之前寫的了,發了吧,原用來總結學習的,再不發估計轉行了,文章也參考了一些資料,摳用了一些圖,主要是爲了說明問題,總結學習
前言
app開發中,圖片是少不了的。各種圖標圖片資源,如果不能很好的處理圖片的利用。會導致app性能嚴重下降,影響用戶體驗,最直觀的感受就是卡頓,手機發熱,有時候還OOM
android系統給每個app分配有一定的內存,android系統的進程(app級別)有最大內存限制,超過這個限制系統就會拋出OOM錯誤。雖然在4.0後可以通過在application節點中設置屬性android:largeHeap=”true”來突破這個上限,但是由於圖片處理不當帶來的影響總是影響app性能的
這裏引申一下關於android系統給app分配的內存:
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
int memorySize = activityManager.getMemoryClass();
Android根據設備屏幕尺寸和dpi的不同,給系統分配的單應用程序內存大小也不同,可以實踐一下通過上面的方法獲取應用的內存,跟下表對比下:
屏幕尺寸 | DPI | 應用內存 |
---|---|---|
small / normal / large | ldpi / mdpi | 16MB |
small / normal / large | tvdpi / hdpi | 32MB |
small / normal / large | xhdpi | 64MB |
small / normal / large | 400dpi | 96MB |
small / normal / large | xxhdpi | 128MB |
xlarge | mdpi | 32MB |
xlarge | tvdpi / hdpi | 64MB |
xlarge | xhdpi | 128MB |
xlarge | 400dpi | 192MB |
xlarge | xxhdpi | 256MB |
拋磚
oom產生原因
一個頁面一次加載過多圖片
加載大圖片沒有進行壓縮(尺寸,質量)。。直接使用了imageView.setImageResource()
android列表加載大量bitmap沒有使用緩存。。。
android 支持的圖片
- png:
無損壓縮,比較大,需要進行壓縮,網站tinypng,一般都是讓美工處理。但解碼相對簡單
- jpeg:
有損壓縮,不支持透明通道,比如在ps裏背景透明的圖片,保持成jpg就不透明瞭,這裏不深入瞭解。但是解碼相對複雜
- webp:
google2010發佈,支持有損無損壓縮,支持透明通道,所以對圖片質量和大小有限制的情況下,webp是首選
- gif:
系統本身不支持,三方圖片庫支持:glide,fresco
關於android中圖片格式的使用,谷歌官方建議:儘量少使用png文件,建議使用webp格式的圖片,相比png小45%。所以,項目中圖片格式該如何平衡,這個還需要美工結合技術需求拿捏,結合每種格式圖片的優缺點,合理規劃開發。既然谷歌建議了,那大概是考慮到png佔內存大導致的,app開闢的運行內存是一定的,當然內存開銷越小,app越流暢嘛
圖片存儲優化
圖片佔用內存計算
這裏的圖片佔用內存是指在Navtive中佔用的內存,當然BitMap使用的絕大多數內存就是該內存。
因此我們可以簡單的認爲它就是BitMap所佔用的內存
Android中一張圖片(BitMap)佔用的內存主要和以下幾個因數有關:圖片長度,圖片寬度,單位像素佔用的字節數,圖片長度和圖片寬度的單位是像素。所以有如下計算公式:
內存 = 圖片長度 * 圖片寬度 * 單位像素佔用的字節數
這裏注意一下,圖片(BitMap)佔用的內存應該和屏幕密度(Density)無關,創建一個BitMap時,其單位像素佔用的字節數由其參數BitmapFactory.Options的inPreferredConfig變量決定。inPreferredConfig爲Bitmap.Config類型。Bitmap.Config類是個枚舉類型,如下:
Bitmap.Config | description |
---|---|
ALPHA_8 | Each pixel is stored as a single translucency (alpha) channel. This is very useful to efficiently store masks for instance. No color information is stored. With this configuration, each pixel requires 1 byte of memory.</br>此時圖片只有alpha值,沒有RGB值,一個像素佔用一個字節 |
ARGB_4444 | This field is deprecated. Because of the poor quality of this configuration, it is advised to use ARGB_8888instead. </br>這種格式的圖片,看起來質量太差,已經不推薦使用。</br>Each pixel is stored on 2 bytes. The three RGB color channels and the alpha channel (translucency) are stored with a 4 bits precision (16 possible values.) This configuration is mostly useful if the application needs to store translucency information but also needs to save memory. It is recommended to use ARGB_8888 instead of this configuration.</br>一個像素佔用2個字節,alpha(A)值,Red(R)值,Green(G)值,Blue(B)值各佔4個bites,共16bites,即2個字節 |
ARGB_8888 | Each pixel is stored on 4 bytes. Each channel (RGB and alpha for translucency) is stored with 8 bits of precision (256 possible values.) This configuration is very flexible and offers the best quality. It should be used whenever possible </br>一個像素佔用4個字節,alpha(A)值,Red(R)值,Green(G)值,Blue(B)值各佔8個bites,共32bites,即4個字節 </br>這是一種高質量的圖片格式,電腦上普通採用的格式。它也是Android手機上一個BitMap的默認格式。 |
RGB_565 | Each pixel is stored on 2 bytes and only the RGB channels are encoded: red is stored with 5 bits of precision (32 possible values), green is stored with 6 bits of precision (64 possible values) and blue is stored with 5 bits of precision. This configuration can produce slight visual artifacts depending on the configuration of the source. For instance, without dithering, the result might show a greenish tint. To get better results dithering should be applied. This configuration may be useful when using opaque bitmaps that do not require high color fidelity.</br> 一個像素佔用2個字節,沒有alpha(A)值,即不支持透明和半透明,Red(R)值佔5個bites ,Green(G)值佔6個bites ,Blue(B)值佔5個bites,共16bites,即2個字節.對於沒有透明和半透明顏色的圖片來說,該格式的圖片能夠達到比較的呈現效果,相對於ARGB_8888來說也能減少一半的內存開銷。因此它是一個不錯的選擇。另外我們通過android.content.res.Resources來取得一個張圖片時,它也是以該格式來構建BitMap的. |
舉個例子,圖片大小的計算:
圖片格式 | 公式 | 一張100 * 100的圖片佔用內存大小 |
---|---|---|
ALPHA_8 | 圖片長度 * 圖片寬度 | 100 * 100=10000字節 |
ARGB_4444 | 圖片長度 * 圖片寬度 * 2 | 100 * 100 * 2 = 20000字節 |
ARGB_8888 | 圖片長度 * 圖片寬度 * 4 | 100 * 100 * 4 = 40000字節 |
RGB_565 | 圖片長度 * 圖片寬度 * 2 | 100 * 100 * 2 = 20000字節 |
注意: ARGB _ 4444 從Android4.0開始,該選項無效。即使設置爲該值,系統任然會採用 ARGB _ 8888 來構造圖片,系統在把res的圖片解析成bitmap時默認是採用ARGB_8888的配置,如下源碼
if (config != null) {
switch (config) {
case RGB_565:
newConfig = Config.RGB_565;
break;
case ALPHA_8:
newConfig = Config.ALPHA_8;
break;
case RGBA_F16:
newConfig = Config.RGBA_F16;
break;
//noinspection deprecation
case ARGB_4444:
case ARGB_8888:
default:
newConfig = Config.ARGB_8888;
break;
}
}
圖片解碼格式枚舉下面在講解圖片存儲優化-質量壓縮時候會給出例子
圖片存儲優化
上面瞭解了儘量減少PNG圖片的大小是Android裏面很重要的一條規範,下面我們需要了解一下爲什麼要做內存的優化,如何做:
Android的Heap空間是不會自動做兼容壓縮的,意思就是如果Heap空間中的圖片被收回之後,這塊區域並不會和其他已經回收過的區域做重新排序合併處理,那麼當一個更大的圖片需要放到heap之前,很可能找不到那麼大的連續空閒區域,那麼就會觸發GC,使得heap騰出一塊足以放下這張圖片的空閒區域,如果無法騰出,就會發生OOM
所以,把圖片做小,圖片內存重用這是眼前的解決方案,我們可以從三個方面來降低圖片內存開銷:
尺寸壓縮
關於圖片縮放,有幾種方法:
1:Pre-scaling Bitmaps
2:inSampleSize
第一種,android中經常會做圖片的縮放,所以,預縮放意義很明顯,能縮小圖片(這裏不單單是縮放圖片尺寸,而是操作的bitmap),降低內存分配,提升顯示性能,api爲createScaledBitmap()。如下:
/**
* bitmap指定寬高
* @param bitmap
* @param width
* @param height
* @return
*/
public static Bitmap resizeBitmap(Bitmap bitmap, int width, int height) {
return Bitmap.createScaledBitmap(bitmap, width, height, true);
}
第二種是inSampleSize,作用是對原圖降採樣,通過設置inJustDecodeBounds = true 在圖片不加載進內存的情況下能獲取圖片寬高,計算合適的壓縮比,設置inSampleSize。
inSampleSize具體原理是直接從點陣中隔行抽取最有效率,所以爲了兼顧效率, inSampleSize只能是2的整數次冪,如果不是的話,向下取得最大的2的整數次冪.
比如你將 inSampleSize 賦值爲3,系統實際使用的縮放比率爲2,那就是每隔2行採1行,每隔2列採一列,那你解析出的圖片就是原圖大小的1/4.
這個值也可以填寫非2的倍數,非2的倍數會被四捨五入.
綜上,用這個參數解析bitmap就是爲了減少內存佔用
Q: inSampleSize取值多少最佳?
A: inSampleSize優化 這裏提供了具體的計算算法
總體來講:就是以圖片寬高較大的一邊爲參考邊進行壓縮,目的還是爲了避免壓縮過大導致圖片失真嚴重,效果不好。。
注意:inSampleSize官方解釋爲必須是2的整數次冪,如果不是,也會向減小方向尋找最近的2的整數次冪的數。但是,經過測試,並不是這樣,貌似有些是2的整數倍就行,比如6,24等。官方計算解釋是這樣的:
// Calculate the largest inSampleSize value that is a power of 2 and
// keeps both
// height and width larger than the requested height and width.
計算最大的inSampleSize值,它是2和的冪,並且仍保持高度和寬度大於要求的高度和寬度
但是inSampleSize的一些規則,可能會有這樣的場景,700的圖片壓縮到600.。結果是不會壓縮的,因爲計算的inSampleSize還是1,其實解碼圖片時候,影響大小的不止inSampleSize,還有其他一些參數:inDensity、inTargetDensity和inScreenDensity(備案學習文章)
inScaled:將它設置爲true,那麼代表這張圖片可以縮放
inDensity:圖片的原來密度,默認一般爲160.
inTargetDensity:圖片的目標密度,圖片操作之後的密度
inScreenDensity:這個參數默認一直是0,源碼中對這個參數沒有賦值的地方,只有一處使用的地方
因爲項目用的圖片一般都放在drawable文件夾中,所以,Options中的inDensity屬性會根據drawable文件夾的分辨率來賦值,inTartgetDensity會根據屏幕的像素密度來賦值
對應關係如下:
設備dpi | 密度類型 |
---|---|
0dpi ~ 120dpi | ldpi |
120dpi ~ 160dpi | mdpi |
160dpi ~ 240dpi | hdpi |
240dpi ~ 320dpi | xhdpi |
320dpi ~ 480dpi | xxhdpi |
480dpi ~ 640dpi | xxxhdpi |
drawable類型 | 分辨率 |
---|---|
ldpi | 120 |
mdpi | 160 |
hdpi | 240 |
xhdpi | 320 |
xxhdpi | 480 |
xxxhdpi | 640 |
輸出圖片寬高的公式如下:
輸出圖片的寬高= 原圖片的寬高 / inSampleSize * (inTargetDensity / inDensity)
注意:
1:上面計算公式僅針對於drawable文件夾的圖片來說,而對於一個file或者stream那麼inDensity和inTargetDensity是不考慮的!他們默認就是0
2:對於drawable中的圖片,inDensity是有默認值的,上面對應關係能看出來
3:inTargetDensity是跟屏幕密度有關的,這是屏幕參數,是常量,所以,可以通過以下方式獲得。
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
Log.i("MainActivity", "onCreate: " + metrics.densityDpi);
DisplayMetrics windowm = getApplicationContext().getResources().getDisplayMetrics();
Log.i("MainActivity", "onCreate: " + windowm.densityDpi);
繼續看,下面有具體的例子演示如何使用:
質量壓縮
質量壓縮就是解碼率壓縮,常見格式的圖片在設置到ui上之前需要經過解碼過程
Q:如何從解碼方面降低圖片內存佔用
A:使用RGB_565代替ARGB_8888可以降低圖片內存佔用
1:因爲它可以降低一個像素佔用的內存,RGB_565一個像素佔2個字節,ARGB_8888一個像素佔4個字節。通過設置options.inPreferredConfig = Bitmap.Config.RGB_565來處理
private void testPicOptimize(ImageView imageView, int size) {
String sdcard = Environment.getExternalStorageDirectory().getAbsolutePath();
String filePath = sdcard + "/xxx.jpg";
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filePath, options);
int width = options.outWidth;
options.inSampleSize = width / 200;
options.inScaled = true;
int calsize=options.outHeight>options.outWidth?options.outWidth:options.outHeight;
options.inTargetDensity =(size*options.inDensity)/(calsize/options.inSampleSize);
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFile(filePath, options);
imageView.setImageBitmap(bitmap);
}
設置inJustDecodeBounds=true,解析圖片,在不加載進內存的情況下獲取圖片寬高,然後進行設置尺寸壓縮、解碼格式,然後在inJustDecodeBounds=false,重新加載圖片到內存中,再講圖片設置到ui上
- 內存重用
android 3.0以後,BitmapFactory.Options提供了一個參數options.inBitmap。如果你使用了這個屬性,那麼使用這個屬性的decode過程中 會直接參考 inBitmap 所引用的那塊內存,,大家都知道 很多時候ui卡頓是因爲gc操作過多而造成的。使用這個屬性 能避免大內存塊的申請和釋放。帶來的好處就是gc 操作的數量減少。這樣cpu會有更多的時間 做ui線程,界面會流暢很多,同時還能節省大量內存!
private void testInBitmap(ImageView imageView) {
String sdcard = Environment.getExternalStorageDirectory().getAbsolutePath();
String filePath1 = sdcard + "/xxx.jpg";
BitmapFactory.Options options = new BitmapFactory.Options();
//size必須爲1 否則是使用inBitmap屬性會報異常
options.inSampleSize = 1;
//這個屬性一定要在用在src Bitmap decode的時候 不然你再使用哪個inBitmap屬性去decode時候會在c++層面報異常
//BitmapFactory: Unable to reuse an immutable bitmap as an image decoder target.
//一定要設置爲true 這樣返回的bitmap 纔是mutable 也就是可重用的,否則是不能重用的
options.inMutable = true;
Bitmap bitmap1 = BitmapFactory.decodeFile(filePath1, options);
//設置複用內存,加載bitmap1已經開闢過內存,所以後續設置了options.inBitmap的圖片加載會首先嚐試利用bitmap1所指向的內存
options.inBitmap = bitmap1;
String filePath2 = sdcard + "/xxx2.jpg";
//這時候bitmap2的內存是bitmap1的內存
Bitmap bitmap2 = BitmapFactory.decodeFile(filePath2, options);
imageView.setImageBitmap(bitmap2);
}
如上例子,實現了第二張圖複用了第一張圖的內存。。有個條件,複用內存的圖片要小於被複用的內存的大小,不然複用不了
使用options.inBitmap需要注意幾點:
1:在SDK 11 -> 18之間,重用的bitmap大小必須是一致的,例如給inBitmap賦值的圖片大小爲100-100,那麼新申請的bitmap必須也爲100-100才能夠被重用
2:從SDK 19開始,新申請的bitmap大小必須小於或者等於已經賦值過的bitmap大小
3:新申請的bitmap與舊的bitmap必須有相同的解碼格式,例如大家都是8888的,如果前面的bitmap是8888,那麼就不能支持4444與565格式的bitmap了,不同的編碼格式佔用的內存是不同的,有時候也可以根據需求指定編碼格式
上面的注意點很大程度上限制了我們使用內存重用的靈活性,難道app中的圖片都要一樣?解碼格式也要一樣?需求不同可能圖片處理就不同,所以,怎們充分的利用options.inBitmap纔是真正提升app內存性能的關鍵:
有一種思路:就是inBitmap池,也就是說管理一個包含多種典型可重用的bitmap集合。這樣,就很大程度的提升了bitmap內存重用概率
這種方案,現流行框架glide就是這麼處理的,當然,細節更深
Bitmap內存管理
Bitmap 對象在不使用時,我們應該先調用recycle()釋放內存,然後才置空,因爲加載bitmap對象的內存空間,一部分是java的,一部分是c的(因爲Bitmap分配的底層是通過jni調用的,BitMap底層是skia圖形庫,skia圖形庫是c實現的,通過jni的方法在java層進行封裝)。這個recycle()函數就是針對c部分的內存釋放
Q:bitmap的存儲在3.0前後有什麼改變?api的調用有什麼變化?
A:在android 3.0之前,像素數據支持保存在本地內存中的。而位圖bitmap本身是存儲在Dalvik堆中的,bitmap數據操作完之後,需要調用bitmap.recycle去釋放這些像素數據。3.0之後,像素數據和位圖都是存儲在Dalvik堆中的,所以bitmap對象是會自動回收的
通過dumpsys meminfo命令可以查看一個進程的內存使用情況,
當然也可以通過它來觀察我們創建或銷燬一張BitMap圖片內存的變化,從而推斷出圖片佔用內存的大小
adb shell "dumpsys meminfo com.lenovo.robin"
圖片加載優化
mipmap
- 在App中,無論你將圖片放在drawable還是mipmap目錄,系統只會加載對應density中的圖片。
而在Launcher中,如果使用mipmap,那麼Launcher會自動加載更加合適的密度的資源。 - 應用內使用到的圖片資源,並不會因爲你放在mipmap或者drawable目錄而產生差異。單純只是資源路徑的差異R.drawable.xxx或者R.mipmap.xxx。(也可能在低版本系統中有差異)
- 一句話來說就是,自動跨設備密度展示的能力是launcher的,而不是mipmap的。
總的來說,app圖標(launcher icon) 必須放在mipmap目錄中,並且最好準備不同密度的圖片,否則縮放後可能導致失真。
而應用內使用到的圖片資源,放在drawable目錄亦或是mipmap目錄中是沒有區別的,該準備多個密度的還是要準備多個密度,如果只想使用一份切圖,那儘量將切圖放在高密度的文件夾中
內存佔用與drawable文件夾關係
如標題換個說法,同一張圖片,放置在不同的drawable文件夾,在同一設備上運行,對圖片大小及內存佔用有什麼影響,下面一步步瞭解
手機屏幕密度對應屏幕密度類型
- 獲取手機屏幕密度:百度吧
工具..這個算出來的,爲啥跟代碼算出來的不一樣?
- 設備屏幕密度
設備dpi | 密度類型 |
---|---|
0dpi ~ 120dpi | ldpi |
120dpi ~ 160dpi | mdpi |
160dpi ~ 240dpi | hdpi |
240dpi ~ 320dpi | xhdpi |
320dpi ~ 480dpi | xxhdpi |
480dpi ~ 640dpi | xxxhdpi |
從上面的屏幕密度匹配類型能看出,假如你的手機dpi是260,那麼你的手機屏幕密度類型就是xhdpi,加載圖片,系統首先會去drawable-xhdpi目錄下查找,其他查找規則請繼續往下看
圖片大小以及dp和px關係一覽表
假設,在mdpi屏幕密度的手機上,你將一張60px乘60px的圖片放到mdpi中,它的大小是60乘60;若把它拿到hdpi中,那麼它的大小應該是45 乘 45,圖片縮小,因爲系統認爲這些圖片都是給高分辨率設備使用的.(由上表可以算出60*3/4)
加載順序
APP在查找圖片資源的時候遵循先高後低的原則,假設設備的分辨率是xxhdpi,那麼查找順序如下
- 先去drawable-xxhdpi文件夾查找,如果有這張圖片就使用,這個時候圖片不會縮放
- 如果沒有找到,則去更高密度的文件夾下找,例如drawable-xxxhdpi,密度依次遞增,如果找到了,圖片將會縮小,因爲系統認爲這些圖片都是給高分辨率設備使用的
- 所有高密度文件夾都沒有的話,就會去drawable-nodpi文件夾去找,如果找到,不縮放,使用原圖
- 還是沒有的話,就會去更低密度的文件夾下面找,xhdpi,hdpi等,密度依次遞減,如果找到了,圖片將會放大,因爲系統認爲這個圖片是給低分辨率設備使用的
總的來說,系統的規則也是優先向減小app運行內存的方向查找處理資源的,因爲找更高密度drawable下的圖片,加載爲bitmap是要縮小的
圖片加載
圖片從res中加載到內存都是以圖片的原始寬高比進行加載的,比如上文中博主採用的圖片是7201280,錘子T1的分辨率是 10801960,
把圖片放在drawable-xhdpi文件夾下,圖片的大小爲10801920,而不是充滿屏幕高度的1960。因爲圖片加載時首先滿足的是寬度,比如把720
放大到1080,此時保持圖片的寬高比不變,高度應該是等比例放大,h = 12801080/720。
圖片加載優化
常用方案有4個方向:
- 異步請求:圖片在線程或後臺請求
- 圖片緩存:列表中的圖片進行緩存
- 網絡請求:使用OKHttp
- 懶加載:當圖片呈現到可視區域再進行加載
android 大圖片加載方案
BitmapRegionDecoder。這個是api 10時候google提供打開大小超過屏幕的圖片的方案。這裏不多說了。查了一些資料,用法簡單,文章幾乎都一毛一樣,大家自行學習瞭解
說一點,由於是打開的超大圖片,所以,解決方案中就用到了前面所說的inJustDecodeBounds。通過設置inJustDecodeBounds = true 在圖片不加載進內存的情況下能獲取圖片寬高
框架優化圖片加載
常見框架:Universal image loader,picasso,glide,fresco等,待續