OOM

OOM現象:

05:15:04.764: ERROR/dalvikvm-heap(264): 3528000-byte external allocationtoo large for …

05:15:04.764: ERROR/(264): VM won’t let us allocate 3528000 bytes

05:15:04.764: DEBUG/skia(264): — decoder->decode returned false

05:15:04.774: DEBUG/AndroidRuntime(264): Shutting down VM

這幾句的意思是,我們的程序申請需要3528000byte太大了,虛擬機不同意給我們,虛擬機shut down自殺了。這個現象,比較常見在需要用到很多圖片或者要用很大圖片的APP開發中。

OOM(Out Of Memory)錯誤,什麼是OOM?

通俗講就是當我們的APP需要申請一塊內存用來裝圖片的時候,系統覺得我們的APP所使用的內存已經夠多了,不同意給我們的APP更多的內存,即使手機系統裏還有1G空餘的內存,然後系統拋出OOM,程序彈框shut down。

爲什麼有OOM,OOM的必然性!

因爲android系統app的每個進程或者每個虛擬機有個最大內存限制,如果申請的內存資源超過了這個限制,系統就會拋出OOM錯誤。跟整個設備的剩餘內存沒太大關係。比如比較早的android系統一個虛擬機最多16M內存,當一個app啓動後,虛擬機不停的申請內存資源用來裝載圖片,當超過內存上限時就OOM。

Android系統APP內存限制怎麼確定的?

Android的APP內存組成:

APP內存由dalvik內存和 native內存2部分組成,dalvik也就是 java堆,創建的對象就是在這裏分配的,而native是通過 c/c++ 方式申請的內存,Bitmap就是以這種方式分配的(android3.0 以後,系統都默認是通過dalvik分配的,native作爲堆來管理)。這2部分加起來不能超過 android 對單個進程、虛擬機的內存限制。

每個手機的內存限制大小是多少?

ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);

 activityManager.getMemoryClass();

以上方法會返回以M爲單位的數字,不同的系統平臺或設備上的值都不太一樣,比如:HTC G7默認 24M,Galaxy 36M,emulator-2.3 24M,等等。我的moto xt681 是42M。3.0系統的設備默認是48M。

上面取到是虛擬機的最大內存資源。而對於heap堆的大小限制,可以查看/system/build.prop文件。

dalvik.vm.heapstartsize=5m

dalvik.vm.heapgrowthlimit=48m

dalvik.vm.heapsize=256m 

heapsize參數表示單個進程heap可用的最大內存,但如果存在如下參數:

dalvik.vm.heapgrowthlimit=48m表示單個進程heap內存被限定在48m,即程序運行過程中實際只能使用48m內存。

爲什麼android系統設定APP的內存限制?

1,要使開發者內存使用更爲合理。限制每個應用的可用內存上限,可以防止某些應用程序惡意或者無意使用過多的內存,而導致其他應用無法正常運行。Android是有多進程的,如果一個進程(也就是一個應用)耗費過多的內存,其他的應用無法運行了。因爲有了限制,使得開發者必須好好利用有限的資源,優化資源的使用。

2,即使有萬千圖片、千萬數據需要使用到,但是特定時刻需要展示給用戶看的總是有限,因爲設備的屏幕顯示就那麼大,上面可以放的信息就是很有限的。大部分信息都是出於準備顯示狀態,所以沒必要給予太多heap內存。也就是出現OOM現象,絕大部分原因是我們的程序設計上有問題,需要優化。比如可以通過時間換空間,不停的加載要用的圖片,不停的回收不用的圖片,把大圖片解析到適合手機屏幕大小的圖片等。

3,android上的APP使用獨立虛擬機,每開一個應用就會打開至少一個獨立的虛擬機。這樣可以避免虛擬機崩潰導致整個系統崩潰,同時代價就是需要浪費更多內存。這些設計確保了android的穩定性。

不是android有gc會自動回收資源麼,爲什麼還會OOM?

Android不是用gc會自動回收資源麼,爲什麼app的哪些不用的資源不回收呢?

