Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android的oom詳解

Android的oom詳解

編輯:關於Android編程

oom的定義

OOM(out of memory)即內存洩露。一個程序中,已經不需要使用某個對象,但是因為仍然有引用指向它垃圾回收器就無法回收它,當該對象占用的內存無法被回收時,就容易造成內存洩露。

Android的oom原因

1.資源對象沒關閉造成的內存洩露,try catch finally中將資源回收放到finally語句可以有效避免OOM。資源性對象比如:

1-1,Cursor

1-2,調用registerReceiver後未調用unregisterReceiver()

1-3,未關閉InputStream/OutputStream

1-4,Bitmap使用後未調用recycle()

2.作用域不一樣,導致對象不能被垃圾回收器回收,比如:

2-1,非靜態內部類會隱式地持有外部類的引用,

2-2,Context洩露

概括一下,避免Context相關的內存洩露,記住以下事情:

1、 不要保留對Context-Activity長時間的引用(對Activity的引用的時候,必須確保擁有和Activity一樣的生命周期)

2、嘗試使用Context-Application來替代Context-Activity 3、如果你不想控制內部類的生命周期,應避免在Activity中使用非靜態的內部類,而應該使用靜態的內部類,並在其中創建一個對Activity的弱引用。

這種情況的解決辦法是使用一個靜態的內部類,其中擁有對外部類的WeakReference。

2-3,Thread 引用其他對象也容易出現對象洩露。

2-4,onReceive方法裡執行了太多的操作

3.內存壓力過大

3-1,圖片資源加載過多,超過內存使用空間,例如Bitmap 的使用

3-2,重復創建view,listview應該使用convertview和viewholder

如何避免oom

1.減小對象的內存占用

避免OOM的第一步就是要盡量減少新分配出來的對象占用內存的大小,盡量使用更加輕量的對象。

1)使用更加輕量的數據結構

使用ArrayMap和sparseMap代替hashmap。

具體看我博客

Android優化之ArrayMap

2)避免在Android裡面使用Enum

Android官方培訓課程提到過“Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.”

3)減小Bitmap對象的內存占用。

Bitmap是一個極容易消耗內存的大胖子,減小創建出來的Bitmap的內存占用是很重要的,通常來說有下面2個措施:

inSampleSize:縮放比例,在把圖片載入內存之前,我們需要先計算出一個合適的縮放比例,避免不必要的大圖載入。

decode format:解碼格式,選擇ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差異。

代碼分析

我們在編寫Android程序的時候經常要用到許多圖片,不同圖片總是會有不同的形狀、不同的大小,但在大多數情況下,這些圖片都會大於我們程序所需要的大小。比如說系統圖片庫裡展示的圖片大都是用手機攝像頭拍出來的,這些圖片的分辨率會比我們手機屏幕的分辨率高得多。大家應該知道,我們編寫的應用程序都是有一定內存限制的,程序占用了過高的內存就容易出現OOM(OutOfMemory)異常。我們可以通過下面的代碼看出每個應用程序最高可用內存是多少。

int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

Log.d("TAG", "Max memory is " + maxMemory + "KB");

因此在展示高分辨率圖片的時候,最好先將圖片進行壓縮。壓縮後的圖片大小應該和用來展示它的控件大小相近,在一個很小的ImageView上顯示一張超大的圖片不會帶來任何視覺上的好處,但卻會占用我們相當多寶貴的內存,而且在性能上還可能會帶來負面影響。下面我們就來看一看,如何對一張大圖片進行適當的壓縮,讓它能夠以最佳大小顯示的同時,還能防止OOM的出現。

BitmapFactory類介紹

BitmapFactory這個類提供了多個解析方法(decodeByteArray, decodeFile, decodeResource等)用於創建Bitmap對象,我們應該根據圖片的來源選擇合適的方法。比如SD卡中的圖片可以使用decodeFile方法,網絡上的圖片可以使用decodeStream方法,資源文件中的圖片可以使用decodeResource方法。這些方法會嘗試為已經構建的bitmap分配內存,這時就會很容易導致OOM出現。為此每一種解析方法都提供了一個可選的BitmapFactory.Options參數,將這個參數的inJustDecodeBounds屬性設置為true就可以讓解析方法禁止為bitmap分配內存,返回值也不再是一個Bitmap對象,而是null。雖然Bitmap是null了,但是BitmapFactory.Options的outWidth、outHeight和outMimeType屬性都會被賦值。這個技巧讓我們可以在加載圖片之前就獲取到圖片的長寬值和MIME類型,從而根據情況對圖片進行壓縮。如下代碼所示:

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

options.inJustDecodeBounds = true;

BitmapFactory.decodeResource(getResources(), R.id.myimage, options);

int imageHeight = options.outHeight;

int imageWidth = options.outWidth;

String imageType = options.outMimeType;

為了避免OOM異常,最好在解析每張圖片的時候都先檢查一下圖片的大小,除非你非常信任圖片的來源,保證這些圖片都不會超出你程序的可用內存。

現在圖片的大小已經知道了,我們就可以決定是把整張圖片加載到內存中還是加載一個壓縮版的圖片到內存中。

以下幾個因素是我們需要考慮的:

預估一下加載整張圖片所需占用的內存。

為了加載這一張圖片你所願意提供多少內存。

用於展示這張圖片的控件的實際大小。

當前設備的屏幕尺寸和分辨率。

比如,你的ImageView只有128*96像素的大小,只是為了顯示一張縮略圖,這時候把一張1024*768像素的圖片完全加載到內存中顯然是不值得的。

那我們怎樣才能對圖片進行壓縮呢?

通過設置BitmapFactory.Options中inSampleSize的值就可以實現。比如我們有一張2048*1536像素的圖片,將inSampleSize的值設置為4,就可以把這張圖片壓縮成512*384像素。原本加載這張圖片需要占用13M的內存,壓縮後就只需要占用0.75M了(假設圖片是ARGB_8888類型,即每個像素點占用4個字節)。下面的方法可以根據傳入的寬和高,計算出合適的inSampleSize值:

public static int calculateInSampleSize(BitmapFactory.Options options,

int reqWidth, int reqHeight) {

// 源圖片的高度和寬度

final int height = options.outHeight;

final int width = options.outWidth;

int inSampleSize = 1;

if (height > reqHeight || width > reqWidth) {

// 計算出實際寬高和目標寬高的比率

final int heightRatio = Math.round((float) height / (float) reqHeight);

final int widthRatio = Math.round((float) width / (float) reqWidth);

// 選擇寬和高中最小的比率作為inSampleSize的值,這樣可以保證最終圖片的寬和高

// 一定都會大於等於目標的寬和高。

inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;

}

return inSampleSize;

}

使用這個方法,首先你要將BitmapFactory.Options的inJustDecodeBounds屬性設置為true,解析一次圖片。然後將BitmapFactory.Options連同期望的寬度和高度一起傳遞到到calculateInSampleSize方法中,就可以得到合適的inSampleSize值了。之後再解析一次圖片,使用新獲取到的inSampleSize值,並把inJustDecodeBounds設置為false,就可以得到壓縮後的圖片了。

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,

int reqWidth, int reqHeight) {

// 第一次解析將inJustDecodeBounds設置為true,來獲取圖片大小

final BitmapFactory.Options options = new BitmapFactory.Options();

options.inJustDecodeBounds = true;

BitmapFactory.decodeResource(res, resId, options);

// 調用上面定義的方法計算inSampleSize值

options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

// 使用獲取到的inSampleSize值再次解析圖片

options.inJustDecodeBounds = false;

return BitmapFactory.decodeResource(res, resId, options);

}

下面的代碼非常簡單地將任意一張圖片壓縮成100*100的縮略圖,並在ImageView上展示。

[java] view plain copy

mImageView.setImageBitmap(

decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

2.內存對象的重復利用

大多數對象的復用,最終實施的方案都是利用對象池技術,要麼是在編寫代碼的時候顯式的在程序裡面去創建對象池,然後處理好復用的實現邏輯,要麼就是利用系統框架既有的某些復用特性達到減少對象的重復創建,從而減少內存的分配與回收。

在Android上面最常用的一個緩存算法是LRU(Least Recently Use)

1)復用系統自帶的資源

Android系統本身內置了很多的資源,例如字符串/顏色/圖片/動畫/樣式以及簡單布局等等,這些資源都可以在應用程序中直接引用。這樣做不僅僅可以減少應用程序的自身負重,減小APK的大小,另外還可以一定程度上減少內存的開銷,復用性更好。但是也有必要留意Android系統的版本差異性,對那些不同系統版本上表現存在很大差異,不符合需求的情況,還是需要應用程序自身內置進去。

2)注意在ListView/GridView等出現大量重復子組件的視圖裡面對ConvertView的復用

3)Bitmap對象的復用

在ListView與GridView等顯示大量圖片的控件裡面需要使用LRU的機制來緩存處理好的Bitmap。

利用inBitmap的高級特性提高Android系統在Bitmap分配與釋放執行效率上的提升(3.0以及4.4以後存在一些使用限制上的差異)。

