本文出自 “阿敏其人” 簡書博客,轉(zhuǎn)載或引用請注明出處夭坪。
OOM(Out Of Memory)
什么是OOM
手機(jī)系統(tǒng)內(nèi)存份存儲內(nèi)存(ROM)和運(yùn)行內(nèi)存(RAM),我們談?wù)揙OM討論的是運(yùn)行內(nèi)存,這點(diǎn)如果是新人需要明確询枚。≌阄祝現(xiàn)在一般來說手機(jī)運(yùn)行內(nèi)存是2G金蜀,3G基本就算很頂配了,4G運(yùn)行內(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分配的運(yùn)行內(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掛了茅郎,不會影響我其他程序的運(yùn)行蜗元。
比如我的手機(jī)里面有10個app,其中3個在運(yùn)行系冗,那么這個手機(jī)就有3個進(jìn)程在運(yùn)行奕扣,這3個進(jìn)程每一個都分配到了(16m)的運(yùn)行內(nèi)存。
每個App的內(nèi)存怎么分配
我是一個app掌敬,我被啟動了惯豆,我分配到了16m的空間,而且奔害,這16m還不是完完整整給你當(dāng)前程序自己玩?zhèn)€夠的楷兽,有一部分還必須分給native內(nèi)存。
- 那么每一個程序的分配到的運(yùn)行內(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兩部分的分配,有個特點(diǎn)值得說一下轰绵。那就是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一下喷面。
因?yàn)榭們?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)格之類碧浊,并沒有實(shí)際的支出上述情況的根源。
直到昨天在一個老外的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的使用衣洁,這個估計(jì)跟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無法生成的原因凸丸,因?yàn)樵?3M被Java用過后拷邢,剩下C能用的只有3M了。
引用至此結(jié)束
點(diǎn)此查看原文地址
引用這一部分的描述屎慢,就是為了進(jìn)一步證明瞭稼,每個app所占用的16m(比如說16m)運(yùn)行內(nèi)存不是自己可以玩?zhèn)€夠的,還得和另外一個小伙伴分享
另外清楚一點(diǎ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)程)分配的運(yùn)行內(nèi)存
// 測試每個app可用的最大內(nèi)存(安卓每一個app都運(yùn)行在自己獨(dú)立的沙箱里面)
ActivityManager activityManager=(ActivityManager)MainActivity.this.getSystemService(Context.ACTIVITY_SERVICE);
int memoryClass = activityManager.getMemoryClass();// 返回的就是本機(jī)給每個app分配的運(yùn)行內(nèi)存
當(dāng)我們當(dāng)前測模擬器返回的 32M 的運(yùn)行內(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都運(yùn)行在自己獨(dú)立的沙箱里面)
ActivityManager activityManager =(ActivityManager)MainActivity.this.getSystemService(Context.ACTIVITY_SERVICE);
int memoryClass = activityManager.getMemoryClass();// 返回的就是本機(jī)給每個app分配的運(yùn)行內(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方法氨淌,這四類方法都是在安卓底層實(shí)現(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像吻,那么一個像素點(diǎn)占4+4+4+4=16位
Bitmap.Config ARGB_8888:由4個8位組成峻黍,即A=8,R=8拨匆,G=8姆涩,B=8,那么一個像素點(diǎn)占8+8+8+8=32位
Bitmap.Config RGB_565:即R=5惭每,G=6骨饿,B=5,沒有透明度台腥,那么一個像素點(diǎn)占5+6+5=16位
Bitmap.Config ALPHA_8:只有透明度宏赘,沒有顏色,那么一個像素點(diǎn)占8位览爵。
一般情況下我們都是使用的ARGB_8888置鼻,由此可知它是最占內(nèi)存的,因?yàn)橐粋€像素占32位蜓竹,8位=1字節(jié)(byte)箕母,所以一個像素占4字節(jié)的內(nèi)存。假設(shè)有一張480x800的圖片俱济,如果格式為ARGB_8888嘶是,那么將會占用(480x800x32)/(8x1024) = 1500KB的內(nèi)存。
簡單來說蛛碌,我們可以知道聂喇,Bitmap默認(rèn)的ARGB8888是一個質(zhì)量較好參數(shù),畢竟一個像素點(diǎn)有32個比特位(bit),相當(dāng)于4個字節(jié)(Byte)了希太。
夢回唐朝克饶,接著說OOM的例子
有了Bitmap和config的知識之后,我們的OOM的成功與否就看我們圖片的分辨率了
加入說誊辉,圖片分辨率是2500+1000矾湃,那么加載這張圖片所需要的運(yùn)行內(nèi)存我們可以大概這么算:
Bitmap.Config ARGB_8888情況下,一個像素點(diǎn)占32位堕澄,也就是4個字節(jié)邀跃。
2500 * 1000 * 4 得出多少個byte
(2500 * 1000) / 1024 得出kb
(2500 * 1000) / 1024 / 1024 得出m
經(jīng)過運(yùn)算,得出加載所需的運(yùn)行內(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的核心原因:圖片分辨率過大
核心解決辦法:圖片窖式,我們只加載適合的潮售、需要的尺寸H睢!利用BitmapFactory.Options可完成這一項(xiàng)任務(wù)酥诽。
注意:我們要處理的分辨率的問題鞍泉,而不是圖片本身大小的問題,一個100*100的10m的圖片和一張2000*2000的2m的圖片肮帐,對我們來說咖驮,2m的那張對我們來說反而是大圖片,我們針對的是分辨率
通過BitmapFactory.Options通過指定的采樣率來縮小圖片的分辨率,把縮小到合適分辨率的圖片的放到ImageView上面來顯示托修,大大降低了內(nèi)存壓力忘巧,有效避免OOM,至于縮小的怎樣的分辨率才算合適,谷歌有為我們提供了一段代碼诀黍,就可以得出這個合適的度袋坑!這段代碼后面會貼出仗处。
inSimpleSize的比例計(jì)算
計(jì)算采樣率眯勾,主要是通過 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都去挨個計(jì)算取樣值嗎?明顯不可能吁断。
inSimpleSize怎么用俺萌铩?
谷歌為我們提供了一個規(guī)則胯府,很好用介衔,看代碼之前,我們還是文字說一下吧骂因,主要邏輯如下炎咖,分三步走:
- (1) 將 BitmapFactory的 inJustDecodeBounds 參數(shù)設(shè)置為true,當(dāng)設(shè)置為true,代表此時不真正加載圖片乘盼,而是將圖片的原始寬和高數(shù)值讀取出來
- (2) 利用options取出原始圖片的寬高和請求的寬高進(jìn)行比較升熊,計(jì)算出一個合適的inSimpleSize的值
- (3) 將 BitmapFactory的 inJustDecodeBounds 參數(shù)設(shè)置為false,真正開始加載圖片(這時候加載就是經(jīng)過計(jì)算后的分辨率)
** 谷歌提供的方法:**
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)用方法計(jì)算合適的 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)用方法計(jì)算合適的 inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);
// inJustDecodeBounds 置為 false 真正開始加載圖片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd, null, options);
}
// 計(jì)算 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;
// 主要計(jì)算邏輯
// 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
高效加載圖片不報(bào)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ù)探索