1. 程式人生 > >談談android中的記憶體洩漏

談談android中的記憶體洩漏

寫在前面

記憶體洩漏實際上很多時候,對於開發者來說不容易引起重視。因為相對於crash來說,android中一兩個地方發生記憶體洩漏的時候,對於整體沒有特別嚴重的影響。但是我想說的是,當記憶體洩漏多的時候,很容易造成他OOM的,因為android給每個app的分配的記憶體是有限的,而且當發生記憶體洩漏的時候,對於app的體驗也會不好,容易造成卡頓等不好的體驗。

Java記憶體結構

這裡寫圖片描述
上面展示的是Java虛擬機器執行時資料區的模型圖

這裡簡單介紹下

  • 方法區:儲存已載入的類資訊,常量池,靜態變數(靜態變數與app的生命週期同步)
  • 虛擬機器棧:儲存基本型別和物件引用
  • 堆:儲存新建的物件或陣列物件
  • 程式計數器:執行緒執行位元組碼檔案位置的指示器,用於執行緒切換後,再次切換回來能夠準確執行上次執行到的位元組碼檔案位置。
  • 本地方法棧:用於記錄Native方法。

我們主要關注這幾個記憶體區域,new出來的物件,放在Heap堆這個區域,這塊記憶體區由GC(Garbage Collector)負責記憶體的回收。對於Java程式設計師來說,很多時候我們不需要關心記憶體的分配與回收問題,但是這並不表示Java沒有記憶體洩漏,Java的記憶體洩漏顯得更為隱蔽,於是這不僅需要我們在開發時避免寫出容易造成記憶體洩漏的問題程式碼,還需要我們在出現記憶體洩漏時掌握相應的技巧去定位和發現問題。

Java 物件在記憶體中的三種狀態

物件的三種狀態

  1. 可達狀態
    當一個物件被建立後,有一個以上的引用變數引用它。在有向圖中可以從起始頂點導航到該物件,那它就處於可達狀態,程式可通過引用變數來呼叫該物件的屬性和方法。
  2. 可恢復狀態
    如果程式中某個物件不再有任何引用變數引用它,它將先進入可恢復狀態,此時從有向圖的起始頂點不能導航到該物件,在這個狀態下,系統的垃圾回收機制準備回收該物件所佔用的記憶體。在回收該物件之前,系統會呼叫可恢復狀態的物件的finalize方法進行資源清理,如果系統在呼叫finalize方法重新讓一個以上的引用變數引用該物件,則這個物件會再次變為可達狀態;否則,該物件進入不可達狀態。
  3. 不可達狀態
    當物件的所有關聯被切斷,且系統呼叫所有物件的finalize方法依然沒有使得該物件變為可達狀態,則這個物件將永久性地失去引用,最後變為不可達狀態。只有當一個物件處於不可達狀態時,系統才會真正回收該物件所佔有的資源。
    GC Root

    左邊的object1,object2,object3和object4是仍然存活的物件,或者說可達狀態的物件,而右邊的object5,object6和object7則是判定可回收的狀態,或者說不可達狀態的物件

這裡說到的GC Roots,一般作為GC Roots的物件有下面幾個(書本上的)可達性演算法的根節點(簡單來說,這些物件一般不會被GC 回收):
a 虛擬機器棧(棧楨中的本地變量表)中的引用的物件

b.方法區中的類靜態屬性引用的物件

c.方法區中的常量引用的物件

d.本地方法棧中JNI的引用的物件

為什麼會有記憶體洩漏

上面所說的不可達狀態的判斷,在Java中是由 可達性分析演算法 來實現的。其基本思路就是通過一系列的稱為“GC Roots”的物件作為起點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連時,則證明該物件是不可達的。
在android中尤為明顯,因為android中很多物件都是有生命週期的。當它們的任務完成之後,它們將被垃圾回收。如果在物件的生命週期本該結束的時候,這個物件還被一系列的引用,這就會導致記憶體洩漏。

簡單來說,沒有被GC ROOTS間接或直接引用的物件的記憶體會被回收。

那麼為什麼會有記憶體洩漏呢
來看一段Java程式碼:

List list = new ArrayList();
for (int i = 1; i < 100; i++) {
    Object object = new Object();
    list.add(object);
    object = null;   
}

針對上面的程式碼我大致畫了下簡圖,我們將object置為null,object變為無用物件,但是GC這時並不能回收object的記憶體,因為list仍然在引用object物件,故這就會導致記憶體洩漏。
image

我們迴圈申請Object物件,並將所申請的物件放入一個 ArrayList 中,如果我們僅僅釋放引用本身,那麼 ArrayList 仍然引用該物件,所以這個物件對 GC 來說是不可回收的。因此,如果物件加入到ArrayList後,還必須從 ArrayList 中刪除。或者我們將ArrayList 物件設定為 null。

其實簡單來說記憶體洩漏就是當物件不再被應用程式使用,但是垃圾回收器卻不能移除它們,因為它們正在被引用。

android中的記憶體洩漏

來到我們的主題,android中的記憶體洩漏大部分是指Activity的記憶體洩漏,因為Activity物件會間接或者直接引用View,Bitmap等,所以一旦無法釋放,會佔用大量記憶體。

android記憶體洩漏的具體場景

錯誤使用Context

public class Singleton {
    private static Singleton instance;

    private Context mContext;

    private Singleton(Context context) {
        this.mContext = context;
    }

    public static Singleton getInstance(Context context) {
        if(instance == null) {
            instance = new Singleton(context);
        }
        return instance;
    }
}

專案中封裝許多工具類,這些工具類有的會被設計成單例模式。

很正常的一個單例模式,可就由於傳入的是一個 Context,而這個 Context 的生命週期的長短就尤為重要了。如果我們傳入的是 Activity 的 Context,當這個 Context 所對應的 Activity 退出的時候,由於該 Context 的引用被單例物件所持有,其生命週期等於整個應用程式的生命週期,所以當前 Activity 退出時它的記憶體並不會回收,最後造成記憶體洩漏。

這裡我們在設計單例的時候儘量把這部分攔截下來,儘量使用ApplicationContext,而不用生命週期短的Activity,容易造成Activity的記憶體洩漏。

Handler使用造成記憶體洩漏

我們在介面上經常會有自動滾動的Banner以及用TextSwitcher實現的自動輪播的公告訊息的功能,這樣的自動滾動通常通常都是通過Handler來不斷的發訊息來實現自動滾動,如下面的程式碼。由於這樣的是一個耗時操作,當前Activity被finish掉的時候,Handler仍然持有外部類Activity的引用,導致Activity不能被GC回收,所以導致記憶體洩露。

import android.content.Intent;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;


public class MainActivity extends AppCompatActivity {


    private static final int TYPE_CHANGE_AD = 1;
    private Handler mHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message message) {
            if(message.what == TYPE_CHANGE_AD) {
                //do something
            }
            return false;
        }
    }) ;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

       mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                mHandler.sendEmptyMessage(TYPE_CHANGE_AD);
            }
        },5000);

    }
}

針對Handler造成的記憶體洩漏,一般有兩種方法來解決:

  • 第一種是在Activity onDestroy的時候,將一些已經在排隊的msg remove掉,通過removeCallbacksAndMessages來把在當前Handler持有的訊息佇列的msg移除掉。
  • 第二種是使用WeakReference來處理。
public class MainActivity extends Activity {

    private static class MyHandler extends Handler {
    private final WeakReference<MainActivity> mActivity;

    public MyHandler(MainActivity activity) {
      mActivity = new WeakReference<MainActivity>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
      SampleActivity activity = mActivity.get();
      if (activity != null) {
        // ...
      }
    }
  }

  private final MyHandler mHandler = new MyHandler(this);
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                mHandler.sendEmptyMessage(TYPE_CHANGE_AD);
            }
        },5000);
    finish();
  }
}

強引用,軟引用,弱引用,虛引用

與Handler有點類似的,我們在Activity中使用Thread(或者TimeTask)的時候去做一些耗時操作的時候,也很容易造成記憶體洩漏。通常我們在Activity的裡面通常都是採用內部類的時候來用的,這種跟Handler機制一樣,因為內部類預設都會持有外部類的引用,當Activity finish的時候,Thread裡面的操作可能還沒結束,這時Activity被Thread所佔用,導致無法回收。針對這種情況,網上建議是採用靜態內部類,因為靜態內部類是不會持有外部類的引用的。

針對這種情況,最好的方式我覺得還是用rxjava來做,能用rxjava來做的儘量用rxjava做。

InputMethodManager造成的記憶體洩漏

使用LeakCanary檢測到的記憶體洩漏,LeakCanary是Square公司是開發的用於檢測記憶體洩漏的開源庫,我們知道Java最常見的記憶體分析工具是MAT,MAT的使用比較簡單,但是用MAT分析記憶體相對比較麻煩。而LeakCanary這個開源庫則不一樣,在簡單的程式碼引入LeakCanary的時候,它就能夠替我們檢測程式中的記憶體洩漏,當出現記憶體洩漏的時候還能以一個非常友好的節目來顯示記憶體洩漏的引用鏈。如下圖所示:

InputMethodManager造成的記憶體洩露

上面展示的是InputMethodManager造成的記憶體洩漏,InputMethodManager造成的記憶體洩漏看起來比較奇怪,畢竟是Google設計的API,查了一些資料,InputMethodManager.mServedView持有一個最後聚焦View的引用,直到另外的一個View聚焦後才會釋放當前的View。當發生GC時mServedView持有的View的引用不會被回收,導致了記憶體洩漏。

針對InputMethodManager造成的記憶體洩漏,LeakCanary這個庫在AndroidExcludedRefs.java頁列舉了很多記憶體洩漏的場景,同時針對InputMethodManager也有提供了相應的解決方法。

在反編譯今日頭條的程式碼發現有下面這段程式碼,發現這段程式碼是通過反射來解決InputMethodManager造成的記憶體洩露,反射這種方式相對比較暴力點,但是針對InputMethodManager造成的記憶體洩漏目前沒發現比較好的方式。

 public static void releaseInputMethodManagerFocus(Activity paramActivity)
  {
    if (paramActivity == null);
    while (true)
    {
      return;
      try
      {
        InputMethodManager localInputMethodManager = (InputMethodManager)paramActivity.getSystemService("input_method");
        if (localInputMethodManager != null)
        {
          Method localMethod = InputMethodManager.class.getMethod("windowDismissed", new Class[] { IBinder.class });
          if (localMethod != null)
            localMethod.invoke(localInputMethodManager, new Object[] { paramActivity.getWindow().getDecorView().getWindowToken() });
          paramActivity = InputMethodManager.class.getDeclaredField("mLastSrvView");
          if (paramActivity != null)
          {
            paramActivity.setAccessible(true);
            paramActivity.set(localInputMethodManager, null);
            return;
          }
        }
      }
      catch (Throwable paramActivity)
      {
        paramActivity.printStackTrace();
      }
    }
  }

使用高德地圖來地位出現的記憶體洩露,由於定位會在多個頁面使用,所以講高德地圖定位抽取成了一個工具類,

下面這個是高德地圖的記憶體洩漏

image

這裡出現記憶體洩漏場景一般是都是我們寫的程式碼不正確導致的。
拿高德地圖舉例:

第一是我們通過AMapLocationClient去開啟地位的時候,不要忘了在Activity Destroy的時候呼叫stopLocation和onDestroy方法。
第二是我們的MapView需要與Activity的生命相繫結,不然也容易造成記憶體洩漏。

與高德地圖這樣的類似就是,SensorManager,EventBus,BroadCast的註冊,在呼叫register方法後不要忘記呼叫unregister方法。

WedView造成的記憶體洩漏

WebView跟MapView有點類似,對於WebView首先要注意的是,Activity onDestroy的時候注意將WebView移除掉:

 @Override
    public void onDestroy() {
        super.onDestroy();
        if (mWebview != null) {
            if (mWebview.getParent() != null) {
                ((ViewGroup) (mWebview.getParent())).removeView(mWebview);
            }
            mWebview.destroy();
            mWebview = null;
        }
    }

但是對於WebView需要載入大量資料的時候,例如需要載入很多圖片的時候,這個時候對於這種場景可以考慮將WebView放進一個垃圾程序,在Activity onDestroy的時候,需要呼叫Process.killProcess(Process.myPid())將當前程序kill掉。

@Override
    public void onDestroy() {
        super.onDestroy();
       Process.killProcess(Process.myPid())
    }
  • 有效增大App的運存,減少由webview引起的記憶體洩露對主程序記憶體的佔用。
  • 避免WebView的Crash影響App主程序的執行。
  • 擁有對WebView獨立程序操控權。

採用這種方式可能需要設計到程序間通訊相關,aidl,messager。

關於Activity記憶體洩漏的場景很多,但是無論是何種案例,都離不開一些表格中集中大的分類。

引用的方式/GC Root Class-(靜態變數) 活著的執行緒 生命週期跟隨app的特殊存在
mContext間接引用 靜態View,InputMethodManager SensorManager、WifiManager(其他Service程序都可以) ViewRootImpl
(this$0間接引用) 內類引用 匿名類/Timer/TimerTask/Handler
資料庫,流等資源未釋放

這裡首先簡單說明下,

  • 第一種:Activity的Context被靜態變數所持有導致記憶體洩漏
class Person {
    static Object obj = new Object();
}