Android的gc會按照特定的算法回收程序不用的內存資源,避免app的內存申請越積越多。但是Gc一般回收的資源是哪些無主的對象內存或者軟引用的資源,或者更軟的引用資源,比如:

Bitmap bt= BitmapFactory.decodeResource(this.getResources(),R.drawable.splash);

使用bt…//此時的圖片資源是強應用,是有主的資源。

bt=null;

此時這個圖片資源就是無主的了,gc心情好的時候就會去回收它。

Bitmap bt= BitmapFactory.decodeResource(this.getResources(),R.drawable.splash);

SoftReference< Bitmap > SoftRef=new SoftReference<Bitmap >(bt); 

bt=null;

其他代碼….。當程序申請很多內存資源時,gc有可能會釋放SoftRef引用的這個圖片內存。

bt=SoftRef.get(); 此時可能得到的是null;需要從新加載圖片。當然這也說明了用軟引用圖片資源的好處,就是gc會自動根據需要釋放資源,一定程度上避免OOM。

TIPS:編程要養成的習慣,不用的對象置null。其實更好是,不用的圖片直接recycle。因爲通過置null,讓gc來回收,有時候還是會來不及。

For Android specific we should use the 'recycle' method ratherthan 'gc', because 'recycle' will free the memory at the same time, but calling'gc' doesn't guaranty to run and free the memory for same time(if it isnot too critical, we should not call gc in our code) and results canvery every time.
One more thing using 'recycle' is faster than the 'gc' and it improves theperformance.

 

怎麼查看APP內存分配情況?

1,通過DDMS中的Heap選項卡監視內存情況:

Heap視圖中部有一個叫做data object,即數據對象,也就是我們的程序中大量存在的類類型的對象。

在data object一行中有一列是“TotalSize”,其值就是當前進程中所有Java數據對象的內存總量。

如果代碼中存在沒有釋放對象引用的情況,則data object的Total Size值在每次GC後不會有明顯的回落,隨着操作次數的增多Total Size的值會越來越大,
  直到到達一個上限後導致進程被kill掉。


 

2,在APP裏可以通過Runtime類的totalMemory() ,freeMemory() 兩個方法獲取VM的一些內存信息,如:

Runtime.getRuntime().freeMemory();

Runtime.getRuntime().totalMemory ();

 

3,adb shell dumpsysmeminfo com.android.demo

 

避免OOM的幾個注意點:

1,適當調整圖像大小,因爲手機屏幕尺寸有限,分配給圖像的顯示區域有限,尤其對於超大圖片,加載自網絡或者sd卡,圖片文件體積達到幾M或者十幾M的:

加載到內存前,先算出該bitmap的大小,然後通過適當調節採樣率使得加載的圖片剛好、或稍大即可在手機屏幕上顯示就滿意了:

BitmapFactory.Options opts= new BitmapFactory.Options(); 

        opts.inJustDecodeBounds = true; 

        BitmapFactory.decodeFile(imageFile,opts);  //此時不加載實際圖片,只獲取到圖片的寬高,大致可以通過寬度*高度*4來估算圖片大小。

        opts.inSampleSize =computeSampleSize(opts, minSideLength, maxNumOfPixels);  // Android提供了一種動態計算的方法computeSampleSize

        opts.inJustDecodeBounds = false; 

        try { 

            return BitmapFactory.decodeFile(imageFile,opts); 

        } catch (OutOfMemoryError err) { 

        } 

2,在ListView或Gallery等控件中一次性加載大量圖片時,只加載屏幕顯示的資源,尚未顯示的不加載,移出屏幕的資源及時釋放,可以採用強引用+軟引用2級緩存方式,提高加載性能。

3,緩存圖像到內存,採用軟引用緩存到內存,而不是在每次使用的時候都從新加載到內存;

4,採用低內存佔用量的編碼方式,比如Bitmap.Config.ARGB_4444比Bitmap.Config.ARGB_8888更省內存;

5,及時回收圖像,如果引用了大量Bitmap對象,而應用又不需要同時顯示所有圖片,可以將暫時用不到的Bitmap對象及時回收掉。對於一些明確知道圖片使用情況的場景可以主動recycle。比如:

