掌握OOM異常的處理,並可以對應用進行相應的優化
阿新 • • 發佈:2019-02-05
一、記憶體溢位如何產生的
Android的虛擬機器是基於暫存器的Dalvik,它的最大堆大小一般是16M,有的機器為24M。因此我們所能利用的記憶體空間是有限的。如果我們的記憶體佔用超過了一定的水平就會出現OutOfMemory的錯誤。
記憶體溢位的幾點原因總結:
1、資源釋放問題:
程式程式碼的問題,長期保持某些資源(如Context)的引用,造成記憶體洩露,資源得不到釋放
2、物件記憶體過大問題:
儲存了多個耗用記憶體過大的物件(如Bitmap),造成記憶體超出限制
3、static:
static是Java中的一個關鍵字,當用它來修飾成員變數時,那麼該變數就屬於該類,而不是該類的例項。所以用static修飾的變數,它的生命週期是很長的,如果用它來引用一些資源耗費過多的例項(Context的情況最多),這時就要謹慎對待了。
public class ClassName {
private static Context mContext;
//省略
}
以上的程式碼是很危險的,如果將Activity賦值到mContext的話。那麼即使該Activity已經onDestroy,但是由於仍有物件儲存它的引用,因此該Activity依然不會被釋放。
我們舉Android文件中的一個例子。
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);
}
sBackground, 是 一個靜態的變數,但是我們發現,我們並沒有顯式的儲存Contex的引用,但是,當Drawable與View連線之後,Drawable就將View設 置為一個回撥,由於View中是包含Context的引用的,所以,實際上我們依然儲存了Context的引用。這個引用鏈如下:
Drawable->TextView->Context
所以,最終該Context也沒有得到釋放,發生了記憶體洩露。
針對static的解決方案:
第一、應該儘量避免static成員變數引用資源耗費過多的例項,比如Context。
第二、Context儘量使用Application Context,因為Application的Context的生命週期比較長,引用它不會出現記憶體洩露的問題。
第三、使用WeakReference代替強引用。比如可以使用WeakReference<Context> mContextRef;
該部分的詳細內容也可以參考Android文件中Article部分。
4、執行緒導致記憶體溢位:
執行緒產生記憶體洩露的主要原因在於執行緒生命週期的不可控。我們來考慮下面一段程式碼。
public class MyActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
new MyThread().start();
}
private class MyThread extends Thread{
@Override
public void run() {
super.run();
//do somthing
}
}
}
這段程式碼很平常也很簡單,是我們經常使用的形式。我們思考一個問題:假設MyThread的run函式是一個很費時的操作,當我們開啟該執行緒後,將裝置的橫屏變為了豎屏,一 般情況下當螢幕轉換時會重新建立Activity,按照我們的想法,老的Activity應該會被銷燬才對,然而事實上並非如此。
由於我們的執行緒是Activity的內部類,所以MyThread中儲存了Activity的一個引用,當MyThread的run函式沒有結束時,MyThread是不會被銷燬的,因此它所引用的老的Activity也不會被銷燬,因此就出現了記憶體洩露的問題。
有些人喜歡用Android提供的AsyncTask,但事實上AsyncTask的問題更加嚴重,Thread只有在run函式不結束時才出現這種記憶體洩露問題,然而AsyncTask內部的實現機制是運用了ThreadPoolExcutor,該類產生的Thread物件的生命週期是不確定的,是應用程式無法控制的,因此如果AsyncTask作為Activity的內部類,就更容易出現記憶體洩露的問題。
針對這種執行緒導致的記憶體洩露問題的解決方案:
第一、將執行緒的內部類,改為靜態內部類。
第二、線上程內部採用弱引用儲存Context引用。
二、避免記憶體溢位的方案:
1、圖片過大導致記憶體溢位:
模擬器的RAM比較小,由於每張圖片先前是壓縮的情況,放入到Bitmap的時候,大小會變大,導致超出RAM記憶體
★android 中用bitmap 時很容易記憶體溢位,報如下錯誤:Java.lang.OutOfMemoryError : bitmap size exceeds VM budget
解決:
方法1: 主要是加上這段:等比例縮小圖片
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;
1)通過getResource()方法獲取資源:
//解決載入圖片 記憶體溢位的問題
//Options 只儲存圖片尺寸大小,不儲存圖片到記憶體
BitmapFactory.Options opts = new BitmapFactory.Options();
//縮放的比例,縮放是很難按準備的比例進行縮放的,其值表明縮放的倍數,SDK中建議其值是2的指數值,值越大會導致圖片不清晰
opts.inSampleSize = 2;
Bitmap bmp = null;
bmp = BitmapFactory.decodeResource(getResources(), mImageIds[position],opts);
...
//回收
bmp.recycle();
2)通過Uri取圖片資源
private ImageView preview;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;//圖片寬高都為原來的二分之一,即圖片為原來的四分之一
Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri), null, options);
preview.setImageBitmap(bitmap);
以上程式碼可以優化記憶體溢位,但它只是改變圖片大小,並不能徹底解決記憶體溢位。
3)通過路徑獲取圖片資源
private ImageView preview;
private String fileName= "/sdcard/DCIM/Camera/2010-05-14 16.01.44.jpg";
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;//圖片寬高都為原來的二分之一,即圖片為原來的四分之一
Bitmap b = BitmapFactory.decodeFile(fileName, options);
preview.setImageBitmap(b);
filePath.setText(fileName);
方法2:對圖片採用軟引用,及時地進行recyle()操作
SoftReference<Bitmap> bitmap;
bitmap = new SoftReference<Bitmap>(pBitmap);
if(bitmap != null){
if(bitmap.get() != null && !bitmap.get().isRecycled()){
bitmap.get().recycle();
bitmap = null;
}
}
具體見“各種引用的簡單瞭解”中的示例
2、複用listView:
方法:對複雜的listview進行合理設計與編碼:
Adapter中:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if(convertView!=null && convertView instanceof LinearLayout){
holder = (ViewHolder) convertView.getTag();
}else{
convertView = View.inflate(MainActivity.this, R.layout.item, null);
holder = new ViewHolder();
holder.tv = (TextView) convertView.findViewById(R.id.tv);
convertView.setTag(holder);
}
holder.tv.setText("XXXX");
holder.tv.setTextColor(Color.argb(180, position*4, position*5, 255-position*2));
return convertView;
}
class ViewHolder{
private TextView tv;
}
3、介面切換
方法1:單個頁面,橫豎屏切換N次後 OOM
1、看看頁面佈局當中有沒有大的圖片,比如背景圖之類的。
去除xml中相關設定,改在程式中設定背景圖(放在onCreate()方法中):
Drawable bg = getResources().getDrawable(R.drawable.bg);
XXX.setBackgroundDrawable(rlAdDetailone_bg);
在Activity destory時注意,bg.setCallback(null); 防止Activity得不到及時的釋放
2. 跟上面方法相似,直接把xml配置檔案載入成view 再放到一個容器裡
然後直接呼叫 this.setContentView(View view);方法,避免xml的重複載入
方法2: 在頁面切換時儘可能少地重複使用一些程式碼
比如:重複呼叫資料庫,反覆使用某些物件等等......
4、記憶體分配:
方法1:Android堆記憶體也可自己定義大小和優化Dalvik虛擬機器的堆記憶體分配
注意若使用這種方法:project build target 只能選擇 <= 2.2 版本,否則編譯將通不過。 所以不建議用這種方式
private final static int CWJ_HEAP_SIZE= 6*1024*1024;
private final static float TARGET_HEAP_UTILIZATION = 0.75f;
VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE);
VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);
常見的記憶體使用不當的情況
1、查詢資料庫沒有關閉遊標
程式中經常會進行查詢資料庫的操作,但是經常會有使用完畢Cursor後沒有關閉的情況。如果我們的查詢結果集比較小,對記憶體的消耗不容易被發現,只有在常時間大量操作的情況下才會復現記憶體問題,這樣就會給以後的測試和問題排查帶來困難和風險。
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
}
}
}
2、構造Adapter時,沒有使用快取的 convertView
以構造ListView的BaseAdapter為例,在BaseAdapter中提供了方法:
public View getView(int position, View convertView, ViewGroup parent)
來向ListView提供每一個item所需要的view物件。初始時ListView會從BaseAdapter中根據當前的屏幕布局例項化一定數量的 view物件,同時ListView會將這些view物件快取起來。當向上滾動ListView時,原先位於最上面的list item的view物件會被回收,然後被用來構造新出現的最下面的list item。這個構造過程就是由getView()方法完成的,getView()的第二個形參 View convertView就是被快取起來的list item的view物件(初始化時快取中沒有view物件則convertView是null)。
由此可以看出,如果我們不去使用convertView,而是每次都在getView()中重新例項化一個View物件的話,即浪費資源也浪費時間,也會使得記憶體佔用越來越大。ListView回收list item的view物件的過程可以檢視:
public View getView(int position, View convertView, ViewGroup parent) {
View view = null;
if (convertView != null) {
view = convertView;
populate(view, getItem(position));
...
} else {
view = new Xxx(...);
...
}
return view;
}
3、Bitmap物件不在使用時呼叫recycle()釋放記憶體
有時我們會手工的操作Bitmap物件,如果一個Bitmap物件比較佔記憶體,當它不在被使用的時候,可以呼叫Bitmap.recycle()方法回收此物件的畫素所佔用的記憶體,但這不是必須的,視情況而定。可以看一下程式碼中的註釋:
4、釋放物件的引用
當一個生命週期較短的物件A,被一個生命週期較長的物件B保有其引用的情況下,在A的生命週期結束時,要在B中清除掉對A的引用。
示例A:
public class DemoActivity extends Activity {
... ...
private Handler mHandler = ...
private Object obj;
public void operation() {
obj = initObj();
...
[Mark]
mHandler.post(new Runnable() {
public void run() {
useObj(obj);
}
});
}
}
我們有一個成員變數 obj,在operation()中我們希望能夠將處理obj例項的操作post到某個執行緒的MessageQueue中。在以上的程式碼中,即便是 mHandler所在的執行緒使用完了obj所引用的物件,但這個物件仍然不會被垃圾回收掉,因為DemoActivity.obj還保有這個物件的引用。 所以如果在DemoActivity中不再使用這個物件了,可以在[Mark]的位置釋放物件的引用,而程式碼可以修改為:
... ...
public void operation() {
obj = initObj();
...
final Object o = obj;
obj = null;
mHandler.post(new Runnable() {
public void run() {
useObj(o);
}
}
}
... ...
示例B:
假設我們希望在鎖屏介面(LockScreen)中,監聽系統中的電話服務以獲取一些資訊(如訊號強度等),則可以在LockScreen中定義一個 PhoneStateListener的物件,同時將它註冊到TelephonyManager服務中。對於LockScreen物件,當需要顯示鎖屏界 面的時候就會建立一個LockScreen物件,而當鎖屏介面消失的時候LockScreen物件就會被釋放掉。
但是如果在釋放LockScreen物件的時候忘記取消我們之前註冊的PhoneStateListener物件,則會導致LockScreen無法被垃 圾回收。如果不斷的使鎖屏介面顯示和消失,則最終會由於大量的LockScreen物件沒有辦法被回收而引起OutOfMemory,使得 system_process程序掛掉。
5、其他
Android應用程式中最典型的需要注意釋放資源的情況是在Activity的生命週期中,在onPause()、onStop()、 onDestroy()方法中需要適當的釋放資源的情況。由於此情況很基礎,在此不詳細說明,具體可以檢視官方文件對Activity生命週期的介紹,以 明確何時應該釋放哪些資源。
三、Android效能優化的一些方案
1、優化Dalvik虛擬機器的堆記憶體分配
1)首先記憶體方面,可以參考 Android堆記憶體也可自己定義大小和優化Dalvik虛擬機器的堆記憶體分配
對於Android平臺來說,其託管層使用的Dalvik JavaVM從目前的表現來看還有很多地方可以優化處理,比如我們在開發一些大型遊戲或耗資源的應用中可能考慮手動干涉GC處理,使用 dalvik.system.VMRuntime類提供的setTargetHeapUtilization方法可以增強程式堆記憶體的處理效率。當然具體原理我們可以參考開源工程,這裡我們僅說下使用方法:
private final static floatTARGET_HEAP_UTILIZATION = 0.75f;
在程式onCreate時就可以呼叫:
VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);
2)Android堆記憶體也可自己定義大小
對於一些大型Android專案或遊戲來說在演算法處理上沒有問題外,影響效能瓶頸的主要是Android自己記憶體管理機制問題,目前手機廠商對RAM都比較吝嗇,對於軟體的流暢性來說RAM對效能的影響十分敏感。
除了上次Android開發網提到的優化Dalvik虛擬機器的堆記憶體分配外,我們還可以強制定義自己軟體的對記憶體大小,我們使用Dalvik提供的 dalvik.system.VMRuntime類來設定最小堆記憶體為例:
private final static int CWJ_HEAP_SIZE = 6* 1024* 1024 ;
VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE); //設定最小heap記憶體為6MB大小
當然對於記憶體吃緊來說還可以通過手動干涉GC去處理,我們將在下次提到具體應用。
2、基礎型別上,因為Java沒有實際的指標,在敏感運算方面還是要藉助NDK來完成。
Android123提示遊戲開發者,這點比較有意思的是Google 推出NDK可能是幫助遊戲開發人員,比如OpenGL ES的支援有明顯的改觀,原生代碼操作圖形介面是很必要的。
3、圖形物件優化:
這裡要說的是Android上的Bitmap物件銷燬,可以藉助recycle()方法顯示讓GC回收一個Bitmap物件,通常對一個不用的Bitmap可以使用下面的方式,如
if(bitmapObject.isRecycled()==false) //如果沒有回收
bitmapObject.recycle();
4、處理GIF動畫:
目前系統對動畫支援比較弱智對於常規應用的補間過渡效果可以,但是對於遊戲而言一般的美工可能習慣了GIF方式的統一處理
目前Android系統僅能預覽GIF的第一幀,可以藉助J2ME中通過執行緒和自己寫解析器的方式來讀取GIF89格式的資源。
5、對於大多數Android手機沒有過多的物理按鍵可能我們需要想象下了做好手勢識別 GestureDetector 和重力感應來實現操控。通常我們還要考慮誤操作問題的降噪處理。
四、圖片佔用程序的記憶體演算法簡介
android中處理圖片的基礎類是Bitmap,顧名思義,就是點陣圖。佔用記憶體的演算法如下:
圖片的width*height*Config。
如果Config設定為ARGB_8888,那麼上面的Config就是4。一張480*320的圖片佔用的記憶體就是480*320*4 byte。
在預設情況下android程序的記憶體佔用量為16M,因為Bitmap除了java中持有資料外,底層C++的 skia圖形庫還會持有一個SKBitmap物件,因此一般圖片佔用記憶體推薦大小應該不超過8M。這個可以調整,編譯原始碼時可以設定引數。
五、記憶體監測工具 DDMS --> Heap
無論怎麼小心,想完全避免bad code是不可能的,此時就需要一些工具來幫助我們檢查程式碼中是否存在會造成記憶體洩漏的地方。Android tools中的DDMS就帶有一個很不錯的記憶體監測工具Heap(這裡我使用eclipse的ADT外掛,並以真機為例,在模擬器中的情況類似)。
用 Heap監測應用程序使用記憶體情況的步驟如下:
1. 啟動eclipse後,切換到DDMS透檢視,並確認Devices檢視、Heap檢視都是開啟的;
2. 將手機通過USB連結至電腦,連結時需要確認手機是處於“USB除錯”模式,而不是作為“Mass Storage”;
3. 連結成功後,在DDMS的Devices檢視中將會顯示手機裝置的序列號,以及裝置中正在執行的部分程序資訊;
4. 點選選中想要監測的程序,比如system_process程序;
5. 點選選中Devices檢視介面中最上方一排圖示中的“Update Heap”圖示;
6. 點選Heap檢視中的“Cause GC”按鈕;
7. 此時在Heap檢視中就會看到當前選中的程序的記憶體使用量的詳細情況。
說明:
a) 點選“Cause GC”按鈕相當於向虛擬機器請求了一次gc操作;
b) 當記憶體使用資訊第一次顯示以後,無須再不斷的點選“Cause GC”,Heap檢視介面會定時重新整理,在對應用的不斷的操作過程中就可以看到記憶體使用的變化;
c) 記憶體使用資訊的各項引數根據名稱即可知道其意思,在此不再贅述。
如何才能知道我們的程式是否有記憶體洩漏的可能性呢。這裡需要注意一個值:Heap檢視中部有一個Type叫做data object,即資料物件,也就是我們的程式中大量存在的類型別的物件。在data object一行中有一列是“Total Size”,其值就是當前程序中所有Java資料物件的記憶體總量,一般情況下,這個值的大小決定了是否會有記憶體洩漏。可以這樣判斷:
a) 不斷的操作當前應用,同時注意觀察data object的Total Size值;
b) 正常情況下Total Size值都會穩定在一個有限的範圍內,也就是說由於程式中的的程式碼良好,沒有造成物件不被垃圾回收的情況,所以說雖然我們不斷的操作會不斷的生成很多對 象,而在虛擬機器不斷的進行GC的過程中,這些物件都被回收了,記憶體佔用量會會落到一個穩定的水平;
c) 反之如果程式碼中存在沒有釋放物件引用的情況,則data object的Total Size值在每次GC後不會有明顯的回落,隨著操作次數的增多Total Size的值會越來越大,
直到到達一個上限後導致程序被kill掉。
d) 此處已system_process程序為例,在我的測試環境中system_process程序所佔用的記憶體的data object的Total Size正常情況下會穩定在2.2~2.8之間,而當其值超過3.55後進程就會被kill。
總之,使用DDMS的Heap檢視工具可以很方便的確認我們的程式是否存在記憶體洩漏的可能性。