Out of Memory(內存溢出) 幾乎是每個Android程序員都會遇到的事。在網上也能找到一大堆的解決方案,之前寫過一篇《Android 內存溢出管理與測試》的博文。但感覺寫得不是很好,今天整理一下打算重新寫一篇。
首先什么是OOM?為什么會出現OOM?
Out Of Memory,一般是由於程序編寫者對內存使用不當,如對該釋放的內存資源沒有釋放,導致其一直不能被再次使用而使計算機內存被耗盡的現象。重啟計算機即可,但根本解決辦法還是對代碼進行優化。(摘自百度百科)
那么解決OOM的方法有哪些呢?或者說常見的導致OOM的錯誤有哪些?
1、動態回收內存。
2、為應用分配更多的內存。
3、自定義內存大小。
4、如果是因為圖片引起的OOM,其實就可以從圖片下手。(使圖片體積大小變小)
5、加載圖片時在內存中做處理。(圖片的邊界壓縮)
6、Context泄漏。
7、使用eclipse DDMS中的heap查看內存。
8、構造Adapter時,沒有使用緩存的convertView。
9、資源對象沒關閉造成的內存泄漏。
10、注冊沒取消造成的內存泄漏。
11、集合中對象沒清理造成的內存泄漏。
12、使用緩存技術。
1、動態回收內存算是最簡單的解決方法吧,就是手動的調用System.gc();
例如:bit為Bitmap對象
if (bit != null && !bit.isRecycled()) { bit.recycle(); bit = null; }
System.gc();
bitmap.recycle()方法用於回收該bitmap所占用的內存,用System.gc()調用一下系統的垃圾回收器。
需要注意的是:回收內存要及時,比如說SurfaceView,就應該在onSurfaceDestroyed這個方法中回收。如果Activity使用了bitmap,就可以在onStop或者onDestroy方法中回收等等。
2、為應用分配更多的內存。
在清單文件中的< application >節點下,添加如下代碼:android:largeHeap="true"。
android:largeHeap應用程序的進程是否會用較大的 Dalvik 堆來創建。 這將作用於所有為該應用程序創建的進程,但只對第一個被裝入進程的應用程序生效。 如果通過共享用戶 ID 的方式讓多個應用程序公用一個進程,那么這些應用程序必須全部指定本選項,否則將會導致不可預知的后果。
大部分應用程序不需要用到本屬性,而是應該關注如何減少內存消耗以提高性能。 使用本屬性並不能確保一定會增加可用的內存,因為某些設備可用的內存本來就很有限。
要在運行時查詢可用的內存大小,請使用 getMemoryClass()
或getLargeMemoryClass()
方法。
除上述方法外,還有一個方法。
使用 dalvik.system.VMRuntime類提供的setTargetHeapUtilization方法可以增強程序堆內存的處理效率。
具體的使用如下:
private final static floatTARGET_HEAP_UTILIZATION = 0.75f; //在程序onCreate時就可以調用 VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION); //即可
3、一直感覺自定義的這種方法實在是太暴力了。
強制定義自己軟件的對內存大小,我們使用Dalvik提供的 dalvik.system.VMRuntime類來設置最小堆內存為例:
private final static int CWJ_HEAP_SIZE = 6* 1024* 1024 ; VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE); //設置最小heap內存為6MB大小。當然對於內存吃緊來說還可以通過手動干涉GC去處理
4、這也分為兩個方面:
1、分辨率不變,圖片大小減小。 2、分辨率改變,圖片減小。(用PS都很容易的)
需要注意的是:不要減小得太小而影響了人眼看上去的美感。
5、這里給出一個簡單的操作和一個封裝后的操作,可以對比看看。
簡單的操作:
//壓縮,用於節省BITMAP內存空間--解決BUG的關鍵步驟 BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inSampleSize = 2;//這個的值壓縮的倍數(2的整數倍),數值越小,壓縮率越小,圖片越清晰 //返回原圖解碼之后的bitmap對象 Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.begin_background, opts);
這里的bitmap就是壓縮后得到的圖片。
封裝后的操作:
private Bitmap imgUtis(Resources res, int img, int reqWidth, int reqHeight) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;// 讓解析方法禁止為bitmap分配內存,返回值也不再是一個Bitmap對象,而是null。 BitmapFactory.decodeResource(getResources(), img, options); // 在加載圖片之前就獲取到圖片的長寬值和MIME類型,並返回壓縮的尺寸 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 使用獲取到的inSampleSize值再次解析圖片 options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(getResources(), img, options); } /** * * @param options 操作對象 * @param reqWidth 目標寬 * @param reqHeight 目標高 * @return */ 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; }
這里得到的bitmap是按照規定的尺度比例來進行壓縮的。
6.
private static Drawable sBackground; @Override protected void onCreate(Bundle state) { super.onCreate(state); TextView label = new TextView(this); label.setText("Leaks are bad"); if (sBackground == null) { sBackground = getDrawable(R.drawable.large_bitmap); } label.setBackgroundDrawable(sBackground); setContentView(label); }
7.Android 開發工具eclipse中的DDMS帶有一個內存監測工具Heap,可以檢測一個進程的內存變化,根據這個工具我們大致可以測試某個應用的內存變化。
具體的操作方法如下:
1、打開eclipse,切換到DDMS,並確認Devices視圖、Heap視圖都是打開的。
2、將手機通過USB鏈接至電腦,鏈接時,選擇 “USB調試”模式。
3、鏈接成功后,在DDMS的Devices視圖中將會顯示手機設備的序列號,以及設備中正在運行的部分進程信息。
4、在Devices 中,點擊要監控的程序。
5、點擊Devices視圖界面中最上方一排圖標中的“Update Heap”。
6、點擊Heap視圖。
7、點擊Heap視圖中的“Cause GC”按鈕。
8、到此為止需檢測的進程就可以被監視。
操作如圖所示:
說明:
1、點擊“Cause GC”按鈕相當於向虛擬機請求了一次垃圾回收操作;
2、當內存使用信息第一次顯示以后,無須再不斷的點擊“Cause GC”,Heap視圖界面會定時刷新,在對應用的不斷的操作過程中就可以看到內存使用的變化;
3、內存使用信息的各項參數根據名稱即可知道其意思,在此不再贅述。
Heap視圖中有一個Type叫做data object,即數據對象,也就是我們的程序中大量存在的類類型的對象。在data object一行中有一列是“Total Size”,其值就是當前進程中所有Java數據對象的內存總量,一般情況下,這個值的大小決定了是否會存在內存泄漏。判斷方法如下:
進入某應用,不斷的操作該應用,同時注意觀察data object的Total Size值。
正常情況下Total Size值都會穩定在一個有限的范圍內。
反之如果代碼中存在沒有釋放對象引用的情況,則data object的Total Size值在每次GC后不會有明顯的回落,隨着操作次數的增多Total Size的值會越來越大,直到到達一個上限后導致進程被kill掉。
8.例如:ListView的工作原理簡而言之是針對List中每個item, adapter都會調用一個getView的方法獲得布局視圖。我們一般會Inflate一個新的View,填充數據並返回顯示。當然如果我們的Item很多話(比如上萬個),都會新建一個View嗎?很明顯這樣內存是接受不了的。因此優化就開始了,我們在getView()方法中使用了convertView == null的判斷,這是Android已經給我們提供了Recycler機制了,我們就應該利用此機制,而不是每次都去inflate一個View。除此之外,我們還是從getView中的每一個方法調用去查看,發現其實我們拿到convertView的時候,每次都會根據這個布局去findViewById。因此,應使用一個靜態類,保存xml中的各個子View的引用關系,這樣就不必要每次都去解析xml了,而這個靜態類就是代碼中的ViewText。
其示例代碼:

public class ExamDataAdapter extends BaseAdapter { private List<Exam> exams = null; private LayoutInflater inflater; private int resource;// 綁定的條目界面 public ExamDataAdapter(List<Exam> exam, Context context, int id) { this.resource = id; this.exams = exam; this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } public int getCount() { return exams.size(); } public Object getItem(int position) { return exams.get(position); } public long getItemId(int position) { return position; } public View getView(int position, View convertView, ViewGroup parent) { TextView examCount = null; TextView examName = null; TextView examNumber = null; if (convertView == null) { convertView = inflater.inflate(resource, null); examName = (TextView) convertView.findViewById(R.id.test_list_name); examCount = (TextView) convertView.findViewById(R.id.test_list_count); examNumber = (TextView) convertView.findViewById(R.id.test_list_id); ViewText viewText = new ViewText(); viewText.examName = examName; viewText.examCount = examCount; viewText.examNumber = examNumber; convertView.setTag(viewText); } else { ViewText viewText = (ViewText) convertView.getTag(); examName = viewText.examName; examCount = viewText.examCount; examNumber = viewText.examNumber; } Exam exam = exams.get(position); examName.setText(exam.getExam_Name().trim()); examCount.setText(exam.getExam_Count() + "".trim()); examNumber.setText(exam.getExam_ID() + "".trim()); return convertView; } public final class ViewText { public TextView examCount; public TextView examName; public TextView examNumber; } }
這種優化方式在2009年 Google IO開發者大會中已做說明,屬於ViewHolder類型,其優化結果如圖所示。
9.資源性對象比如(Cursor,File文件等)往往都用了一些緩沖,我們在不使用的時候,應該及時關閉它們,以便它們的緩沖及時回收內存。它們的緩沖不僅存在於java虛擬機內,還存在於java虛擬機外。如果我們僅僅是把它的引用設置為null,而不關閉它們,往往會造成內存泄漏。因為有些資源性對象,比如SQLiteCursor(在析構函數finalize(),如果我們沒有關閉它,它自己會調close()關閉),如果我們沒有關閉它,系統在回收它時也會關閉它,但是這樣的效率太低了。因此對於資源性對象在不使用的時候,應該調用它的close()函數,將其關閉掉,然后才置為null.在我們的程序退出時一定要確保我們的資源性對象已經關閉。
程序中經常會進行查詢數據庫的操作,但是經常會有使用完畢Cursor后沒有關閉的情況。如果我們的查詢結果集比較小,對內存的消耗不容易被發現,只有在常時間大量操作的情況下才會復現內存問題,這樣就會給以后的測試和問題排查帶來困難和風險。
示例代碼:
Cursor cursor = getContentResolver().query(uri...); if (cursor.moveToNext()) { ... ... }
修正示例代碼:
Cursor cursor = null; try { cursor = getContentResolver().query(uri...); if (cursor != null &&cursor.moveToNext()) { ... ... } } finally { if (cursor != null) { try { cursor.close(); } catch (Exception e) { //ignore this } } }
10.一些Android程序可能引用我們的Anroid程序的對象(比如注冊機制)。即使我們的Android程序已經結束了,但是別的引用程序仍然還有對我們的Android程序的某個對象的引用,泄漏的內存依然不能被垃圾回收。調用registerReceiver后未調用unregisterReceiver。
比如:假設我們希望在鎖屏界面(LockScreen)中,監聽系統中的電話服務以獲取一些信息(如信號強度等),則可以在LockScreen中定義一個PhoneStateListener的對象,同時將它注冊到TelephonyManager服務中。對於LockScreen對象,當需要顯示鎖屏界面的時候就會創建一個LockScreen對象,而當鎖屏界面消失的時候LockScreen對象就會被釋放掉。
但是如果在釋放LockScreen對象的時候忘記取消我們之前注冊的PhoneStateListener對象,則會導致LockScreen無法被垃圾回收。如果不斷的使鎖屏界面顯示和消失,則最終會由於大量的LockScreen對象沒有辦法被回收而引起OutOfMemory,使得system_process進程掛掉。
雖然有些系統程序,它本身好像是可以自動取消注冊的(當然不及時),但是我們還是應該在我們的程序中明確的取消注冊,程序結束時應該把所有的注冊都取消掉。
11.我們通常把一些對象的引用加入到了集合中,當我們不需要該對象時,並沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就更嚴重了。
12.(此段內容摘自Android官網)
內存緩存技術對那些大量占用應用程序寶貴內存的圖片提供了快速訪問的方法。其中最核心的類是LruCache (此類在android-support-v4的包中提供) 。這個類非常適合用來緩存圖片,它的主要算法原理是把最近使用的對象用強引用存儲在 LinkedHashMap 中,並且把最近最少使用的對象在緩存值達到預設定值之前從內存中移除。
在過去,我們經常會使用一種非常流行的內存緩存技術的實現,即軟引用或弱引用 (SoftReference or WeakReference)。但是現在已經不再推薦使用這種方式了,因為從 Android 2.3 (API Level 9)開始,垃圾回收器會更傾向於回收持有軟引用或弱引用的對象,這讓軟引用和弱引用變得不再可靠。另外,Android 3.0 (API Level 11)中,圖片的數據會存儲在本地的內存當中,因而無法用一種可預見的方式將其釋放,這就有潛在的風險造成應用程序的內存溢出並崩潰。
為了能夠選擇一個合適的緩存大小給LruCache, 有以下多個因素應該放入考慮范圍內,例如:
- 你的設備可以為每個應用程序分配多大的內存?
- 設備屏幕上一次最多能顯示多少張圖片?有多少圖片需要進行預加載,因為有可能很快也會顯示在屏幕上?
- 你的設備的屏幕大小和分辨率分別是多少?一個超高分辨率的設備(例如 Galaxy Nexus) 比起一個較低分辨率的設備(例如 Nexus S),在持有相同數量圖片的時候,需要更大的緩存空間。
- 圖片的尺寸和大小,還有每張圖片會占據多少內存空間。
- 圖片被訪問的頻率有多高?會不會有一些圖片的訪問頻率比其它圖片要高?如果有的話,你也許應該讓一些圖片常駐在內存當中,或者使用多個LruCache 對象來區分不同組的圖片。
- 你能維持好數量和質量之間的平衡嗎?有些時候,存儲多個低像素的圖片,而在后台去開線程加載高像素的圖片會更加的有效。
並沒有一個指定的緩存大小可以滿足所有的應用程序,這是由你決定的。你應該去分析程序內存的使用情況,然后制定出一個合適的解決方案。一個太小的緩存空間,有可能造成圖片頻繁地被釋放和重新加載,這並沒有好處。而一個太大的緩存空間,則有可能還是會引起 java.lang.OutOfMemory 的異常。
下面是一個使用 LruCache 來緩存圖片的例子:
private LruCache<String, Bitmap> 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<String, Bitmap>(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 還要把新加載的圖片的鍵值對放到緩存中。
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { // 在后台加載圖片。 @Override protected Bitmap doInBackground(Integer... params) { final Bitmap bitmap = decodeSampledBitmapFromResource( getResources(), params[0], 100, 100); addBitmapToMemoryCache(String.valueOf(params[0]), bitmap); return bitmap; } }
大體過程就如上面的例子所示。下面來一個簡單的完整的例子:

public class MainActivity extends Activity { private LruCache<String, Bitmap> mMemoryCache; private ImageView iv; public String str = "http://wenwen.sogou.com/p/20100623/20100623101110-601052657.jpg"; @Override protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); setContentView(R.layout.main); iv = (ImageView) findViewById(R.id.iv); // 獲取到可用內存的最大值,使用內存超出這個值會引起OutOfMemory異常。 // LruCache通過構造函數傳入緩存值,以KB為單位。 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // 使用最大可用內存值的1/8作為緩存的大小。 int cacheSize = maxMemory / 8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { // 重寫此方法來衡量每張圖片的大小,默認返回圖片數量。 return bitmap.getByteCount() / 1024; } }; loadBitmap(str, iv); } public void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemCache(key) == null) { mMemoryCache.put(key, bitmap); } } public Bitmap getBitmapFromMemCache(String key) { return mMemoryCache.get(key); } public void loadBitmap(String url, ImageView imageView) { String imageKey = url; Bitmap bitmap = getBitmapFromMemCache(imageKey); if (bitmap != null) { imageView.setImageBitmap(bitmap); } else { imageView.setImageResource(R.drawable.empty_photo); BitmapWorkerTask1 task = new BitmapWorkerTask1(); task.execute(url); } } /** * 異步下載圖片的任務。 * * @author guolin */ class BitmapWorkerTask1 extends AsyncTask<String, Void, Bitmap> { /** * 圖片的URL地址 */ private String imageUrl; @Override protected Bitmap doInBackground(String... params) { imageUrl = params[0]; // 在后台開始下載圖片 Bitmap bitmap = downloadBitmap(imageUrl); if (bitmap != null) { // 圖片下載完成后緩存到LrcCache中 addBitmapToMemoryCache(imageUrl, bitmap); } return bitmap; } @Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); iv.setImageBitmap(bitmap); } /** * 建立HTTP請求,並獲取Bitmap對象。 * * @param imageUrl * 圖片的URL地址 * @return 解析后的Bitmap對象 */ private Bitmap downloadBitmap(String imageUrl) { Bitmap bitmap = null; HttpURLConnection con = null; try { URL url = new URL(imageUrl); con = (HttpURLConnection) url.openConnection(); con.setConnectTimeout(5 * 1000); con.setReadTimeout(10 * 1000); if (con.getResponseCode() == 200) { bitmap = BitmapFactory.decodeStream(con.getInputStream()); } else { System.out.println("輸入的路徑不存在"); } } catch (Exception e) { e.printStackTrace(); } finally { if (con != null) { con.disconnect(); } } if (bitmap != null) { return bitmap; } return null; } } }
運行結果如下(來張吾女王的圖片,嘿嘿):
PS:如果讀者對第6段中提到的 強引用、軟引用、弱引用、虛引用 不了解,可以查看相關博文: