Android位圖(圖片)加載引入的內(nèi)存溢出問(wèn)題詳細(xì)解析
Android ImageView進(jìn)行圖片加載時(shí),經(jīng)常會(huì)遇到內(nèi)存溢出的問(wèn)題,本文針對(duì)于這一問(wèn)題出現(xiàn)的定義、原理、過(guò)程、解決方案做統(tǒng)一總結(jié)。
1.一些定義
在分析具體問(wèn)題之前,我們先了解一些基本概念,這樣可以幫助理解后面的原理部分。當(dāng)然了,大家對(duì)于這部分定義已經(jīng)了然于胸的,就可以跳過(guò)了。
什么是內(nèi)存泄露?
我們知道Java GC管理的主要區(qū)域是堆,Java中幾乎所有的實(shí)例對(duì)象數(shù)據(jù)實(shí)際是存儲(chǔ)在堆上的(當(dāng)然JDK1.8之后,針對(duì)于不會(huì)被外界調(diào)用的數(shù)據(jù)而言,JVM是放置于棧內(nèi)的)。針對(duì)于某一程序而言,堆的大小是固定的,我們?cè)诖a中新建對(duì)象時(shí),往往需要在堆中申請(qǐng)內(nèi)存,那么當(dāng)系統(tǒng)不能滿(mǎn)足需求,于是產(chǎn)生溢出。或者可以這樣理解堆上分配的內(nèi)存沒(méi)有被釋放,從而失去對(duì)其控制。這樣會(huì)造成程序能使用的內(nèi)存越來(lái)越少,導(dǎo)致系統(tǒng)運(yùn)行速度減慢,嚴(yán)重情況會(huì)使程序宕掉。
什么是位圖?
位圖使用我們稱(chēng)為像素的一格一格的小點(diǎn)來(lái)描述圖像,計(jì)算機(jī)屏幕其實(shí)就是一張包含大量像素點(diǎn)的網(wǎng)格,在位圖中,平時(shí)看到的圖像將會(huì)由每一個(gè)網(wǎng)格中的像素點(diǎn)的位置和色彩值來(lái)決定,每一點(diǎn)的色彩是固定的,而每個(gè)像素點(diǎn)色彩值的種類(lèi),產(chǎn)生了不同的位圖Config,常見(jiàn)的有:
ALPHA_8, 代表8位Alpha位圖,每個(gè)像素占用1byte內(nèi)存 RGB_565,代表8位RGB位圖,每個(gè)像素占用2byte內(nèi)存 ARGB_4444 (@deprecated),代表16位ARGB位圖,每個(gè)像素占用2byte內(nèi)存 ARGB_8888,代表32位ARGB位圖,每個(gè)像素占用4byte內(nèi)存
其實(shí)很好理解,我們知道RGB是指紅藍(lán)綠,不同的config代表,計(jì)算機(jī)中每種顏色用幾位二進(jìn)制位來(lái)表示,例如:RGB_565代表紅 5為、藍(lán)6位、綠5為。
2.原理分析
2.1 原理分析一
由第一節(jié)的基礎(chǔ)定義,我們知道不過(guò)JVM還是Android虛擬機(jī),對(duì)于每個(gè)應(yīng)用程序可用內(nèi)存大小是有約束的,而針對(duì)于單個(gè)程序中Bitmap所占的內(nèi)存大小也有約束(一般機(jī)器是8M、16M,大家可以通過(guò)查看build.prop文件去查看這個(gè)定義大?。?,一旦超過(guò)了這個(gè)大小,就會(huì)報(bào)OOM錯(cuò)誤。 Android編程中,我們經(jīng)常會(huì)使用ImageView 控件,加載圖片,例如以下代碼:
package com.itbird.BitmapOOM;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.widget.ImageView;
import androidx.appcompat.app.AppCompatActivity;
import com.itbird.R;
public class ImageViewLoadBitmapTestActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.imageviewl_load_bitmap_test);
ImageView imageView = findViewById(R.id.imageview);
imageView.setImageResource(R.drawable.bigpic);
imageView.setBackgroundResource(R.drawable.bigpic);
imageView.setImageBitmap(BitmapFactory.decodeFile("path/big.jpg"));
imageView.setImageBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.bigpic));
}
}當(dāng)圖片很小時(shí),一般不會(huì)有問(wèn)題,當(dāng)圖片很大時(shí),就會(huì)出現(xiàn)OOM錯(cuò)誤,原因是直接調(diào)用decodeResource、setImageBitmap、setBackgroundResource時(shí),實(shí)際上,這些函數(shù)在完成照片的decode之后,都是調(diào)用了java底層的createBitmap來(lái)完成的,需要消耗更多的內(nèi)存。至于為什么會(huì)消耗那么多內(nèi)存,如下面的源碼分析: android8.0之前Bitmap源碼
public final class Bitmap implements Parcelable {
private static final String TAG = "Bitmap";
...
private byte[] mBuffer;
...
}android8.0之后Bitmap源碼
public final class Bitmap implements Parcelable {
...
// Convenience for JNI access
private final long mNativePtr;
...
}對(duì)上上述兩者,相信大家已經(jīng)看出點(diǎn)什么了,android8.0之前,Bitmap在Java層保存了byte數(shù)組,而且細(xì)跟源碼的話(huà),您也會(huì)發(fā)現(xiàn),8.0之前雖然調(diào)用了native函數(shù),但是實(shí)際其實(shí)就是在native層創(chuàng)建Java層byte[],并將這個(gè)byte[]作為像素存儲(chǔ)結(jié)構(gòu),之后再通過(guò)在native層構(gòu)建Java Bitmap對(duì)象的方式,將生成的byte[]傳遞給Bitmap.java對(duì)象。(這里其實(shí)有一個(gè)小知識(shí)點(diǎn),android6.0之前,源碼里面很多這樣的實(shí)現(xiàn),通過(guò)C層來(lái)創(chuàng)建Java層對(duì)象)。