使用inBitmap屬性可以告知Bitmap解碼器去嘗試使用已經存在的內存區域,新解碼的bitmap會嘗試去使用之前那張bitmap在heap中所占據的pixel data內存區域,而不是去問內存重新申請一塊區域來存放bitmap。利用這種特性,即使是上千張的圖片,也只會僅僅只需要占用屏幕所能夠顯示的圖片數量的內存大小。

使用inBitmap需要注意幾個限制條件:

在SDK 11 -> 18之間,重用的bitmap大小必須是一致的,例如給inBitmap賦值的圖片大小為100-100,那麼新申請的bitmap必須也為100-100才能夠被重用。從SDK 19開始,新申請的bitmap大小必須小於或者等於已經賦值過的bitmap大小。

新申請的bitmap與舊的bitmap必須有相同的解碼格式,例如大家都是8888的,如果前面的bitmap是8888,那麼就不能支持4444與565格式的bitmap了。 我們可以創建一個包含多種典型可重用bitmap的對象池,這樣後續的bitmap創建都能夠找到合適的“模板”去進行重用。如下圖所示:

另外提一點:在2.x的系統上,盡管bitmap是分配在native層,但是還是無法避免被計算到OOM的引用計數器裡面。這裡提示一下,不少應用會通過反射BitmapFactory.Options裡面的inNativeAlloc來達到擴大使用內存的目的,但是如果大家都這麼做,對系統整體會造成一定的負面影響,建議謹慎采納。

使用圖片緩存技術

背景

在你應用程序的UI界面加載一張圖片是一件很簡單的事情,但是當你需要在界面上加載一大堆圖片的時候,情況就變得復雜起來。在很多情況下,(比如使用ListView, GridView 或者 ViewPager 這樣的組件),屏幕上顯示的圖片可以通過滑動屏幕等事件不斷地增加,最終導致OOM。

為了保證內存的使用始終維持在一個合理的范圍,通常會把被移除屏幕的圖片進行回收處理。此時垃圾回收器也會認為你不再持有這些圖片的引用,從而對這些圖片進行GC操作。用這種思路來解決問題是非常好的,可是為了能讓程序快速運行,在界面上迅速地加載圖片,你又必須要考慮到某些圖片被回收之後,用戶又將它重新滑入屏幕這種情況。這時重新去加載一遍剛剛加載過的圖片無疑是性能的瓶頸,你需要想辦法去避免這個情況的發生。

這個時候,使用內存緩存技術可以很好的解決這個問題,它可以讓組件快速地重新加載和處理圖片。下面我們就來看一看如何使用內存緩存技術來對圖片進行緩存,從而讓你的應用程序在加載很多圖片的時候可以提高響應速度和流暢性。

方法

內存緩存技術對那些大量占用應用程序寶貴內存的圖片提供了快速訪問的方法。

其中最核心的類是LruCache

(此類在android-support-v4的包中提供) 。這個類非常適合用來緩存圖片,它的主要算法原理是把最近使用的對象用強引用存儲在 LinkedHashMap 中,並且把最近最少使用的對象在緩存值達到預設定值之前從內存中移除。

為了能夠選擇一個合適的緩存大小給LruCache, 有以下多個因素應該放入考慮范圍內

你的設備可以為每個應用程序分配多大的內存?

設備屏幕上一次最多能顯示多少張圖片?

有多少圖片需要進行預加載,因為有可能很快也會顯示在屏幕上?

你的設備的屏幕大小和分辨率分別是多少?

一個超高分辨率的設備(例如 Galaxy Nexus) 比起一個較低分辨率的設備(例如 Nexus S),在持有相同數量圖片的時候,需要更大的緩存空間。

圖片的尺寸和大小,還有每張圖片會占據多少內存空間。

圖片被訪問的頻率有多高?會不會有一些圖片的訪問頻率比其它圖片要高?如果有的話,你也許應該讓一些圖片常駐在內存當中,或者使用多個LruCache 對象來區分不同組的圖片。

你能維持好數量和質量之間的平衡嗎?有些時候,存儲多個低像素的圖片,而在後台去開線程加載高像素的圖片會更加的有效。

並沒有一個指定的緩存大小可以滿足所有的應用程序,這是由你決定的。

你應該去分析程序內存的使用情況,然後制定出一個合適的解決方案。一個太小的緩存空間,有可能造成圖片頻繁地被釋放和重新加載,這並沒有好處。而一個太大的緩存空間,則有可能還是會引起 java.lang.OutOfMemory 的異常。

下面是一個使用 LruCache 來緩存圖片的例子:

private LruCache mMemoryCache;

@Override

