安卓OOM和Bitmap圖片二級緩存機(jī)制(一)

本文出自 “阿敏其人” 簡書博客,轉(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。

舉個栗子.png

舉個栗子,一條金魚,每次只能吃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));

當(dāng)前模擬器為32M運行內(nèi)存的

注意看下面控制臺的打印信息
加載一張寬高為 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應(yīng)用中OOM問題剖析和解決方案

Android開發(fā)藝術(shù)探索

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容