App的啓動splash畫面上的圖片資源,使用完就recycle;對於幀動畫,可以加載一張,畫一張,釋放一張。

6,不要在循環中創建過多的本地變量; 慎用static,用static來修飾成員變量時,該變量就屬於該類,而不是該類的實例,它的生命週期是很長的。如果用它來引用一些資源耗費過多的實例,這時就要謹慎對待了。

public class ClassName {  

     private static Context mContext;  

     //省略  

}  

如果將Activity賦值到mContext的話。即使該Activity已經onDestroy,由於仍有對象保存它的引用,因此該Activity依然不會被釋放。

 

7,自定義堆內存分配大小,優化Dalvik虛擬機的堆內存分配;

 

App避免OOM的幾種方式

 1,直接null或recycle。

對於app裏使用的大量圖片,採用方式:使用時加載,不顯示時直接置null或recycle。

這樣處理是個好習慣,基本上可以杜絕OOM。但是缺憾是代碼多了,可能會忘記某些資源recycle。而且有些情況下會出現特定的圖片反覆加載、釋放、再加載等,低效率的事情。

2,簡單通過SoftReference引用方式管理圖片資源

建個SoftReference的hashmap;

使用圖片時先查詢這個hashmap是否有SoftReference,SoftReference裏的圖片是否空;

如果空就加載圖片到SoftReference並加入hashmap。

無需在代碼裏顯式的處理圖片的回收和釋放,gc會自動處理資源的釋放。

這種方式處理起來簡單實用,能一定程度上避免前一種方法反覆加載釋放的低效率。但還不夠優化。

3,強引用+軟引用二級緩

Android示範程序ImageDownloader.java,使用了一個二級緩存機制。就是有一個數據結構中直接持有解碼成功的Bitmap對象引用,同時使用一個二級緩存數據結構保持淘汰的Bitmap對象的SoftReference對象,由於SoftReference對象的特殊性,系統會在需要內存的時候首先將SoftReference對象持有的對象釋放掉,也就是說當VM發現可用內存比較少了需要觸發GC的時候,就會優先將二級緩存中的Bitmap回收,而保有一級緩存中的Bitmap對象用於顯示。

其實這個解決方案最爲關鍵的一點是使用了一個比較合適的數據結構,那就是LinkedHashMap類型來進行一級緩存Bitmap的容器,由於LinkedHashMap的特殊性,我們可以控制其內部存儲對象的個數並且將不再使用的對象從容器中移除,放到SoftReference二級緩存裏,我們可以在一級緩存中一直保存最近被訪問到的Bitmap對象,而已經被訪問過的圖片在LinkedHashMap的容量超過我們預設值時將會把容器中存在時間最長的對象移除,這個時候我們可以將被移除出LinkedHashMap中的對象存放至二級緩存容器中,而二級緩存中對象的管理就交給系統來做了,當系統需要GC時就會首先回收二級緩存容器中的Bitmap對象了。

在獲取圖片對象的時候先從一級緩存容器中查找,如果有對應對象並可用直接返回,如果沒有的話從二級緩存中查找對應的SoftReference對象,判斷SoftReference對象持有的Bitmap是否可用,可用直接返回,否則返回空。如果2級緩存都找不到圖片,就直接加載圖片資源。

privatestaticfinalintHARD_CACHE_CAPACITY = 16;

// Hard cache, with a fixed maximum capacity and a lifeduration

privatestaticfinalHashMap<String, Bitmap>sHardBitmapCache = newLinkedHashMap<String, Bitmap>(HARD_CACHE_CAPACITY, 0.75f, true) {

privatestaticfinallongserialVersionUID = -57738079457331894L;

@Override

protectedbooleanremoveEldestEntry(LinkedHashMap.Entry<String,Bitmap> eldest) {

if(size() > HARD_CACHE_CAPACITY) {

sSoftBitmapCache.put(eldest.getKey(), newSoftReference<Bitmap>(eldest.getValue()));

returntrue;

} else

returnfalse;

}

};

// Soft cache for bitmap kicked out of hard cache