protected void onCreate(Bundle savedInstanceState) {

// 獲取到可用內存的最大值,使用內存超出這個值會引起OutOfMemory異常。

// LruCache通過構造函數傳入緩存值,以KB為單位。

int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

// 使用最大可用內存值的1/8作為緩存的大小。

int cacheSize = maxMemory / 8;

mMemoryCache = new LruCache(cacheSize) {

@Override

protected int sizeOf(String key, Bitmap bitmap) {

// 重寫此方法來衡量每張圖片的大小,默認返回圖片數量。

return bitmap.getByteCount() / 1024;

}

};

}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {

if (getBitmapFromMemCache(key) == null) {

mMemoryCache.put(key, bitmap);

}

}

public Bitmap getBitmapFromMemCache(String key) {

return mMemoryCache.get(key);

}

在這個例子當中,使用了系統分配給應用程序的八分之一內存來作為緩存大小。在中高配置的手機當中,這大概會有4兆(32/8)的緩存空間。一個全屏幕的 GridView 使用4張 800x480分辨率的圖片來填充,則大概會占用1.5兆的空間(800*480*4)。因此,這個緩存大小可以存儲2.5頁的圖片。

當向 ImageView 中加載一張圖片時,首先會在 LruCache 的緩存中進行檢查。如果找到了相應的鍵值,則會立刻更新ImageView ,否則開啟一個後台線程來加載這張圖片。

public void loadBitmap(int resId, ImageView imageView) {

final String imageKey = String.valueOf(resId);

final Bitmap bitmap = getBitmapFromMemCache(imageKey);

if (bitmap != null) {

imageView.setImageBitmap(bitmap);

} else {

imageView.setImageResource(R.drawable.image_placeholder);

BitmapWorkerTask task = new BitmapWorkerTask(imageView);

task.execute(resId);

}

}

BitmapWorkerTask 還要把新加載的圖片的鍵值對放到緩存中。

[java] view plain copy

class BitmapWorkerTask extends AsyncTask {

// 在後台加載圖片。

@Override

protected Bitmap doInBackground(Integer... params) {

final Bitmap bitmap = decodeSampledBitmapFromResource(

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

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

return bitmap;

}

}

4)避免在onDraw方法裡面執行對象的創建

類似onDraw等頻繁調用的方法,一定需要注意避免在這裡做創建對象的操作,因為他會迅速增加內存的使用,而且很容易引起頻繁的gc,甚至是內存抖動。

5)StringBuilder

在有些時候,代碼中會需要使用到大量的字符串拼接的操作,這種時候有必要考慮使用StringBuilder來替代頻繁的“+”。

3.避免對象的內存洩露

內存對象的洩漏,會導致一些不再使用的對象無法及時釋放,這樣一方面占用了寶貴的內存空間,很容易導致後續需要分配內存的時候,空閒空間不足而出現OOM。顯然,這還使得每級Generation的內存區域可用空間變小,gc就會更容易被觸發,容易出現內存抖動,從而引起性能問題。

1)注意Activity的洩漏

通常來說,Activity的洩漏是內存洩漏裡面最嚴重的問題,它占用的內存多,影響面廣,我們需要特別注意以下兩種情況導致的Activity洩漏:

內部類引用導致Activity的洩漏

最典型的場景是Handler導致的Activity洩漏,如果Handler中有延遲的任務或者是等待執行的任務隊列過長,都有可能因為Handler繼續執行而導致Activity發生洩漏。此時的引用關系鏈是Looper -> MessageQueue -> Message -> Handler -> Activity。為了解決這個問題,可以在UI退出之前,執行remove Handler消息隊列中的消息與runnable對象。或者是使用Static + WeakReference的方式來達到斷開Handler與Activity之間存在引用關系的目的。

Activity Context被傳遞到其他實例中,這可能導致自身被引用而發生洩漏。

內部類引起的洩漏不僅僅會發生在Activity上,其他任何內部類出現的地方,都需要特別留意!我們可以考慮盡量使用static類型的內部類,同時使用WeakReference的機制來避免因為互相引用而出現的洩露。

2)考慮使用Application Context而不是Activity Context

對於大部分非必須使用Activity Context的情況(Dialog的Context就必須是Activity Context),我們都可以考慮使用Application Context而不是Activity的Context,這樣可以避免不經意的Activity洩露。

3)注意臨時Bitmap對象的及時回收

雖然在大多數情況下,我們會對Bitmap增加緩存機制,但是在某些時候,部分Bitmap是需要及時回收的。例如臨時創建的某個相對比較大的bitmap對象,在經過變換得到新的bitmap對象之後,應該盡快回收原始的bitmap,這樣能夠更快釋放原始bitmap所占用的空間。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved