Android內存泄漏及分析

內存泄漏的定義

大家都知道,Java是有垃圾回收機制的,這使得Java程序員比C++程序員輕鬆了許多,存儲申請了,不用心心念念要加一句釋放,Java虛擬機會派出一些回收線程兢兢業業不定時地回收那些不再被需要的內存空間(注意回收的不是對象本身,而是對象佔據的內存空間)。

什麼叫不再被需要的內存空間

Java沒有指針,全憑引用來和對象進行關聯,通過引用來操作對象。如果一個對象沒有與任何引用關聯,那麼這個對象也就不太可能被使用到了,回收器便是把這些“無任何引用的對象”作爲目標,回收了它們佔據的內存空間

如何分辨爲對象無引用

1.引用計數法
直接計數,簡單高效,Python便是採用該方法。但是如果出現兩個對象相互引用,即使它們都無法被外界訪問到,計數器不爲0它們也始終不會被回收。爲了解決該問題,Java採用的是可達性分析法

2.可達性分析法
這個方法設置了一系列的“GC Roots”對象作爲索引起點,如果一個對象與起點對象之間均無可達路徑,那麼這個不可達的對象就會成爲回收對象。這種方法處理兩個對象相互引用的問題,如果兩個對象均沒有外部引用,會被判斷爲不可達對象進而被回收

兩種方法圖示如下:

這裏寫圖片描述

不幸的是,雖然垃圾回收器會幫我們幹掉大部分無用的內存空間,但是對於還保持着引用,但邏輯上已經不會再用到的對象,垃圾回收器不會回收它們。這些對象積累在內存中,直到程序結束,就是我們所說的“內存泄漏”。

進一步瞭解垃圾回收和引用請移步JVM垃圾回收(深入理解Java虛擬機學習筆記)

內存泄漏的源頭

  • 自身編碼引起:由項目開發人員自身的編碼造成。

  • 第三方代碼引起:第三方非開源的SDK和開源的第三方框架。

  • 系統原因:由 Android 系統自身造成的泄漏,如像WebView 、InputMethodManager等引起的問題,還有某些第三方 ROM 存在的問題。

Android中常見的內存泄漏種類

1. 靜態 Activity

泄漏 activity 最簡單的方法就是在 activity 類中定義一個 static 變量,並且將其指向一個運行中的 activity 實例。如果在 activity 的生命週期結束之前,沒有清除這個引用,那它就會泄漏了。這是因爲 activity(例如 MainActivity) 的類對象是靜態的,一旦加載,就會在 APP 運行時一直常駐內存,因此如果類對象不卸載,其靜態成員就不會被垃圾回收,例如:

void setStaticActivity() {
  activity = this;
}

View saButton = findViewById(R.id.sa_button);
saButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
    setStaticActivity();
    nextActivity();
  }
});

2. 靜態 View

如果我們有一個創建起來非常耗時的 View,在同一個 activity 不同的生命週期中都保持不變呢?所以讓我們爲它實現一個單例模式,就像這段代碼。現在一旦 activity 被銷燬,那我們就應該釋放大部分的內存了,例如:

void setStaticView() {
  view = findViewById(R.id.sv_button);
}

View svButton = findViewById(R.id.sv_button);
svButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
    setStaticView();
    nextActivity();
  }
});

內存泄漏了!因爲一旦 view 被加入到界面中,它就會持有 context 的強引用,也就是我們的 activity。由於我們通過一個靜態成員引用了這個 view,所以我們也就引用了 activity,因此 activity 就發生了泄漏。所以一定不要把加載的 view 賦值給靜態變量,如果你真的需要,那一定要確保在 activity 銷燬之前將其從 view 層級中移除。

3. 內部類

現在讓我們在 activity 內部定義一個類,也就是內部類。這樣做的原因有很多,比如增加封裝性和可讀性。如果我們創建了一個內部類的對象,並且通過靜態變量持有了 activity 的引用,那也會發生 activity 泄漏,例如:

void createInnerClass() {
    class InnerClass {
    }
    inner = new InnerClass();
}

View icButton = findViewById(R.id.ic_button);
icButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createInnerClass();
        nextActivity();
    }
});

不幸的是,內部類能夠引用外部類的成員這一優勢,就是通過持有外部類的引用來實現的,而這正是 activity 泄漏的原因

4.匿名類

類似的,匿名類同樣會持有定義它們的對象的引用。因此如果在 activity 內定義了一個匿名的 AsyncTask 對象,就有可能發生內存泄漏了。如果 activity被銷燬之後 AsyncTask 仍然在執行,那就會組織垃圾回收器回收 activity 對象,進而導致內存泄漏,直到執行結束才能回收 activity嗎,例如:

void startAsyncTask() {
    new AsyncTask<Void, Void, Void>() {
        @Override protected Void doInBackground(Void... params) {
            while(true);
        }
    }.execute();
}

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View aicButton = findViewById(R.id.at_button);
aicButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        startAsyncTask();
        nextActivity();
    }
});

