本文出自 “阿敏其人” 簡書博客,轉(zhuǎn)載或引用請注明出處。
OOM(Out Of Memory)
什么是OOM
手機(jī)系統(tǒng)內(nèi)存份存儲內(nèi)存(ROM)和運行內(nèi)存(RAM),我們談?wù)揙OM討論的是運行內(nèi)存,這點如果是新人需要明確。。現(xiàn)在一般來說手機(jī)運行內(nèi)存是2G,3G基本就算很頂配了,4G運行內(nèi)存的話只有個別手機(jī)配置了。
簡而言之,OOM就是我們申請的內(nèi)存太大了,超出了系統(tǒng)分配給我們(app或者說進(jìn)程)的可用內(nèi)存。
android系統(tǒng)的app的每個進(jìn)程或者每個虛擬機(jī)有個最大內(nèi)存限制,如果申請的內(nèi)存資源超過這個限制,系統(tǒng)就會拋出OOM錯誤。跟整個設(shè)備的剩余內(nèi)存沒太大關(guān)系。比如比較早的android系統(tǒng)的一個虛擬機(jī)最多16M內(nèi)存,當(dāng)一個app啟動后,虛擬機(jī)不停的申請內(nèi)存資源來裝載圖片,當(dāng)超過內(nèi)存上限時就出現(xiàn)OOM。