根據jvm的分代的回收機制,Person類的資訊將會被存入方法區永久(Permanent)代。也就是說,Person類、obj引用變數都將存在Permanent裡,這會導致obj物件一直有效,從而使得obj物件不能被回收。
同樣的道理,Activity裡面的靜態變數同樣會造成Activity不能被回收從而導致記憶體洩漏。

  • 第二種:內部類(this$0)造成Activity的洩漏
    內部類是很容易造成記憶體洩漏的,因為內部類都能夠訪問外部類的成員變數,預設內部類都會持有外部類的引用,即this$0這樣的字串,當內部類和外部類的生命週期不一致的時候,在android中最典型就是Handler了。

Message物件有個target欄位,該欄位是Handler型別,引用了當前Handler物件。一句話就是:你通過Handler發往訊息佇列的Message物件持有了Handler物件的引用。假如Message物件一直在訊息佇列中未被處理釋放掉,你的Handler物件就不會被釋放,進而你的Activity也不會被釋放。這種現象很常見,當訊息佇列中含有大量的Message等待處理,你發的Message需要等幾秒才能被處理,而此時你關閉Activity,就會引起記憶體洩露。如果你經常send一些delay的訊息,即使訊息佇列不繁忙,在delay到達之前關閉Activity也會造成記憶體洩露。

  • 第三種 資源沒釋放(資料庫連線,Bitmap,IO流)
3634 3644 E JavaBinder: *** Uncaught remote exception! (Exceptions are not yet supported across processes.)
3634 3644 E JavaBinder: android.database.CursorWindowAllocationException: Cursor window allocation of 2048 kb failed. # Open Cursors=866 (# cursors opened by pid 1565=866)
3634 3644 E JavaBinder: at android.database.CursorWindow.(CursorWindow.java:104)
3634 3644 E JavaBinder: at android.database.AbstractWindowedCursor.clearOrCreateWindow(AbstractWindowedCursor.java:198)
3634 3644 E JavaBinder: at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:147)
3634 3644 E JavaBinder: at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:141)
3634 3644 E JavaBinder: at android.database.CursorToBulkCursorAdaptor.getBulkCursorDescriptor(CursorToBulkCursorAdaptor.java:143)
3634 3644 E JavaBinder: at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:118)
3634 3644 E JavaBinder: at android.os.Binder.execTransact(Binder.java:367)
3634 3644 E JavaBinder: at dalvik.system.NativeStart.run(Native Method)

資料庫的Cursor在使用完要記得close,Bitmap如果是採用createBitmap這種方法建立的在釋放的時候要記得呼叫recycle方法,IO流在使用之後要記得close,這種比較小的地方雖然不是必現問題,但是會佔用資源,這種未釋放的資源多了場景多了很容易造成記憶體洩漏。

寫在最後

  • 內類是非常危險的編碼
  • 在使用Handler,Thead,TimeTask的時候需要需要注意
  • 對於資源一定要記得釋放
  • 對於一些系統API容易造成記憶體洩漏的地方可以重點關注下。

歡迎關注我的公眾號 ,不定期會有優質技術文章推送 。

微信掃一掃下方二維碼即可關注
在這裡插入圖片描述

相關推薦

Android記憶體洩漏的幾種情況

1.單例造成的記憶體洩漏; Android中單例模式中的餓漢式寫法如下: public class Example  { private static Example Instance; private Example(Context context) { this.con

Android記憶體洩漏與OOM避免措施總結

文章部落格地址:http://blog.csdn.net/gjnm820/article/details/51579080 一、關於OOM與記憶體洩露的概念 我們在Android開發過程中經常會遇到OOM的錯誤,這是因為我們在APP中沒有考慮dalvi

Android記憶體洩漏超級精煉詳解

一、前期基礎知識儲備 (1)什麼是記憶體? JAVA是在JVM所虛擬出的記憶體環境中執行的,JVM的記憶體可分為三個區:堆(heap)、棧(stack)和方法區(method)。 棧(stack):是簡單的資料結構,但在計算機中使用廣泛。棧最顯著的特徵是:LIF

每日一問:Android 記憶體洩漏都有哪些注意點?

記憶體洩漏對每一位 Android 開發一定是司空見慣,大家或多或少都肯定有些許接觸。大家都知道,每一個手機都有一定的承載上限,多處的記憶體洩漏堆積一定會堆積如山,最終出現記憶體爆炸 OOM。 而這,也是極有可能在 Android 面試中一道常見的開放題。 記憶體洩漏的根本原因是一個長生命週期的物件持有了一個

談談android記憶體洩漏

寫在前面 記憶體洩漏實際上很多時候,對於開發者來說不容易引起重視。因為相對於crash來說,android中一兩個地方發生記憶體洩漏的時候,對於整體沒有特別嚴重的影響。但是我想說的是,當記憶體洩漏多的時候,很容易造成他OOM的,因為android給每個app的

談談-Android的接口回調技術

賦值 布局 event public 理解 cti schema alt group Android中的接口回調技術有很多應用的場景,最常見的:Activity(人機交互的端口)的UI界面中定義了Button,點擊該Button時,執行某個邏輯。 下面參見上述執行的模

android記憶體洩漏記憶體優化的方法整理

記憶體洩漏 一、單利洩漏 存在記憶體洩露問題的一些程式碼片段像下面這樣:  public class Util {              private Context mContext;  

android基礎--記憶體洩漏

Android(Java)中常見的容易引起記憶體洩漏的不良程式碼: 1. 查詢資料庫沒有關閉遊標               程式中經常會進行查詢資料庫的操作,但是經常會有使用完畢Cursor後沒有關閉的情況。如果我們的查

java記憶體洩漏的理解

JAVA記憶體機制及記憶體洩露   一、Java記憶體管理機制     在C++語言中,如果需要動態分配一塊記憶體,程式設計師需要負責這塊記憶體的整個生命週期。從申請分配、到使用、再到最後的釋放。這樣的過程非常靈活,但是卻十分繁瑣,程式設計師很容易由於疏忽而

Android Studio 3.0)Android Profiler記憶體洩漏檢查

前提概要 記憶體洩漏是常見又重要的問題,針對這個問題谷歌在Android Studio 3.0中推出了Android Profiler。筆者此篇文章主要記錄一下Android Profiler在記憶體洩漏方面的使用。 Android Profiler Android

小心遞迴記憶體洩漏

前段時間由於業務需要,需要從資料庫中查詢出來所有滿足條件的資料,然後匯入到檔案中。於是隨便寫了個程式,查詢出所有滿足條件然後再寫入檔案。但是實際上線後卻發現,程式剛開始執行馬上看到部分資料寫入到檔案,但是後面執行越來越慢,於是對此分析排查了一下。 應用環境 JDK 1.7 + Spring 4.3 + m

利用Android Studio、MAT對Android進行記憶體洩漏檢測

專案進入維護階段時才有時間測試分析app的記憶體問題,這時就要用到測試工具了,可以使用Android Studio、MAT互相結合進行測試, 但是對於複雜的,這兩者很難分析出來,但這兩測試工具也是必須掌握的,感覺網上大多文章講得不怎麼細緻,所以想寫篇文章記錄下,剛好看到本文

Android rxjava記憶體洩漏問題

雖然rxjava很好用, 如果產生過多的訂閱就會造成記憶體洩漏問題, 如何解決呢? @Override protected void onDestroy() { super

檢視Linux & Android記憶體佔用方法

1. procrank (only for Android) 它從/proc/pid/maps中讀取資訊來進行統計。原始碼位於:/system/extras/procrank 記憶體耗用:VSS/RSS/PSS/USS • VSS - Virtual Set Size 虛擬

Android 防止記憶體洩漏的幾個注意點

1)getSystemService的時候,應避免使用activity的context,而是使用application的context2)單例模式的context,應使用context.getApplicationContext來代替,如下:public class AppS

linux記憶體洩漏的檢測(五)記錄記憶體洩漏的程式碼

到目前為止,先後通過wrap malloc、new函式過載和計算指標記憶體大小的方法,基本上滿足了對記憶體洩漏檢測的需要。 如果發現了記憶體洩漏,那麼就要找到記憶體洩漏的地方並且修正它了。 茫茫程式碼,如何去找?如果能根據未釋放的記憶體找到申請它的地方就好了。 我們

android記憶體快取是如何實現的

那就有必要來看看LruCache原始碼了 裡面有一個重要的資料結構LinkedHashMap。具體講解在這裡(http://blog.csdn.net/lxj1137800599/article/details/54974988) 在此總結一下用法: 1.

celery記憶體洩漏問題

CELERYD_MAX_TASKS_PER_CHILD CELERYD_CONCURRENCY = 20# 併發worker數CELERYD_FORCE_EXECV = True# 非常重要,有些情況下

android ion 記憶體洩漏排查

1.檢視各個程序的ION :/sys/kernel/debug/ion/heaps # cat system-heap cat system-heap           client              pid             size ----------

android面試-記憶體洩漏(美圖、久邦面涉及到)

一、Android中會造成記憶體洩露的情景無外乎兩種: 全域性程序(process-global)的static變數。這個無視應用的狀態,持有Activity的強引用的怪物。活在Activity生命