5.Handler

同樣的,定義一個匿名的 Runnable 對象並將其提交到 Handler 上也可能導致 activity 泄漏。Runnable 對象間接地引用了定義它的 activity 對象,而它會被提交到 Handler 的 MessageQueue 中,如果它在 activity 銷燬時還沒有被處理,那就會導致 activity 泄漏了,例如:

void createHandler() {
    new Handler() {
        @Override public void handleMessage(Message message) {
            super.handleMessage(message);
        }
    }.postDelayed(new Runnable() {
        @Override public void run() {
            while(true);
        }
    }, Long.MAX_VALUE >> 1);
}


View hButton = findViewById(R.id.h_button);
hButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createHandler();
        nextActivity();
    }
});

這裏提一下解決方法:可以自己寫一個繼承Handler的靜態子類,如果需要引用Activity請使用軟引用或者虛引用

同樣的,使用 Thread 和 TimerTask 也可能導致 activity 泄漏

6.Sensor Manager

系統服務可以通過 context.getSystemService 獲取,它們負責執行某些後臺任務,或者爲硬件訪問提供接口。如果 context 對象想要在服務內部的事件發生時被通知,那就需要把自己註冊到服務的監聽器中。然而,這會讓服務持有 activity 的引用,如果程序員忘記在 activity 銷燬時取消註冊,那就會導致 activity 泄漏了,例如:

void registerListener() {
       SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
       Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
       sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}

View smButton = findViewById(R.id.sm_button);
smButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        registerListener();
        nextActivity();
    }
});


同樣的,對於使用了BraodcastReceiverContentObserverFile,遊標 CursorStreamBitmap等資源的使用,應該在Activity銷燬時及時關閉或者註銷,否則這些資源將不會被回收,造成內存泄漏。

總結

如上,我們展示了幾種很容易不經意間就泄漏大量內存的情景,真正總結下來其實就三點:

  • 靜態變量引用Activity

  • 內部類或者匿名內部類的生命週期長於外部類

  • 使用資源沒有關閉

請記住,最壞的情況下,你的 APP 可能會由於大量的內存泄漏而內存耗盡,進而閃退,但它並不總是這樣。相反,內存泄漏會消耗大量的內存,但卻不至於內存耗盡,這時,APP 會由於內存不夠分配而頻繁進行垃圾回收。垃圾回收是非常耗時的操作,會導致嚴重的卡頓。在 activity 內部創建對象時,一定要格外小心,並且要經常測試是否存在內存泄漏。

內存泄漏分析

使用Android Monitor + MAT

Android Monitor是Android Studio自帶的內存分析工具。

MAT(Memory Analyzer Tool)是基於Eclipse的內存分析工具

第一步:強制GC,生成Java Heap文件

我們都知道Java有一個非常強大的垃圾回收機制,會幫我回收無引用的對象,這些無引用的對象不在我們內存泄漏分析的範疇,Android Studio有一個Android Monitor幫助我們進行強制GC,獲取Java Heap文件。

這裏寫圖片描述

強制GC:點擊Initate GC(1)按鈕,建議點擊後等待幾秒後再次點擊,嘗試多次,讓GC更加充分。然後點擊Dump Java Heap(2)按鈕,然後等到一段時間,生成有點慢。

生成的Java Heap文件會在新建窗口打開:

這裏寫圖片描述

第二步:分析內存泄漏的Activity

這裏寫圖片描述

點擊Analyzer TasksPerform Analysis(1)按鈕,然後等待幾秒十幾秒不等,即可找出內存泄漏的Activity(2)。

那麼我們就可以知道內存泄漏的Activity,因爲這個例子比較簡單,其實在(3)就已經可以看到問題所在,如果比較複雜的問題Android Studio並不夠直觀,不夠MAT方便,如果Android Studio無法解決我們的問題,就建議使用MAT來分析,所以下一步我們就生成標準的hprof文件,通過MAT來找出泄漏的根源。

第三步:轉換成標準的hprof文件

剛纔生成的Heap文件不是標準的Java Heap,所以MAT無法打開,我們需要轉換成標準的Java Heap文件,這個工具Android Studio就有提供,叫做Captures,右擊選中的hprof,Export to standard .hprof選擇保存的位置,即可生成一個標準的hprof文件:

這裏寫圖片描述

第四步:MAT打開hprof文件

MAT的下載地址

這裏寫圖片描述

MAT的使用方式和eclipse一樣,這裏就不多說了,打開剛纔生成的hprof文件。點擊(1)按鈕打開Histogram。(2)這裏是支持正則表達式,我們直接輸入Activity名稱,點擊enter鍵即可。

當然也可以利用QQL,點擊下圖中標記的QQL圖標,然後輸入select * from instanceof android.app.Activity

這裏寫圖片描述

然後我們搜索到了目標的Activity:

這裏寫圖片描述

右擊搜索出來的類名,選擇Merge Shortest Paths to GC Rootsexclude all phantom/weak/soft etc. references,來到這一步,就可以看到內存泄漏的原因,我們就需要根據內存泄漏的信息集合我們的代碼去分析原因:

這裏寫圖片描述

第五步:根據內存泄漏信息和代碼分析原因

使用Handler案例分析,給出的信息是Thread和android.os.Message,這個Thread和Message配合通常是在Handler使用,結合代碼,所以我猜測是Handler導致內存泄漏問題,查看代碼,直接就在函數中定義了一個final的Handler用來定時任務,在Activity的onDestroy後,這個Handler還在不斷地工作,導致Activity無法正常回收:

// 導致內存泄漏的代碼
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
textView = (TextView) findViewById(R.id.text);
final Handler handler = new Handler();
handler.post(new Runnable() {
 @Override
 public void run() {
   textView.setText(String.valueOf(timer++));
   handler.postDelayed(this, 1000);
  }
 });
}

修改代碼,避免內存泄漏:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
textView = (TextView) findViewById(R.id.text);
handler.post(new Runnable() {
 @Override
 public void run() {
  textView.setText(String.valueOf(timer++));
  if (handler != null) {
   handler.postDelayed(this, 1000);
  }
 }
});
}
private Handler handler = new Handler();
@Override
protected void onDestroy() {
 super.onDestroy();
 // 避免Handler導致內存泄漏
 handler.removeCallbacksAndMessages(null);
 handler = null;
}

重新測試,確保問題已經解決。

以上用例代碼:https://github.com/taoweiji/DemoAndroidMemoryLeak

使用DDMS + MAT

DDMS最早是Eclipse開發Android的ADT插件的一部分,現在Android Studio中也有,打開方式如下:

這裏寫圖片描述

運行程序,然後進入 DDMS管理界面,如下:

這裏寫圖片描述

點擊工具欄上的 這裏寫圖片描述來更新統計信息。

然後點擊右側的 Cause GC 按鈕或工具欄上的這裏寫圖片描述即可查看當前的堆情況,如下:

這裏寫圖片描述

點擊工具欄上的這裏寫圖片描述按鈕,將內存信息保存成文件,再轉換成標準的hprof格式,利用MAT查看。

靜態代碼分析工具 —— Lint

Lint 是 Android Studio 自帶的工具,使用姿勢很簡單 Analyze -> Inspect Code 然後選擇想要掃面的區域即可:

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

這裏只是拋磚引玉的介紹 Lint ,實際上玩法還有很多,大家可以自行拓展學習。除了 Lint 外,還有像 FindBugsCheckstyle 等靜態代碼分析工具也是很不錯的。

嚴苛模式 —— StrictMode

Android官方文檔–StrictMode

StrictMode 是 Android 系統提供的 API ,在開發環境下引入可以更早的暴露發現問題。

以官網的示例代碼爲栗子,一般 StrictMode 只在測試環境下啓用,到了生產環境就會進行關閉,通常我們都會藉助 BuildConfig.DEBUG 來實現:

這裏寫圖片描述

啓用 StrictMode 後,在過濾日誌的地方加上 StrictMode 的過濾 Tag ,如果手機連接着電腦進行開發,定期觀察一下 StrictMode 這個 Tag 下的日誌,一般你看到一大堆紅色告警的 Log,就需要好好排查一下是否跟內存泄漏有關了:

這裏寫圖片描述

LeakCanary

LeakCanary是Square公司出品的內存分析工具。

LeakCanary 和 StrictMode 一樣,需要在項目代碼中集成,不過代碼也非常簡單,如下的官方示例:

In your build.gradle:

 dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.4'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
 }

In your Application class:

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
    // Normal app init code...
  }
}

詳細使用除了官方的README也可以參考:LeakCanary 中文使用說明

我對使用LeakCanary有以下兩點感受:

  • 當內存泄漏發生時,LeakCanary 會彈窗提示並生成對應的堆存儲信息記錄,這讓我們對隱蔽的內存泄漏問題有了更加直觀的感覺,但從實際使用來看,LeakCanary 的每個提示也並非是真正存在內存泄漏問題,要想確定是否存在問題我們還需要藉助 MAT 來進行最後的確定。

  • Android 系統本身就存在一些問題導致應用內存泄漏,LeakCanary 的 AndroidExcludedRefs 類幫助我們處理了不少這類問題。

參考:
1.常見的八種導致 APP 內存泄漏的問題
2.Android內存泄漏的簡單檢查與分析方法
3.Android 內存泄漏總結
4.Android 應用內存泄漏的定位、分析與解決策略
5.利用Android Studio、MAT對Android進行內存泄漏檢測
6.Android Studio +MAT 分析內存泄漏實戰
7.內存分析工具 MAT 的使用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章