Android內存溢出OOM:常見內存泄漏
在上一篇文章中我們對Android中內存有了一個基本的了解,在本文繼續介紹有關內存溢出的相關點。當內存泄漏超過一定的界限,必然會引起內存溢出,有些內存泄漏在開發中是比較常見的,接下來通過介紹幾種常見額內存泄漏情形,以便在開發過程中采取必要的措施以此防止內存泄漏。
如下是Android開發者在開發中比較常見的幾種內存泄漏,并給出了相對應的防止內存泄漏的解決方式。
單例模式引起的內存泄漏
單例模式可以說在Android開發過程中使用最多的一種設計模式,所以由該模式導致的內存泄漏也是比較常見的。
在這里介紹兩種由單例模式導致的內存泄漏,其實引起內存泄漏的原因都是一樣的。
- 在構造單例模式的getInstance()方法中傳入了一個Context對象,但是Context是某個Activity調用者自己。
- 在單例模式中的成員變量是一個監聽器Listener或者說是一個回調Callback,但是該監聽器監聽器Listener或者回調Callback的實現類是一個Activity或者Fragment。
public class AppManager {
private static volatile AppManager instance;
private Context context;
private Callback callback;
private AppManager(Context context) {
this.context = context;
}
public static AppManager getInstance(Context context) {
if (instance == null) {
synchronized (AppManager.class) {
if (instance == null) {
instance = new AppManager(context);
}
}
}
return instance;
}
public void setCallback(Callback callback) {
this.callback = callback;
}
...
}
上面就是一個典型的會造成內存泄漏的單利模式示例,Context和Callback都是作為單利模式的一個成員變量傳入的。如果僅僅是作為AppManager中某個方法的入參,這種情形是不會導致內存泄漏的,因為方法被調用后,方法中局部變量會被自動釋放。然而作為成員變量或者屬性則不同了,當調用getInstance()方法的時候如果傳入一個Activity或者Fragment,那么AppManager就會持有Activity,其實傳入Context與某個Activity或者Fragment實現了AppManager的Callback一樣。由于單例模式的生命周期跟整個應用的生命周期一樣長,所以上面介紹的兩種情形都會導致AppManager持有Activity或者Fragment引用,當JVM進行垃圾回收的時候,導致Activity或者Fragment無法被GC回收,從而導致內存泄漏。
針對第一種情況,有些開發者可能會說只要Context傳入的是getApplicationContext()就可以因Activity導致的內存泄漏,但是不是所有的情況下都可以將Context與getApplicationContext()相互替換的。
- 數字1:啟動Activity在這些類中是可以的,但是需要創建一個新的task。一般情況不推薦。
- 數字2:在這些類中去layout inflate是合法的,但是會使用系統默認的主題樣式,如果你自定義了某些樣式可能不會被使用。
- 數字3:在receiver為null時允許,在4.2或以上的版本中,用于獲取黏性廣播的當前值。(可以無視)
第二種方式中,除非我們的單例模式就是給應用的Application使用的,否則只要某個Activity或者Fragment實現了該Callback,都會導致該Activity無法被GC回收釋放掉導致內存泄漏,最后導致OOM。
通過上面介紹,我們知道如果作為單例模式AppManager某個方法的入參,當方法使用之后,局部變量會自動釋放,這種自動釋放機制是Java語言本身就具有的。但是成員變量被釋放的前提是AppManager實例對象被垃圾回收器回收后才釋放,由于AppManager的實例對象是靜態變量,所以必須在應用退出之后才會被釋放,Context和Callback作為AppManager的成員變量,是我們在使用過程中自己設置的,如果想要在應用沒有退出之前就讓系統自動釋放是不可能的,所以需要我們在使用后自己手動釋放。在Java中將對象釋放很簡單,只需要賦值為null即可,這樣一旦垃圾回收器執行GC時機會自動回收,所以為了在單例模式中不引起內存泄漏,只需要再添加一個clear()方法,在clear()方法中將Context或者Callback手動賦值為null,然后在Activity或者Fragment的onDestory()方法中調用一下AppManager.getInstance().clear()。
Handler引起的內存泄漏
在開發過程中Handler是最常用的用于處理子線程和主線程數據交互的工具。由于Android采用單線程UI模型,開發者無法在子線程處理UI,再者在Android4.0之后,網絡請求不能放在主線程,只能在子線程請求,在子線程請求到的數據必須返回到主線程才可以用于更新UI,為此我們必須使用Handler工具將數據切換到主線程更新UI。
很多情況下開發者是直接采用下面的方式使用Handler。
private Handler handler=new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//TODO
}
};
這種直接采用內部類的方式處理Handler中的Message是最直接也是最簡單的做法,但是這樣會導致一個問題,非靜態內部類會持有外部類的引用,更多內部類引起的內存泄漏在下面會繼續討論,如果Handler中發送了一個延時消息,這樣會導致Activity無法被釋放掉進而引起內存泄漏。
網上有許多討論Handler引起內存泄漏的示例,被大家普遍接受的就是使用如下方式:
private static class UIHandler extends Handler {
private final WeakReference<Activity> weakReference;
public UIHandler(Activity activity) {
weakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
if (weakReference.get() != null) {
//TODO
}
}
}
上述實現方式首先使用了一個靜態內部類繼承Handler,然后定義一個弱引用WeakReference,在構造方法中將Activity的Context存入弱引用中,這樣在JVM執行GC的時候就會直接回收掉弱引用中持有的Activity。在handleMessage()方法中,我們只需要對Activity進行判空處理一下即可。如果想了解更多有關Handler導致內存泄漏,可以參看這篇文章 Handler造成Activity泄漏,用弱引用真的有用么?
有些開發者可能有些疑問了,Handler作為Android系統剛開始時就使用的工具類,而且很多系統自帶的類中都有Handler的足跡,Google不可能留給開發者這么一個大坑,每次使用Handler的時候還必須時刻想 <著如果使用最簡單直接的內部類方式就會導致內存泄漏 其實之所以handler會引起內存泄漏="" 就是因為jvm在進行回收activity的時候="" 會判斷該activity還有沒有在被使用="" 而這時候如果handler中還有message未處理完成="" 就會導致該activity不能及時被釋放="" 那么如果在activity或者fragment生命周期的ondestroy="" 方法中可以將handler沒有執行完的消息清空="" 這樣activity就不會被引用了="" 垃圾回收器就可以在執行gc的時候回收activity了="" 當然也就不會再發生內存泄漏的事情了="">
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacksAndMessages(null);
}
非靜態內部類引起內存泄漏
非靜態內部類會引起內存泄漏,當然了并不是說所有的都會引起內存泄漏,這里只是指在非靜態內部類中處理比較耗時的操作容易導致內存泄漏,更多的可能涉及到異步操作。下面幾個是比較常使用的用于處理異步任務的類,Handler上面已經介紹了就不再羅列出來了。
new Thread(){
@Override
public void run() {
//TODO
}
}.start();
new Timer().schedule(new TimerTask() {
@Override
public void run() {
//TODO
}
},1000);
private class MyTask extends AsyncTask<String,Void,String>{
@Override
protected String doInBackground(String... strings) {
return null;
}
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
}
}
非靜態內部類之所以容易引起內存泄漏是因為它持會隱式持有外部類的引用,更多內容可以參看 Java”失效”的private修飾符 。當我們在Activity或者Fragment中使用了上面幾個比較耗時的類處理一些業務邏輯時,一旦跳過了當前頁面,有可能異步任務還在執行中,這樣就會導致Activity或者Fragment無法被垃圾回收器及時回收從而導致內存泄漏。
多線程并發在Android開發中是一定會涉及的,由于頻繁的創建和銷毀線程會大大的降低系統效率,再者就是線程本身也會占用內存,所以不建議直接使用Thread來頻繁新建線程,而是使用線程池,線程池適當地調整池中工作線線程的數目,防止消耗過多的內存,也可以防止頻繁的創建和銷毀線程,從而提高系統運行效率。還有一點就是使用線程池可以方便的停止正在運行的線程,當界面用戶已經看不到的時候可以直接調用線程池停止方法及時關閉異步任務。
Timer和AsyncTask使用方式類似,這兩個類都有提供用于取消任務的cancel()方法,當不再使用的時候我們可以直接調用cancel()方法。如果說在界面不可見的時候還想在后臺繼續執行任務,那么這時候就建議直接新建一個單獨的類或者使用靜態內部類,這樣就可以不必隱式持有Activity或者Fragment,當然了具體的情況還是要根據需求確定。
WebView引起的內存泄漏
現在安卓APP大多都用到了WebView+H5混合開發,在Android中的WebView存在很大的兼容性問題,不僅僅是Android系統版本的不同對WebView產生很大的差異,另外不同的廠商出貨的ROM里面WebView也存在著很大的差異。即使WebView不加載任何內容,它本身也會占用很大的內存,在此基礎上啟動多個WebView占用的內存不會有太多變化,WebView的渲染類似一個單例模式View,但又不同于原生View,因為WebView在頁面銷毀之后內存仍然不會有明確減少。如下是兩張截圖,第一張是在Activity之上啟動一個使用WebView的Activity,可以看出內存有明顯的漲幅,第二張是關閉已經開啟的WebView的Activity,內存雖有部分減少,但是跟沒有加載WebView時相比微乎其微。
在網上也有許多討論如何減少WebView導致的內存泄漏,有些建議不要在xml中設置WebView,而應該使用Java代碼動態構建一個WebView,在新建WebView的時候傳入getApplicationContext(),然后通過一個布局文件addView()將WebView添加進視圖中。
layout = findViewById(R.id.layout);
webView = new WebView(getApplicationContext());
layout.addView(webView);
但是這種方式也不是很完美解決WebView內存泄漏的問題,如下是網上的有部分開發者遇到的問題:如果在WebView中打開鏈接或者你打開的頁面帶有flash,獲得你的WebView想彈出一個dialog,都會導致從ApplicationContext到ActivityContext的強制類型轉換錯誤,從而導致應用崩潰。這是因為在加載flash的時候,系統會首先把WebView作為父控件,然后在該控件上繪制flash,它想找一個Activity的Context來繪制他,但是你傳入的是ApplicationContext。這種類型的問題我在開發過程中還沒有遇到過,這應該也是確實存在的問題之一。
上面是介紹的是在構建的時候可以采取的優化錯誤,接下來在頁面銷毀的時候,可以在onDestroy()方法調用部分方法清空WebView的內容。
@Override
protected void onDestroy() {
super.onDestroy();
layout.removeView(webView);
webView.removeAllViews();
webView.destroy();
webView = null;
}
實際上上面的兩幅截圖對比,本身就已經采用的上面的優化方式防止WebView內存泄漏,可是通過截圖也可以發現,采取的措施幾乎沒有起到作用。由于標準的WebView就存在內存泄露的問題,Google上面有專門討論這個問題,更多內容可以參看 WebView causes memory leak – leaks the parent Activity 。所以通常根治這個問題的辦法是為 WebView 開啟另外一個進程,通過AIDL或者Messenger與主進程進行通信,WebView 所在的進程可以根據業務的需要選擇合適的時機進行銷毀,從而達到內存的完整釋放。
現在常用的QQ和微信就是采用的開啟一個獨立進程來處理WebView中的相關業務邏輯的。
圖片引起的內存泄漏
在Android移動端開發中圖片想來都是一個吃內存大戶,Google在Android系統的每一次升級中也都試圖減少由Bitmap導致的內存溢出。如下是Google官方文檔的說明:
On Android 2.3.3 (API level 10) and lower, the backing pixel data for a Bitmap is stored in native memory. It is separate from the Bitmap itself, which is stored in the Dalvik heap. The pixel data in native memory is not released in a predictable manner, potentially causing an application to briefly exceed its memory limits and crash. From Android 3.0 (API level 11) through Android 7.1 (API level 25), the pixel data is stored on the Dalvik heap along with the associated Bitmap. In Android 8.0 (API level 26), and higher, the Bitmap pixel data is stored in the native heap.
在Android2.3.3(API 10)及之前的版本中,Bitmap對象與其像素數據是分開存儲的,Bitmap對象存儲在Dalvik heap中,而Bitmap對象的像素數據則存儲在Native Memory(本地內存)中,并且生命周期不太可控,可能需要用戶自己調用recycle()方法回收,在回收之前必須清楚的確定Bitmap已不再使用了,如果調用了Bitmap對象recycle()之后再將Bitmap繪制出來,就會出現”Canvas: trying to use a recycled bitmap”錯誤。3.0-7.1之間,Bitmap的像素數據和Bitmap對象一起存儲在Dalvik heap中,所以我們不用手動調用recycle()來釋放Bitmap對象,內存的釋放都交給垃圾回收器來做,更多可以參看Google官方給的一個圖片處理Demo android-DisplayingBitmaps 。
在8.0之后的像素內存又重新回到native上去分配,不需要用戶主動回收,8.0之后圖像資源的管理更加優秀,極大降低了OOM。在Android2.3.3以及以前的版本中Bitmap的像素信息也是存儲的Native Memory中,這樣雖然可以增加了對手機本身內存的利用率,可是JVM的垃圾收集器并不能回收Native分配的內存,需要開發人員手動調用recycle()方法回收圖片內存,而且容易出問題。但是在Android 8.0引入的一種輔助自動回收native內存的一種機制NativeAllocationRegistry,它可以輔助回收Java對象所申請的native內存。
已經讀取到內存中Bitmap,如何獲取它們在內存中占用的內存大小呢。
public static int getBitmapSize(Bitmap bitmap) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { //API 19
return bitmap.getAllocationByteCount();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {//API 12
return bitmap.getByteCount();
}
// 在低版本中用一行的字節x高度
return bitmap.getRowBytes() * bitmap.getHeight(); //earlier version
}
通過這個方法可以發現,同樣尺寸的圖片在內存中占用的大小跟圖片本身的大小沒有關系。將相同尺寸的圖片放在同一個屏幕密度的手機上面,即使一張圖片大小是1M,另一張圖片大小是100K,但是在內存中占用的大小仍然是相同。如果一張圖片尺寸是720*1280,大小是61.37kb,如果放在手機分辨率也是720*1280的drawable-xhdpi,那么該圖片占用的內存大小是多小呢?我們使用BitmapFactory的 decodeResource(Resources res, int id) 方法來加載圖片,實際上加載在內存中大小是3600kb大小,讀取的Bitmap的尺寸剛好和原圖尺寸一致也是720*1280,但是內存卻幾乎是原圖片占硬盤大小的60倍。同樣如果將圖片放在drawable-hdpi,圖片內存大小為6401kb,圖片尺寸960*1707,原圖只是現在在內存中圖片寬高的0.75倍。
BitmapFactory在加載一個圖片時占用內存大小跟兩個參數有關系inDensity和inTargetDensity,由于設備的屏幕密度是320dpi,所以inTargetDensity是320,如果將圖片放在drawable-xhdpi,inDensity也是320,但是如果放在drawable-hdpi,此時的inDensity變為了240,由于將一個圖片顯示在某個屏幕密度的設備上就要根據屏幕密度進行縮放,原來的寬高乘以所以系數,這個縮放系數即為inTargetDensity除以inDensity,所以上的輸出值也就說的通了。
除了與放置圖片的的資源目錄如(drawable-hdpi、drawable-xhdpi)有關系,還跟Bitmap的像素格式有關系,在加載圖片時如果不做任何設置默認像素格式是ARGB_8888,這樣每像素占用4Byte,而 RGB565則是 2Byte,除此之外還有ARGB_4444和ALPHA_8,雖然ARGB_4444占用內存只有ARGB8888的一半,不過圖片的失真比較嚴重已經被官方嫌棄。而ALPHA_8只有透明度,沒有顏色值,對于在圖片上設置遮蓋的效果的是有很有用。因此如果設置的圖片不設置透明度,RGB_565是個不錯選擇,既要設置透明度,對圖片質量要求又高的化,那就用 ARGB_8888。
一般在開發中在處理圖片加載使用的是第三方圖片加載框架,只要可以熟練使用,基本上我們不必擔心圖引起的內存泄漏問題。但是如果在某些場景需要自己處理Bitmap,記住使用BitmapFactory的Option,大圖片一定要使用inJustDecodeBounds進行縮放,因為設置inJustDecodeBounds的屬性為true的時候,我們解碼的時候不分配內存但是卻可以知道圖片的寬高。另外在設置inSampleSize時,建議設置的圖片的寬高以不超過原圖2倍寬高為宜。
系統管理類引起的內存泄漏
這里所指的一些類是指通過getSystemService()方法獲取的類,一般情況下建議使用上文所說的getApplicationContext()獲取,因為這些類是系統服務類,但是常用的LayoutInflater除外。因為這些常用的系統服務類都是單利模式的,如果在整個應用的生命周期之內都必須使用的話,使用getApplicationContext()也不必擔心內存溢出。但是某些類是在某個頁面或者某種特殊場景下才會用到,一旦使用過有可能系統并不會主動釋放資源,很有可能導致內存泄漏。比如InputMethodManager在某種類型的手機上回出現內存泄漏,ClipboardManager以及SensorManager在使用之后可能忘記解綁了,都有會導致內存泄漏。
網上有關InputMethodManager引起內存泄漏的討論也挺多的,有些開發者認為可以不必關心這點,因為InputMethodManager對象并不是完全歸前一個Activity持有,只是暫時性的指向了它,InputMethodManager的對象是被整個APP循環的使用。另外,InputMethodManager是通過單例實現的,不會造成內存的疊加,如果使用了leakCancy一直檢測出來可以直接屏蔽掉。但是盡管如此,InputMethodManager確實有內存泄漏的情況,如下給出了解決內存泄漏的代碼,代碼參考自 InputMethodManager內存泄露現象及解決 。
public static void fixInputMethodManagerLeak(Context context) {
if (context == null) {
return;
}
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm == null) {
return;
}
String[] fieldValues = new String[]{"mCurRootView", "mServedView", "mNextServedView"};
Object objGet = null;
Field oomField = null;
for (String fieldValue : fieldValues) {
try {
oomField = InputMethodManager.class.getDeclaredField(fieldValue);
if (oomField == null) {
continue;
}
if (oomField.isAccessible() == false) {
oomField.setAccessible(true);
}
objGet = oomField.get(imm);
View viewGet = (View) objGet;
if (viewGet.getContext() == context) {
oomField.set(imm, null);
}
} catch (Throwable e) {
continue;
}
}
}
如果某些特定頁面使用了ClipboardManager以及SensorManager等系統服務類,一定要在頁面銷毀的時候注釋解綁服務,防止造成不必要的內存泄漏。
@Override
protected void onDestroy() {
super.onDestroy();
clipboardManager.removePrimaryClipChangedListener(clipChangedListener);
sensorManager.unregisterListener(eventListener);
//...
}
動畫引起的內存泄漏
動畫的使用雖然可以提高用戶體驗,可是使用不當也非常容易造成內存溢出。這里主要介紹兩種情況,一種是幀動畫引起內存溢出,某些特效需要多張圖片,并且單張圖片尺寸有比較大,使用系統提供的方法很容易就會導致內存溢出。幀動畫將圖片全部讀出來后按順序設置給ImageView,利用視覺暫留效果實現了動畫。一次拿出這么多圖片,而系統都是以Bitmap位圖形式讀取的。而動畫的播放是按順序來的,大量Bitmap就排好隊等待播放然后釋放。在網上也給出了許多解決方式,出發點基本是開發人員自己寫一個實現圖片順序循環加載的邏輯達到幀動畫同樣的效果。但是個人認為這里應該從設計方面考慮,動效設計必須考慮不同平臺的流暢性可用性,不能影響到產品性能。
另外一種就是頁面在用戶不可見時沒有及時停止動畫導致內存泄漏。由于動畫從開始到結束時需要一定的時間的,但是有可能用戶還沒等到動畫執行結束就已經跳過該頁面,這時候應該及時停止動畫。如果動畫是在View中定義的,在View不可見時注意停止動畫,如果是在Activity中,注意在onDestroy()或者onStop()方法中停止動畫。
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (visibility != VISIBLE) {
animation.cancel();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
animation.cancel();
}
其它
資源未關閉造成的內存泄漏。資源性對象比如Cursor、Stream、MediaRecorder、File文件等往往都用了一些緩沖。這些資源在進行讀寫操作時通常都使用了緩沖,如果及時不關閉,這些緩沖對象就會一直被占用而得不到釋放,以致發生內存泄露。因此在不需要使用它們的時候就及時關閉,以便緩沖能及時得到釋放,從而避免內存泄露。另外需要注意的一點就是,如果數據庫頻繁開啟關閉連接,這樣也很影響性能,而且容易導致StackOverFlowError。
集合中的元素在使用過之后要及時清理。如果一個對象放入到ArrayList、HashMap等集合中,這個集合就會持有該對象的引用。當我們不再需要這個對象時,也并沒有將它從集合中移除,這樣只要集合還在使用(而此對象已經無用了),這個對象就造成了內存泄露。
Protocol buffers是由Google為序列化結構數據而設計的,一種語言無關,平臺無關,具有良好的擴展性。類似XML,卻比XML更加輕量,快速,簡單。如果你需要為你的數據實現序列化與協議化,建議使用nano protobufs。關于更多細節,請參考protobuf readme的”Nano version”章節。
如果應用需要在后臺使用service,除非它被觸發并執行一個任務,否則其他時候Service都應該是停止狀態。當你啟動一個Service,系統會傾向為了保留這個Service而一直保留Service所在的進程。這使得進程的運行代價很高,因為系統沒有辦法把Service所占用的RAM空間騰出來讓給其他組件,另外Service還不能被Paged out。這減少了系統能夠存放到LRU緩存當中的進程數量,它會影響應用之間的切換效率,甚至會導致系統內存使用不穩定,從而無法繼續保持住所有目前正在運行的service。建議使用 IntentService,它會在處理完交代給它的任務之后盡快結束自己。
除此之外,在使用BroadcastReceiver的時候要注意注銷掉廣播,應用內廣播建議使用LocalBroadcastManager。ListView和GridView一定要注意復用convertView并結合ViewHolder。謹慎使用第三方庫以及注解等等,本篇文章就介紹到這里,后續會再另起一篇博文介紹出現內存溢出OOM后如何使用工具檢查內存泄漏。
來自:http://www.sunnyang.com/765.html