舉個栗子,一條金魚,每次只能吃24顆飼料,你偏偏要喂它30顆,結(jié)果,金魚受不鳥,就掛掉了。
安卓手機(jī)有多少內(nèi)存
早期的手機(jī)是每個進(jìn)程(每個app)分配16M。
后來隨著慢慢發(fā)展,開始有了24M的,32M的,再變態(tài)就是64了。
具體每個手機(jī)的給app分配的運行內(nèi)存根據(jù)廠商和機(jī)型的不同而定,但是基本的幾個數(shù)值是一樣的。
安卓手機(jī)基于Linux系統(tǒng),Linux是一個多用戶的操作系統(tǒng),一個app在安卓手機(jī)里面就是一個用戶,一個用戶分配到了16m(假如是16m,那么統(tǒng)一每一個app就是16m),當(dāng)我當(dāng)前這個app掛了,不會影響我其他程序的運行。
比如我的手機(jī)里面有10個app,其中3個在運行,那么這個手機(jī)就有3個進(jìn)程在運行,這3個進(jìn)程每一個都分配到了(16m)的運行內(nèi)存。
每個App的內(nèi)存怎么分配
我是一個app,我被啟動了,我分配到了16m的空間,而且,這16m還不是完完整整給你當(dāng)前程序自己玩?zhèn)€夠的,有一部分還必須分給native內(nèi)存。
- 那么每一個程序的分配到的運行內(nèi)存到底是怎么分配的呢?
16M = dalvik內(nèi)存(Java) + native內(nèi)存(C/C++)
APP內(nèi)存由 dalvik內(nèi)存 和 native內(nèi)存 2部分組成,dalvik也就是java堆,創(chuàng)建的對象就是就是在這里分配的,而native是通過c/c++方式申請的內(nèi)存,Bitmap就是以這種方式分配的。(android3.0以后,系統(tǒng)都默認(rèn)通過dalvik分配的,native作為堆來管理)。這2部分加起來不能超過android對單個進(jìn)程,虛擬機(jī)的內(nèi)存限制。
至于這Dvlyik和Native兩部分的分配,有個特點值得說一下。那就是Dalvik(Java)申請的內(nèi)存即使釋放了,native也別想去申請,只能Dalvik自己用,Dalivk申請過的內(nèi)存Native就不能用了。
以下為引用部分
基于Android開發(fā)多媒體和游戲應(yīng)用時,可能會挺經(jīng)常出現(xiàn)Out Of Memory 異常 ,顧名思義這個異常是說你的內(nèi)存不夠用或者耗盡了。
在Android中,一個Process 只能使用16M內(nèi)存,如果超過了這個限制就會跳出這個異常。這樣就要求我們要時刻想著釋放資源。Java的回收工作是交給GC的,如何讓GC能及時的回收已經(jīng)不是用的對象,這個里面有很多技巧,大家可以google一下。
因為總內(nèi)存的使用超過16M而導(dǎo)致OOM的情況,非常簡單,我就不繼續(xù)展開說。值得注意的是Bitmap在不用時,一定要recycle,不然OOM是非常容易出現(xiàn)的。
本文想跟大家一起討論的是另一種情況:明明還有很多內(nèi)存,但是發(fā)生OOM了。
這種情況經(jīng)常出現(xiàn)在生成Bitmap的時候。有興趣的可以試一下,在一個函數(shù)里生成一個13m 的int數(shù)組。
再該函數(shù)結(jié)束后,按理說這個int數(shù)組應(yīng)該已經(jīng)被釋放了,或者說可以釋放,這個13M的空間應(yīng)該可以空出來,
這個時候如果你繼續(xù)生成一個10M的int數(shù)組是沒有問題的,反而生成一個4M的Bitmap就會跳出OOM。這個就奇怪了,為什么10M的int夠空間,反而4M的Bitmap不夠呢?
這個問題困擾很久,在網(wǎng)上,國外各大論壇搜索了很久,一般關(guān)于OOM的解釋和解決方法都是,如何讓GC盡快回收的代碼風(fēng)格之類,并沒有實際的支出上述情況的根源。
直到昨天在一個老外的blog上終于看到了這方面的解釋,我理解后歸納如下:
在Android中:
1.一個進(jìn)程的內(nèi)存可以由2個部分組成:java 使用內(nèi)存 ,C 使用內(nèi)存 ,這兩個內(nèi)存的和必須小于16M,不然就會出現(xiàn)大家熟悉的OOM,這個就是第一種OOM的情況。
2.更加奇怪的是這個:一旦內(nèi)存分配給Java后,以后這塊內(nèi)存即使釋放后,也只能給Java的使用,這個估計跟java虛擬機(jī)里把內(nèi)存分成好幾塊進(jìn)行緩存的原因有關(guān),反正C就別想用到這塊的內(nèi)存了,所以如果Java突然占用了一個大塊內(nèi)存,即使很快釋放了:
C能使用的內(nèi)存 = 16M - Java某一瞬間占用的最大內(nèi)存。
而Bitmap的生成是通過malloc進(jìn)行內(nèi)存分配的,占用的是C的內(nèi)存,這個也就說明了,上述的4MBitmap無法生成的原因,因為在13M被Java用過后,剩下C能用的只有3M了。
引用至此結(jié)束
點此查看原文地址
引用這一部分的描述,就是為了進(jìn)一步證明,每個app所占用的16m(比如說16m)運行內(nèi)存不是自己可以玩?zhèn)€夠的,還得和另外一個小伙伴分享
另外清楚一點,1、我們在Bitmap的時候申請的內(nèi)存是輸入C/C++的,也就是Native這一塊的
OOM一般在什么時候發(fā)生?
造成OOM的可以概括為兩種情況:
1、Bitmap的使用上 (利用Lru的LruCache和DiskLruCache兩個類來解決)
2、線程的管理上(利用線程池管理解決。不納入本次探討)
Bitmap導(dǎo)致的OOM是比較常見的,而針對Bitmap,常見的有兩種情況:
- 單個ImageView加載高清大圖的時候
- ListView或者GridView等批量快速加載圖片的時候
簡而言之,幾乎都是操作Bitmap的時候發(fā)生的。
制造一個OOM的例子
當(dāng)前環(huán)境:
- Android Studio1.4
- win7 64bit
- 模擬器: Genymotion Nexus One 2.3.7 API10 480*800
如何獲得當(dāng)前手機(jī)把為每個app(進(jìn)程)分配的運行內(nèi)存
// 測試每個app可用的最大內(nèi)存(安卓每一個app都運行在自己獨立的沙箱里面)
ActivityManager activityManager=(ActivityManager)MainActivity.this.getSystemService(Context.ACTIVITY_SERVICE);
int memoryClass = activityManager.getMemoryClass();// 返回的就是本機(jī)給每個app分配的運行內(nèi)存
當(dāng)我們當(dāng)前測模擬器返回的 32M 的運行內(nèi)存

在此附上相關(guān)代碼:
public class MainActivity extends Activity implements View.OnClickListener {
private TextView mTvBtn; // 按鈕
private TextView mTvNum; // 顯示最大內(nèi)存
private TextView mTvLoadBigPic; // 加載圖片按鈕
private ImageView mIvPic;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
mTvBtn= (TextView) findViewById(R.id.mTvBtn);
mTvNum= (TextView) findViewById(R.id.mTvNum);
mTvLoadBigPic= (TextView) findViewById(R.id.mTvLoadBigPic);
mIvPic= (ImageView) findViewById(R.id.mIvPic);
mTvBtn.setOnClickListener(this);
mTvLoadBigPic.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.mTvBtn:
// 測試每個app可用的最大內(nèi)存(安卓每一個app都運行在自己獨立的沙箱里面)
ActivityManager activityManager =(ActivityManager)MainActivity.this.getSystemService(Context.ACTIVITY_SERVICE);
int memoryClass = activityManager.getMemoryClass();// 返回的就是本機(jī)給每個app分配的運行內(nèi)存
mTvNum.setText("最大內(nèi)存: "+memoryClass);
break;
case R.id.mTvLoadBigPic:
Bitmap bigPicBitMap=BitmapFactory.decodeResource(getResources(),R.mipmap.test_pic);
mIvPic.setImageBitmap(bigPicBitMap);
break;
}
}
}
看完代碼,我們這里應(yīng)該停下來看看一下Bitmap類,補(bǔ)充一些知識
通過這樣的代碼就可以首先從資源文件里面加載圖片
Bitmap bigPicBitMap=BitmapFactory.decodeResource(getResources(),R.mipmap.test_pic);
mIvPic.setImageBitmap(bigPicBitMap);
關(guān)于Bitmap和BitmapFactory的知識可以百度補(bǔ)充
提一下,BitmapFactory提供了4類方法用于加載Bitmap對象
- 1、decodeFile
- 2、decodeResource
- 3、decodeStream
- 4、decodeByteArray
分別從文件系統(tǒng)、資源、輸入流和字節(jié)數(shù)組讀取Bitmap對象
其中,decodeFile和decodeResource又間接調(diào)用了decodeStream方法,這四類方法都是在安卓底層實現(xiàn)的,對應(yīng)BitmapFactory類的幾個Native類。
decodeResource這個方法內(nèi)應(yīng)說到底還是需要創(chuàng)建一個位圖(Bitmap),
對于創(chuàng)建位圖,我們來補(bǔ)充一個知識,先看一下的下面這個方法:
public static Bitmap createBitmap (int[] colors, int width, int height, Bitmap.Config config)
具體安卓內(nèi)部如何調(diào)用這個方法本人不得而知,但是我們要明白的是 config 這個參數(shù),每一個位圖都有一個默認(rèn)confit參數(shù),默認(rèn)值是 ARGB8888
對于config,有幾個值,我們借用一個文章說明一下:
A:透明度
R:紅色
G:綠
B:藍(lán)
Bitmap.Config ARGB_4444:由4個4位組成,即A=4,R=4,G=4,B=4,那么一個像素點占4+4+4+4=16位
Bitmap.Config ARGB_8888:由4個8位組成,即A=8,R=8,G=8,B=8,那么一個像素點占8+8+8+8=32位
Bitmap.Config RGB_565:即R=5,G=6,B=5,沒有透明度,那么一個像素點占5+6+5=16位
Bitmap.Config ALPHA_8:只有透明度,沒有顏色,那么一個像素點占8位。
一般情況下我們都是使用的ARGB_8888,由此可知它是最占內(nèi)存的,因為一個像素占32位,8位=1字節(jié)(byte),所以一個像素占4字節(jié)的內(nèi)存。假設(shè)有一張480x800的圖片,如果格式為ARGB_8888,那么將會占用(480x800x32)/(8x1024) = 1500KB的內(nèi)存。
簡單來說,我們可以知道,Bitmap默認(rèn)的ARGB8888是一個質(zhì)量較好參數(shù),畢竟一個像素點有32個比特位(bit),相當(dāng)于4個字節(jié)(Byte)了。
夢回唐朝,接著說OOM的例子
有了Bitmap和config的知識之后,我們的OOM的成功與否就看我們圖片的分辨率了
加入說,圖片分辨率是2500+1000,那么加載這張圖片所需要的運行內(nèi)存我們可以大概這么算:
Bitmap.Config ARGB_8888情況下,一個像素點占32位,也就是4個字節(jié)。
2500 * 1000 * 4 得出多少個byte
(2500 * 1000) / 1024 得出kb
(2500 * 1000) / 1024 / 1024 得出m
經(jīng)過運算,得出加載所需的運行內(nèi)存大致為9.5m
是不是說我們當(dāng)前手機(jī)的這個app就一定可以加載這張圖片呢?
不一定,如果這個時候app還有其他代碼也占用著的內(nèi)存,可能就加載不了了。而且我們說過,分配到的16m內(nèi)存不是自己玩?zhèn)€夠,還得兩個哥們分著玩。
如果想一針見血,徹底減小,可以整個5000*2000的圖片,肯定馬上掛掉,爆出OOM。
5000 * 2000 * 4 / 1024 / 1024 得出 38m多。一針見血
高效加載大圖和二級緩存,避免OOM
知道了OOM是什么,怎么發(fā)生的,接下來我們就應(yīng)該知道怎么解決問題了。
提出的問題的人很多,拿出解決辦法才是關(guān)鍵。
如何高效加載大圖?
造成OOM的核心原因:圖片分辨率過大
核心解決辦法:圖片,我們只加載適合的、需要的尺寸!!利用BitmapFactory.Options可完成這一項任務(wù)。
注意:我們要處理的分辨率的問題,而不是圖片本身大小的問題,一個100*100的10m的圖片和一張2000*2000的2m的圖片,對我們來說,2m的那張對我們來說反而是大圖片,我們針對的是分辨率
通過BitmapFactory.Options通過指定的采樣率來縮小圖片的分辨率,把縮小到合適分辨率的圖片的放到ImageView上面來顯示,大大降低了內(nèi)存壓力,有效避免OOM,至于縮小的怎樣的分辨率才算合適,谷歌有為我們提供了一段代碼,就可以得出這個合適的度!這段代碼后面會貼出。
inSimpleSize的比例計算
計算采樣率,主要是通過 BitmaoFactory.Options 的inSimpleSize參數(shù)進(jìn)行。
這里我們以120*800的分辨率的圖片舉例子
當(dāng)inSimpleSize為1時,圖片的分辨率就是原來的分辨率,也就是1200*800
當(dāng)inSimpleSize為2時,表示圖片的寬和高都是為原來的1/2,所整張圖變成了原來的1/4
當(dāng)inSimpleSize位4時,表示圖片的寬和高都是為原來的1/4,所以整張圖也就變成原來的1/16
依次類推
inSimpleSize數(shù)值的說明
- inSimpleSize的值必須是整數(shù)
- inSimpleSize的值不能是負(fù)數(shù),負(fù)數(shù)無效
- inSimpleSize的值谷歌建議是2的整數(shù)倍,當(dāng)然你可以寫個3,但是最好不要這么干
inSimpleSize的數(shù)值怎么確定
這里我們以為400*400圖片為例子
比如我們ImageView的大小位100*100,那么我們的,那么這時我們寫一個 inSimpleSize 為2的值,那么久剛好變成原圖的四分之一,那么很好,剛剛好,那么如果ImageView的大小是320*120之類的呢?問題就來了,怎么去的一個合適的值呢,還有就是,一個頁面有多個ImageView,難道我們?yōu)槊恳粋€ImageView都去挨個計算取樣值嗎?明顯不可能。
inSimpleSize怎么用?。?/h2>
谷歌為我們提供了一個規(guī)則,很好用,看代碼之前,我們還是文字說一下吧,主要邏輯如下,分三步走:
- (1) 將 BitmapFactory的 inJustDecodeBounds 參數(shù)設(shè)置為true,當(dāng)設(shè)置為true,代表此時不真正加載圖片,而是將圖片的原始寬和高數(shù)值讀取出來
- (2) 利用options取出原始圖片的寬高和請求的寬高進(jìn)行比較,計算出一個合適的inSimpleSize的值
- (3) 將 BitmapFactory的 inJustDecodeBounds 參數(shù)設(shè)置為false,真正開始加載圖片(這時候加載就是經(jīng)過計算后的分辨率)
** 谷歌提供的方法:**
import java.io.FileDescriptor;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
public class ImageResizer {
private static final String TAG = "ImageResizer";
public ImageResizer() {
}
// 從資源加載
public Bitmap decodeSampledBitmapFromResource(Resources res,int resId, int reqWidth, int reqHeight) {
// 設(shè)置inJustDecodeBounds = true ,表示先不加載圖片
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 調(diào)用方法計算合適的 inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);
// inJustDecodeBounds 置為 false 真正開始加載圖片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
// 設(shè)置inJustDecodeBounds = true ,表示先不加載圖片
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, options);
// 調(diào)用方法計算合適的 inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);
// inJustDecodeBounds 置為 false 真正開始加載圖片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd, null, options);
}
// 計算 BitmapFactpry 的 inSimpleSize的值的方法
public int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}
// 獲取圖片原生的寬和高
final int height = options.outHeight;
final int width = options.outWidth;
Log.d(TAG, "origin, w= " + width + " h=" + height);
int inSampleSize = 1;
// 如果原生的寬高大于請求的寬高,那么將原生的寬和高都置為原來的一半
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// 主要計算邏輯
// Calculate the largest inSampleSize value that is a power of 2 and
// keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
Log.d(TAG, "sampleSize:" + inSampleSize);
return inSampleSize;
}
}
來一個調(diào)用的代碼示例:
mIvPic.setImageBitmap(new ImageResizer().decodeSampledBitmapFromResource(getResources(),R.mipmap.test_pic,300,200));

注意看下面控制臺的打印信息
加載一張寬高為 5120*3200的圖片,依然沒問題,sampleSize為16
16*16=256,代表現(xiàn)在加載的這樣圖是原圖的256分之1.
差別好大
10-23 08:34:13.884 13265-13265/oomtest.amqr.com.oomandbitmap D/ImageResizer: origin, w= 5120 h=3200
10-23 08:34:13.884 13265-13265/oomtest.amqr.com.oomandbitmap D/ImageResizer: sampleSize:16
高效加載圖片不報OOM就先說到這里啦,下一篇再說圖片的二級緩存,也叫圖片的存取機(jī)制
二級,即為內(nèi)存緩存,本地緩存,網(wǎng)絡(luò),,三者一起構(gòu)成了圖片的存取機(jī)制。
內(nèi)存緩存拿不到就去本地拿。本地拿不到就去網(wǎng)絡(luò)拿。當(dāng)我們第一次獲取A圖片,肯定是是從網(wǎng)絡(luò)獲取的,網(wǎng)絡(luò)獲取后,圖片A就存儲到本地緩存,就這還會緩存到內(nèi)存緩存。
緩存主要利用的一個機(jī)制是Lru,(Least Recently Used)最近最少使用的。
而Lru和只要是利用兩個類,LruCache 和 DiskLruCache。
LruCache主要針對的是 內(nèi)存緩存 (緩存)
DiskLruCache 主要針對的是 存儲緩存 (本地)
第二篇鏈接
安卓OOM和Bitmap圖片二級緩存機(jī)制(二)
本篇完。
本篇相關(guān)參考:
Android Out Of Memory(OOM) 的詳細(xì)研究
Android開發(fā)藝術(shù)探索