privatefinalstaticConcurrentHashMap<String,SoftReference<Bitmap>> sSoftBitmapCache = newConcurrentHashMap<String,SoftReference<Bitmap>>(HARD_CACHE_CAPACITY);

publicBitmap getBitmap(String id) {

// First try the hard reference cache

synchronized(sHardBitmapCache) {

finalBitmap bitmap = sHardBitmapCache.get(id);

if(bitmap != null) {

// Bitmap found in hard cache

// Move element to first position, so that it is removedlast

sHardBitmapCache.remove(id);

sHardBitmapCache.put(id, bitmap);

returnbitmap;

} else{

// Then try the soft reference cache

SoftReference<Bitmap> bitmapReference = sSoftBitmapCache.get(id);

if(bitmapReference != null) {

finalBitmap bitmap = bitmapReference.get();

if(bitmap != null) {

// Bitmap found in soft cache

returnbitmap;

} else{

// Soft reference has been Garbage Collected

sSoftBitmapCache.remove(id);

}

}

}

}

returnnull;

}

publicvoidputBitmap(Stringid, Bitmap bitmap) {

synchronized(sHardBitmapCache) {

if(sHardBitmapCache != null) {

sHardBitmapCache.put(id, bitmap);

}

}

}

 

4,LruCache +sd的緩存方式

LruCache 類特別合適用來caching bitmaps;

private LruCachemMemoryCache;

@Override

protected voidonCreate(Bundle savedInstanceState) {    ...

    // Get memory class of this device,exceeding this amount will throw an

    // OutOfMemory exception.

    final int memClass = ((ActivityManager)context.getSystemService(

           Context.ACTIVITY_SERVICE)).getMemoryClass(); 

    // Use 1/8th of the available memory forthis memory cache.

    final int cacheSize = 1024 * 1024 *memClass / 8; 

    mMemoryCache = new LruCache(cacheSize) {

        @Override

        protected int sizeOf(String key, Bitmapbitmap) {

            // The cache size will be measuredin bytes rather than number of items.

            return bitmap.getByteCount();

        }

    };

    ...

public void addBitmapToMemoryCache(Stringkey, Bitmap bitmap) {

    if (getBitmapFromMemCache(key) == null) {

        mMemoryCache.put(key, bitmap);

    }

public BitmapgetBitmapFromMemCache(String key) {

    return mMemoryCache.get(key);

}

當加載位圖到ImageView時,LruCache會先被檢查是否存在這張圖片。如果找到有,它會被用來立即更新 ImageView 組件,否則一個後臺線程則被觸發去處理這張圖片。

public void loadBitmap(intresId, ImageView imageView) {

    final String imageKey =String.valueOf(resId); 

    final Bitmap bitmap =getBitmapFromMemCache(imageKey);

    if (bitmap != null) {

        mImageView.setImageBitmap(bitmap);

    } else {        mImageView.setImageResource(R.drawable.image_placeholder); //默認圖片

        BitmapWorkerTask task = newBitmapWorkerTask(mImageView);

        task.execute(resId);

    }

}

上面的程序中 BitmapWorkerTask 也需要做添加到內存Cache中的動作:

class BitmapWorkerTaskextends AsyncTask {

    ...

    // Decode image in background.

    @Override

    protected Bitmap doInBackground(Integer...params) {

        final Bitmap bitmap =decodeSampledBitmapFromResource( 

                getResources(),params[0], 100, 100)); 

        addBitmapToMemoryCache(String.valueOf(params[0]),bitmap); 

        returnbitmap; 

    }

    ...

}

Use a Disk Cache [使用磁盤緩存]

private DiskLruCachemDiskCache; 

private static final intDISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB 

private static finalString DISK_CACHE_SUBDIR = "thumbnails"; 

 @Override 

protected voidonCreate(Bundle savedInstanceState) { 

    ... 

    //Initialize memory cache 

    ... 

    FilecacheDir = getCacheDir(this, DISK_CACHE_SUBDIR); 

    mDiskCache= DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE); 

    ... 

}  

class BitmapWorkerTaskextends AsyncTask { 