而android8.0之后,Bitmap在Java層保存的只是一個(gè)地址,,Bitmap像素內(nèi)存的分配是在native層直接調(diào)用calloc,所以其像素分配的是在native heap上, 這也是為什么8.0之后的Bitmap消耗內(nèi)存可以無(wú)限增長(zhǎng),直到耗盡系統(tǒng)內(nèi)存,也不會(huì)提示Java OOM的原因。

2.2 原理分析二
看完上面的源碼解讀,大家一定想知道,那我如果在自己應(yīng)用中的確有大圖片的加載需求,那怎么辦呢?調(diào)用哪個(gè)函數(shù)呢? BitmapFactory.java中有一個(gè)Bitmap decodeStream(InputStream is)這個(gè)函數(shù),我們可以查看源碼,這個(gè)函數(shù)底層調(diào)用了native c函數(shù)

在底層進(jìn)行了decode之后,轉(zhuǎn)換為了bitmap對(duì)象,返回給Java層。
3 編程中如何避免圖片加載的OOM錯(cuò)誤
通過(guò)上面章節(jié)的知識(shí)探索,相信大家已經(jīng)知道了加載圖片時(shí)出現(xiàn)OOM錯(cuò)誤的原因,其實(shí)真正的原因并未是網(wǎng)上很多文章說(shuō)的,不要使用調(diào)用ImageView的某某函數(shù)、BitmapFactory的某某函數(shù),真正的原因是,對(duì)于大圖片,Java堆和Native堆無(wú)法申請(qǐng)到可用內(nèi)存時(shí),就會(huì)出現(xiàn)OOM錯(cuò)誤,那么針對(duì)于不同的系統(tǒng)版本,Android存儲(chǔ)、創(chuàng)建圖片的方式又有所不同,帶來(lái)了加載大圖片時(shí)的OOM錯(cuò)誤。 那么接下來(lái),大家最關(guān)心的解決方案,有哪些?我們?cè)谌粘>幋a中,應(yīng)該如何編碼,才能有效規(guī)避此類(lèi)錯(cuò)誤的出現(xiàn),別急。
3.1 利用BitmapFactory.decodeStream加載InputStream圖片字節(jié)流的方式顯示圖片
/**
* 以最省內(nèi)存的方式讀取本地資源的圖片
*/
public static Bitmap readBitMap(String path, BitmapFactory.Options opt, InputStream is) {
opt.inPreferredConfig = Bitmap.Config.RGB_565;
if (Build.VERSION.SDK_INT <=android.os.Build.VERSION_CODES.KITKAT ) {
opt.inPurgeable = true;
opt.inInputShareable = true;
}
opt.inSampleSize = 2;//二分之一縮放,可寫(xiě)1即100%顯示
//獲取資源圖片
try {
is = new FileInputStream(path);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return BitmapFactory.decodeStream(is, null, opt);
}大家可以看到上面的代碼,實(shí)際上一方面針對(duì)Android 4.4之下的直接聲明了opt屬性,告訴系統(tǒng)可以回收,一方面直接進(jìn)行了圖片縮放。說(shuō)到這里,大家會(huì)有疑問(wèn),為什么是android4.4以下加這兩個(gè)屬性,難道之后就不用了了。不要著急,我們看源碼:

可以看到源碼上說(shuō)明,此屬性4.4之前有用,5.0之后即使設(shè)置了,底層也是忽略的。也許大家會(huì)問(wèn),難道5.0之后Bitmap的源碼有什么大的改動(dòng)嗎?的確是,可以看一下以下源碼。 8.0之后的Bitmap內(nèi)存回收機(jī)制 NativeAllocationRegistry是Android 8.0引入的一種輔助自動(dòng)回收native內(nèi)存的一種機(jī)制,當(dāng)Java對(duì)象因?yàn)镚C被回收后,NativeAllocationRegistry可以輔助回收J(rèn)ava對(duì)象所申請(qǐng)的native內(nèi)存,拿Bitmap為例,如下:
Bitmap(long nativeBitmap, int width, int height, int density,
boolean isMutable, boolean requestPremultiplied,
byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
...
mNativePtr = nativeBitmap;
long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
<!--輔助回收native內(nèi)存-->
NativeAllocationRegistry registry = new NativeAllocationRegistry(
Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
registry.registerNativeAllocation(this, nativeBitmap);
if (ResourcesImpl.TRACE_FOR_DETAILED_PRELOAD) {
sPreloadTracingNumInstantiatedBitmaps++;
sPreloadTracingTotalBitmapsSize += nativeSize;
}
}當(dāng)然這個(gè)功能也要Java虛擬機(jī)的支持,有機(jī)會(huì)再分析。
**實(shí)際使用效果:**3M以?xún)?nèi)的圖片加載沒(méi)有問(wèn)題,但是大家注意到一點(diǎn),沒(méi)我們代碼中是固定縮放了一般,這時(shí)大家肯定有疑問(wèn),有沒(méi)有可能,去動(dòng)態(tài)根據(jù)圖片的大小,決定縮放比例。
3.2 利用BitmapFactory.decodeStream通過(guò)按比例壓縮方式顯示圖片
/**
* 以計(jì)算的壓縮比例加載大圖片
*
* @param res
* @param resId
* @param reqWidth
* @param reqHeight
* @return
*/
public static Bitmap decodeCalSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
// 檢查bitmap的大小
final BitmapFactory.Options options = new BitmapFactory.Options();
// 設(shè)置為true,BitmapFactory會(huì)解析圖片的原始寬高信息,并不會(huì)加載圖片
options.inJustDecodeBounds = true;
options.inPreferredConfig = Bitmap.Config.RGB_565;
BitmapFactory.decodeResource(res, resId, options);
// 計(jì)算采樣率
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 設(shè)置為false,加載bitmap
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
/*********************************
* @function: 計(jì)算出合適的圖片倍率
* @options: 圖片bitmapFactory選項(xiàng)
* @reqWidth: 需要的圖片寬
* @reqHeight: 需要的圖片長(zhǎng)
* @return: 成功返回倍率, 異常-1
********************************/
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth,
int reqHeight) {
// 設(shè)置初始?jí)嚎s率為1
int inSampleSize = 1;
try {
// 獲取原圖片長(zhǎng)寬
int width = options.outWidth;
int height = options.outHeight;
// reqWidth/width,reqHeight/height兩者中最大值作為壓縮比
int w_size = width / reqWidth;
int h_size = height / reqHeight;
inSampleSize = w_size > h_size ? w_size : h_size; // 取w_size和h_size兩者中最大值作為壓縮比
Log.e("inSampleSize", String.valueOf(inSampleSize));
} catch (Exception e) {
return -1;
}
return inSampleSize;
}大家可以看到,上面代碼實(shí)際上使用了一個(gè)屬性inJustDecodeBounds,當(dāng)inJustDecodeBounds設(shè)為true時(shí),不會(huì)加載圖片僅獲取圖片尺寸信息,也就是說(shuō),我們先通過(guò)不加載實(shí)際圖片,獲取其尺寸,然后再按照一定算法(以需要的圖片長(zhǎng)寬與實(shí)際圖片的長(zhǎng)寬比例來(lái)計(jì)算)計(jì)算出壓縮的比例,然后再進(jìn)行圖片加載。
**實(shí)際使用效果:**測(cè)試該方法可以顯示出來(lái)很大的圖片,只要你設(shè)定的長(zhǎng)寬合理。
3,3 及時(shí)的回收和釋放
直接上代碼
/**
* 回收bitmap
*/
private static void recycleBitmap(ImageView iv) {
if (iv != null && iv.getDrawable() != null) {
BitmapDrawable bitmapDrawable = (BitmapDrawable) iv.getDrawable();
iv.setImageDrawable(null);
if (bitmapDrawable != null) {
Bitmap bitmap = bitmapDrawable.getBitmap();
if (bitmap != null) {
bitmap.recycle();
}
}
}
}
/**
* 在Activity或Fragment的onDestory方法中進(jìn)行回收(必須確保bitmap不在使用)
*/
public static void recycleBitmap(Bitmap bitmap) {
// 先判斷是否已經(jīng)回收
if (bitmap != null && !bitmap.isRecycled()) {
// 回收并且置為null
bitmap.recycle();
bitmap = null;
}
}4.總結(jié)
4.1 OOM出現(xiàn)原因
對(duì)于大圖片,直接調(diào)用decodeResource、setImageBitmap、setBackgroundResource時(shí),這些函數(shù)在完成照片的decode之后,都是調(diào)用了java底層的createBitmap來(lái)完成的,需要消耗更多的內(nèi)存。Java堆和Native堆無(wú)法申請(qǐng)到可用內(nèi)存時(shí),就會(huì)出現(xiàn)OOM錯(cuò)誤,那么針對(duì)于不同的系統(tǒng)版本,Android存儲(chǔ)、創(chuàng)建圖片的方式又有所不同,帶來(lái)了加載大圖片時(shí)的OOM錯(cuò)誤。
4.2 解決方案
1.針對(duì)于圖片小而且頻繁加載的,可以直接使用系統(tǒng)函數(shù)setImageXXX等 2針對(duì)于大圖片,在進(jìn)行ImageView setRes之前,需要先對(duì)圖片進(jìn)行處理 1)壓縮 2)android4.4之前,需要設(shè)置opt,釋放bitmap,android5.0之后即使設(shè)置,系統(tǒng)也會(huì)忽略 3)設(shè)置optConfig為565,降低每個(gè)像素點(diǎn)的色彩值 4)針對(duì)于頻繁使用的圖片,可以使用inBitmap屬性 5)由于decodeStream直接讀取的圖片字節(jié)碼,并不會(huì)根據(jù)各種機(jī)型做自動(dòng)適配,所以需要在各個(gè)資源文件夾下放置相應(yīng)的資源 6)及時(shí)回收
總結(jié)
到此這篇關(guān)于Android位圖(圖片)加載引入的內(nèi)存溢出問(wèn)題詳細(xì)解析的文章就介紹到這了,更多相關(guān)Android位圖加載引入內(nèi)存溢出內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android AsyncTask實(shí)現(xiàn)異步處理任務(wù)的方法詳解
這篇文章主要介紹了Android AsyncTask實(shí)現(xiàn)異步處理任務(wù)的方法詳解的相關(guān)資料,需要的朋友可以參考下2017-04-04
屬于自己的Android對(duì)話(huà)框(Dialog)自定義集合
這篇文章主要為大家分享了一個(gè)屬于自己的Android對(duì)話(huà)框(Dialog)自定義集合,建立自己的Android對(duì)話(huà)框,感興趣的小伙伴們可以參考一下2016-02-02
Android Gradle Build Error:Some file crunching failed, see l
這篇文章主要介紹了Android Gradle Build Error:Some file crunching failed, see logs for details解決辦法的相關(guān)資料,需要的朋友可以參考下2016-11-11
Android實(shí)現(xiàn)一個(gè)帶粘連效果的LoadingBar
Loading效果相信大家應(yīng)該都實(shí)現(xiàn)過(guò),最近發(fā)現(xiàn)了一個(gè)不錯(cuò)的效果,決定分享給大家,所以下面這篇文章主要給大家介紹了關(guān)于利用Android實(shí)現(xiàn)一個(gè)帶粘連效果的LoadingBar的相關(guān)資料,需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-12-12
詳解Android如何實(shí)現(xiàn)好的彈層體驗(yàn)效果
當(dāng)前?App?的設(shè)計(jì)趨勢(shì)越來(lái)越希望給用戶(hù)沉浸式體驗(yàn),這種設(shè)計(jì)會(huì)讓用戶(hù)盡量停留在當(dāng)前的界面,而不需要太多的跳轉(zhuǎn),這就需要引入彈層。本篇我們就來(lái)講講彈層這塊需要注意哪些用戶(hù)體驗(yàn)2022-11-11
詳解Android首選項(xiàng)框架的使用實(shí)例
首選項(xiàng)這個(gè)名詞對(duì)于熟悉Android的朋友們一定不會(huì)感到陌生,它經(jīng)常用來(lái)設(shè)置軟件的運(yùn)行參數(shù)。本篇文章主要介紹詳解Android首選項(xiàng)框架的使用實(shí)例,有興趣的可以了解一下。2016-11-11
Android實(shí)現(xiàn)網(wǎng)絡(luò)加載時(shí)的對(duì)話(huà)框功能
這篇文章主要介紹了Android實(shí)現(xiàn)網(wǎng)絡(luò)加載時(shí)的對(duì)話(huà)框功能,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-02-02
Android仿淘寶view滑動(dòng)至屏幕頂部會(huì)一直停留在頂部的位置
這篇文章主要介紹了Android仿淘寶view滑動(dòng)至屏幕頂部會(huì)一直停留在頂部的位置的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-11-11