    ... 

    //Decode image in background. 

    @Override 

    protectedBitmap doInBackground(Integer... params) { 

        finalString imageKey = String.valueOf(params[0]);  

        //Check disk cache in background thread 

        Bitmapbitmap = getBitmapFromDiskCache(imageKey);  

        if(bitmap == null) { // Not found in disk cache 

            //Process as normal 

            finalBitmap bitmap = decodeSampledBitmapFromResource( 

                    getResources(),params[0], 100, 100)); 

        }  

        //Add final bitmap to caches 

        addBitmapToCache(String.valueOf(imageKey,bitmap);  

        returnbitmap; 

    } 

    ... 

}  

public voidaddBitmapToCache(String key, Bitmap bitmap) { 

    //Add to memory cache as before 

    if(getBitmapFromMemCache(key) == null) { 

        mMemoryCache.put(key,bitmap); 

    }  

    //Also add to disk cache 

    if(!mDiskCache.containsKey(key)) { 

        mDiskCache.put(key,bitmap); 

    } 

}  

public BitmapgetBitmapFromDiskCache(String key) { 

    returnmDiskCache.get(key); 

}  

// Creates a uniquesubdirectory of the designated app cache directory. Tries to use external 

// but if not mounted,falls back on internal storage. 

public static FilegetCacheDir(Context context, String uniqueName) { 

    //Check if media is mounted or storage is built-in, if so, try and use externalcache dir 

    //otherwise use internal cache dir 

    finalString cachePath = Environment.getExternalStorageState() ==Environment.MEDIA_MOUNTED 

            ||!Environment.isExternalStorageRemovable() ? 

                    context.getExternalCacheDir().getPath(): context.getCacheDir().getPath();  

    returnnew File(cachePath + File.separator + uniqueName); 

 

兩種場景下的圖片加載建議:

1,網絡下載大量圖片

比如微博客戶端:

多線程異步網絡下載圖片,小圖直接用LRUcache+softref+sd卡,大圖按需下載;

 

 2,對於需要展現非常多條目信息的listview、gridview等的情況

在adapter的getview函數裏有個convertView參數,告知你是否有課利舊的view對象。如果不使用利舊convertView的話,每次調用getView時每次都會重新創建View,這樣之前的View可能還沒有銷燬,加之不斷的新建View勢必會造成內存泄露。同時利舊convertView時,裏面原有的圖片等資源就會變成無主的了。

推介使用convertView+靜態類ViewHolder。

在這裏,官方給出瞭解釋

提升Adapter的兩種方法

重用緩存convertView傳遞給getView()方法來避免填充不必要的視圖

使用ViewHolder模式來避免沒有必要的調用findViewById():因爲太多的findViewById也會影響性能

ViewHolder類的作用

ViewHolder模式通過getView()方法返回的視圖的標籤(Tag)中存儲一個數據結構,這個數據結構包含了指向我們

要綁定數據的視圖的引用,從而避免每次調用getView()的時候調用findViewById())

 

 

遠遠超過限制的內存分配方式有兩種:

1,是從本機代碼分配內存。使用NDK(本地開發工具包)和JNI,它可能從C級(如的malloc / free或新建/刪除)分配內存,這樣的分配是不計入對24 MB的限制。這是真的,從本機代碼分配內存是爲從Java方便,但它可以被用來存儲在RAM中的數據(即使圖像數據)的一些大金額。

2,使用OpenGL的紋理-紋理內存不計入限制 ,要查看您的應用程序確實分配多少內存可以使用android.os.Debug.getNativeHeapAllocatedSize( ),可以使用上面介紹的兩種技術的Nexus之一,我可以輕鬆地爲一個單一的前臺進程分配300MB - 10倍以上的默認24 MB的限制,從上面來看使用navtive代碼分配內存是不在24MB的限制內的(開放的GL的質地也是使用navtive代碼分配內存的)。

但是,這2個方法有個風險就是,本地堆分配內存超過系統可用內存限制的話,通常都是直接崩潰。

發佈了8 篇原創文章 · 獲贊 17 · 訪問量 22萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章