Android 性能優化以內存泄漏檢測以及內存優化(中)

  上篇博客咱們寫到了 Java/Android 內存的分配以及相關 GC 的詳細分析,這篇博客咱們會繼續分析 Android 中內存泄漏的檢測以及相關案例,和 Android 的內存優化相關內容。
  上篇:Android 性能優化以內存泄漏檢測以及內存優化(上)
  中篇:Android 性能優化以內存泄漏檢測以及內存優化(中)
  下篇:Android 性能優化以內存泄漏檢測以及內存優化(下)
  轉載請註明出處:blog.csdn.net/self_study/…
  對技術感興趣的同鞋加羣544645972一塊兒交流。javascript

Android 內存泄漏檢測

  經過上篇博客咱們瞭解了 Android JVM/ART 內存的相關知識和泄漏的緣由,再來歸類一下內存泄漏的源頭,這裏咱們簡單將其歸爲一下三類:html

  • 自身編碼引發
  • 由項目開發人員自身的編碼形成;
  • 第三方代碼引發
  • 這裏的第三方代碼包含兩類,第三方非開源的 SDK 和開源的第三方框架;
  • 系統緣由
  • 由 Android 系統自身形成的泄漏,如像 WebView、InputMethodManager 等引發的問題,還有某些第三方 ROM 存在的問題。

Android 內存泄漏的定位,檢測與修復

  內存泄漏不像閃退的 BUG,排查起來相對要困難一些,比較極端的狀況是當你的應用 OOM 才發現存在內存泄漏問題,到了這種狀況纔去排查處理問題的話,對用戶的影響就太大了,爲此咱們應該在編碼階段儘早地發現問題,而不是拖到上線以後去影響用戶體驗,下面總結一下經常使用內存泄漏的定位和檢測工具:java

Lint

  Lint 是 Android studio 自帶的靜態代碼分析工具,使用起來也很方便,選中須要掃描的 module,而後點擊頂部菜單欄 Analyze -> Inspect Code ,選擇須要掃描的地方便可:
android

這裏寫圖片描述
      
這裏寫圖片描述


這裏寫圖片描述

最後在 Performance 裏面有一項是 Handler reference leaks,裏面列出來了可能因爲內部 Handler 對象持有外部 Activity 引用致使內存泄漏的地方,這些地方均可以根據實際的使用場景去排查一下,由於畢竟不是每一個內部 Handler 對象都會致使內存泄漏。Lint 還能夠自定義掃描規則,使用姿式不少很強大,感興趣的能夠去了解一下,除了 Lint 以外,還有像 FindBugs、Checkstyle 等靜態代碼分析工具也是很不錯的。

StrictMode

  StrictMode 是 Android 系統提供的 API,在開發環境下引入能夠更早的暴露發現問題給開發者,於開發階段解決它,StrictMode 最常被使用來檢測在主線程中進行讀寫磁盤或者網絡操做等耗時任務,把這些耗時任務放置於主線程會形成主線程阻塞卡頓甚至可能出現 ANR ,官方例子:git

public void onCreate() {
     if (DEVELOPER_MODE) {
         StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                 .detectDiskReads()
                 .detectDiskWrites()
                 .detectNetwork()   // or .detectAll() for all detectable problems
                 .penaltyLog()
                 .build());
         StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                 .detectLeakedSqlLiteObjects()
                 .detectLeakedClosableObjects()
                 .penaltyLog()
                 .penaltyDeath()
                 .build());
     }
     super.onCreate();
 }複製代碼

把上面這段代碼放在早期初始化的 Application、Activity 或者其餘應用組件的 onCreate 函數裏面來啓用 StrictMode 功能,通常 StrictMode 只是在測試環境下啓用,到了線上環境就不要開啓這個功能。啓用 StrictMode 以後,在 logcat 過濾日誌的地方加上 StrictMode 的過濾 tag,若是發現一堆紅色告警的 log,說明可能就出現了內存泄漏或者其餘的相關問題了:
github

這裏寫圖片描述

好比上面這個就是由於調用 registerReceiver 以後忘記調用 unRegisterReceiver 致使的 activity 泄漏,根據錯誤信息即可以定位和修復問題。

LeakCanary

   LeakCanary 是一個 Android 內存泄漏檢測的神器,正確使用能夠大大減小內存泄漏和 OOM 問題,地址:web

https://github.com/square/leakcanary複製代碼

集成 LeakCanary 也很簡單,在 build.gradle 文件中加入:正則表達式

dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
 }複製代碼

而後在 Application 類中添加下面代碼:shell

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...
  }
}複製代碼

上面兩步作完以後就算是集成了 LeakCanary 了,很是簡單方便,若是程序出現了內存泄漏會彈出 notification,點擊這個 notification 就會進入到下面這個界面,或者集成 LeakCanary 以後在桌面會有一個 LeakCanary 的圖標,點擊進去是全部的內存泄漏列表,點擊其中一項一樣是進入到下面界面:
數據庫

這裏寫圖片描述

這個界面就會詳細展現引用持有鏈,一目瞭然,對於問題的解決方便了不少,堪稱神器,更多實用姿式能夠看看 LeakCanary FAQ
  還有一點須要提到的是,LeakCanary 在檢測內存泄漏的時候會阻塞主界面,這是一點體驗有點不爽的地方,可是這時候阻塞確定是必要的,由於此時必需要掛起線程來獲取當前堆的狀態。而後也並非每一個 LeakCanary 提示的地方都有內存泄漏,這時候可能須要藉助 MAT 等工具去具體分析。不過 LeakCanary 有一點很是好的地方是由於 Android 系統也會有一些內存泄漏,而 LeakCanary 對此則提供了一個 AndroidExcludedRefs 類來幫助咱們排除這些問題。

Android Memory Monitor

  Memory Monitor 是 Android Studio 自帶的一個監控內存使用狀態的工具,入口以下所示:

這裏寫圖片描述

在 Android Monitor 點開以後 logcat 的右側就是 Monitor 工具,其中能夠檢測內存、CPU、網絡等內容,咱們這裏只用到了 Memory Monitor 功能,點擊紅色箭頭所指的區域,就會 dump 此時此刻的 Memory 信息,而且生成一個 .hprof 文件,dump 完成以後會自動打開這個文件的顯示界面,若是沒有打開,能夠經過點擊最左側的 Capture 界面或者 Tool Window 裏面的 Capture 進入 dump 的 .hprof 文件列表:
這裏寫圖片描述

  接着咱們來分析一下這個生成的 .hprof 文件所展現的信息:
這裏寫圖片描述

首先左上角的下拉框,能夠選擇 App Heap、Image Heap 和 Zygote Heap,對應的就是上篇博客講到的 Allocation Space,Image Space 和 Zygote Space,咱們這裏選擇 Allocation Space,而後第二個選擇 PackageTreeView 這一項,展開以後就能看見一個樹形結構了,而後繼續展開咱們應用包名的對應對象,就能夠很清晰的看到有多少個 Activity 對象了,上面那兩欄展現的信息按照從左到右的順序,定義以下所示:

Column Description
Class Name 佔有這塊內存的類名
Total Count 未被處理的數量
Heap Count 在上面選擇的指定 heap 中的數量
Sizeof 這個對象的大小,若是在變化中,就顯示 0
Shallow Size 在當前這個 heap 中的全部該對象的總數
Retained Size 這個類的全部對象佔有的總內存大小
Instance 這個類的指定對象
Reference Tree 指向這個選中對象的引用,還有指向這個引用的引用
Depth 從 GC Root 到該對象的引用鏈路的最短步數
Shallow Size 這個引用的大小
Dominating Size 這個引用佔有的內存大小

而後能夠點擊展開右側的 Analyzer Tasks 項,勾選上須要檢測的任務,而後系統就會給你分析出結果:

這裏寫圖片描述

從分析的結果能夠看到泄漏的 Activity 有兩個,很是直觀,而後點開其中一個,觀察下面的 ReferenceTree 選項:
這裏寫圖片描述

能夠看到 Thread 對象持有了 SecondActivity 對象的引用,也就是 GC Root 持有了該 Activity 的引用,致使這個 Activity 沒法回收,問題的根源咱們就發現了,接下來去處理它就行了。
  關於更多 Android Memory Monitor 的使用能夠去看看這個官方文檔: HPROF Viewer and Analyzer

MAT

  MAT(Memory Analyzer Tools)是一個 Eclipse 插件,它是一個快速、功能豐富的 JAVA heap 分析工具,它能夠幫助咱們查找內存泄漏和減小內存消耗,MAT 插件的下載地址:Eclipse Memory Analyzer Open Source Project,上面經過 Android studio 生成的 .hprof 文件由於格式稍有不一樣,因此須要通過一個簡單的轉換,而後就能夠經過 MAT 去打開了:

這裏寫圖片描述

經過 MAT 去打開轉換以後的這個文件:
這裏寫圖片描述

用的最多的就是 Histogram 功能,點擊 Actions 下的 Histogram 項就能夠獲得 Histogram 結果:
這裏寫圖片描述

咱們能夠在左上角寫入一個正則表達式,而後就能夠對全部的 Class Name 進行篩選了,很方便,頂欄展現的信息 "Objects" 表明該類名對象的數量,剩下的 "Shallow Heap" 和 "Retained Heap" 則和 Android Memory Monitor 相似。我們接着點擊 SecondActivity,而後右鍵:
這裏寫圖片描述

在彈出來的菜單中選擇 List objects->with incoming references 將該類的實例所有列出來:
這裏寫圖片描述

經過這個列表咱們能夠看到 SecondActivity@0x12faa900 這個對象被一個 this$00x12c65140 的匿名內部類對象持有,而後展開這一項,發現這個對象是一個 handler 對象:
這裏寫圖片描述

快速定位找到這個對象沒有被釋放的緣由,能夠右鍵 Path to GC Roots->exclude all phantom/weak/soft etc. references 來顯示出這個對象到 GC Root 的引用鏈,由於強引用纔會致使對象沒法釋放,因此這裏咱們要排除其餘三種引用:
這裏寫圖片描述

這麼處理以後的結果就很明顯了:
這裏寫圖片描述

一個很是明顯的強引用持有鏈,GC Root 咱們前面的博客中說到包含了線程,因此這裏的 Thread 對象 GC Root 持有了 SecondActivity 的引用,致使該 Activity 沒法被釋放。
  MAT 還有一個功能就是可以對比兩個 .hprof 文件,將兩個文件都添加到 Compare Basket 裏面:
這裏寫圖片描述

添加進去以後點擊右上角的 ! 按鈕,而後就會生成兩個文件的對比:
這裏寫圖片描述

一樣適用正則表達式將須要的類篩選出來:
這裏寫圖片描述

結果也很明顯,退出 Activity 以後該 Activity 對象未被回收,仍然在內存中,或者能夠調整對比選項讓對比結果更加明顯:
這裏寫圖片描述

也能夠對比兩個對象集合,方法與此相似,都是將兩個 Dump 結果中的對象集合添加到 Compare Basket 中去對比,找出差別後用 Histogram 查詢的方法找出 GC Root,定位到具體的某個對象上。

adb shell && Memory Usage

  能夠經過命令 adb shell dumpsys meminfo [package name] 來將指定 package name 的內存信息打印出來,這種模式能夠很是直觀地看到 Activity 未釋放致使的內存泄漏:

這裏寫圖片描述

或者也能夠經過 Android studio 的 Memory Usage 功能進行查看,最後的結果是同樣的:
這裏寫圖片描述

Allocation Tracker

  Android studio 還自帶一個 Allocation Tracker 工具,功能和 DDMS 中的基本差很少,這個工具能夠監控一段時間以內的內存分配:

這裏寫圖片描述

在內存圖中點擊途中標紅的部分,啓動追蹤,再次點擊就是中止追蹤,隨後自動生成一個 .alloc 文件,這個文件就記錄了此次追蹤到的全部數據,而後會在右上角打開一個數據面板:
這裏寫圖片描述

這個工具詳細的介紹能夠看看這個博客: Android性能專項測試之Allocation Tracker(Android Studio)

常見的內存泄漏案例

  咱們來看看常見的致使內存泄漏的案例:

靜態變量形成的內存泄漏

  因爲靜態變量的生命週期和應用同樣長,因此若是靜態變量持有 Activity 或者 Activity 中 View 對象的應用,就會致使該靜態變量一直直接或者間接持有 Activity 的引用,致使該 Activity 沒法釋放,從而引起內存泄漏,不過須要注意的是在大多數這種狀況下因爲靜態變量只是持有了一個 Activity 的引用,因此致使的結果只是一個 Activity 對象未能在退出以後釋放,這種問題通常不會致使 OOM 問題,只能經過上面介紹過的幾種工具在開發中去觀察發現。
  這種問題的解決思路很簡單,就是不讓靜態變量直接或者間接持有 Activity 的強引用,能夠將其修改成 soft reference 或者 weak reference 等等之類的,或者若是能夠的話將 Activity Context 更換爲 Application Context,這樣就能保證生命週期一致不會致使內存泄漏的問題了。

內部類持有外部類引用

  咱們上面的 demo 中模擬的就是內部類對象持有外部類對象的引用致使外部類對象沒法釋放的問題,在 Java 中非靜態內部類和匿名內部類會持有他們所屬外部類對象的引用,若是這個非靜態內部類對象或者匿名內部類對象被一個耗時的線程(或者其餘 GC Root)直接或者間接的引用,甚至這些內部類對象自己就在作一些耗時操做,這樣就會致使這個內部類對象直接或者間接沒法釋放,內部類對象沒法釋放,外部類的對象也就沒法釋放形成內存泄漏,並且若是沒法釋放的對象積累起來就會形成 OOM,示例代碼以下所示:

public class SecondActivity extends AppCompatActivity{
    private Handler handler;
    private Bitmap bitmap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.pic);//decode 一個大圖來模擬內存沒法釋放致使的崩潰
        findViewById(R.id.btn_second).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                finish();
            }
        });

        handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);

            }
        };
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                handler.sendEmptyMessage(0);
            }
        }).start();
    }
}複製代碼

  這個問題的解決方法能夠根據實際狀況進行選擇:

  • 將非靜態內部類或者匿名內部類修改成靜態內部類,好比 Handler 修改成靜態內部類,而後讓 Handler 持有外部 Activity 的一個 Weak Reference 或者 Soft Reference;
  • 在 Activity 頁面銷燬的時候將耗時任務中止,這樣就能保證 GC Root 不會間接持有 Activity 的引用,也就不會致使內存泄漏;

錯誤使用 Activity Context

  這個很好理解,在一個錯誤的地方使用 Activity Context,形成 Activity Context 被靜態變量長時間引用致使沒法釋放而引起的內存泄漏,這個問題的處理方式也很簡單,若是能夠的話修改成 Application Context 或者將強引用變成其餘引用。

資源對象沒關閉形成的內存泄漏

  資源性對象好比(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
        }
    }
}複製代碼

集合中對象沒清理形成的內存泄漏

  在實際開發過程當中不免會有把對象添加到集合容器(好比 ArrayList)中的需求,若是在一個對象使用結束以後未將該對象從該容器中移除掉,就會形成該對象不能被正確回收,從而形成內存泄漏,解決辦法固然就是在使用完以後將該對象從容器中移除。

WebView形成的內存泄露

  具體的能夠看看個人這篇博客:android WebView詳解,常見漏洞詳解和安全源碼(下)

未取消註冊致使的內存泄漏

  一些 Android 程序可能引用咱們的 Android 程序的對象(好比註冊機制),即便咱們的 Android 程序已經結束了,可是別的應用程序仍然還持有對咱們 Android 程序某個對象的引用,這樣也會形成內存不能被回收,好比調用 registerReceiver 後未調用unregisterReceiver。假設咱們但願在鎖屏界面(LockScreen)中,監聽系統中的電話服務以獲取一些信息,則能夠在 LockScreen 中定義一個 PhoneStateListener 的對象,同時將它註冊到 TelephonyManager 服務中,對於 LockScreen 對象,當須要顯示鎖屏界面的時候就會建立一個 LockScreen 對象,而當鎖屏界面消失的時候 LockScreen 對象就會被釋放掉,可是若是在釋放 LockScreen 對象的時候忘記取消咱們以前註冊的 PhoneStateListener 對象,則會間接致使 LockScreen 沒法被回收,若是不斷的使鎖屏界面顯示和消失,則最終會因爲大量的 LockScreen 對象沒有辦法被回收而引發 OOM,雖然有些系統程序自己好像是能夠自動取消註冊的(固然不及時),可是咱們仍是應該在程序結束時明確的取消註冊。

由於內存碎片致使分配內存不足

  還有一種狀況是由於頻繁的內存分配和釋放,致使內存區域裏面存在不少碎片,當這些碎片足夠多,new 一個大對象的時候,全部的碎片中沒有一個碎片足夠大以分配給這個對象,可是全部的碎片空間加起來又是足夠的時候,就會出現 OOM,並且這種 OOM 從某種意義上講,是徹底可以避免的。
  因爲產生內存碎片的場景不少,從 Memory Monitor 來看,下面場景的內存抖動是很容易產生內存碎片的:

這裏寫圖片描述

最多見產生內存抖動的例子就是在 ListView 的 getView 方法中未複用 convertView 致使 View 的頻繁建立和釋放,針對這個問題的處理方式那固然就是複用 convertView;或者是 String 拼接建立大量小的對象(好比在一些頻繁調用的地方打字符串拼接的 log 的時候);若是是其餘的問題,就須要經過 Memory Monitor 去觀察內存的實時分配釋放狀況,找到內存抖動的地方修復它,或者若是當出現下面這種狀況下的 OOM 時,也是因爲內存碎片致使沒法分配內存:
這裏寫圖片描述

出現上面這種類型的 Crash 時就要去分析應用裏面是否是存在大量分配釋放對象的地方了。

Android 內存優化

  內存優化請看下篇:Android 性能優化以內存泄漏檢測以及內存優化(下)

引用

blog.csdn.net/luoshengyan…
blog.csdn.net/luoshengyan…
blog.csdn.net/luoshengyan…
blog.csdn.net/luoshengyan…
blog.csdn.net/luoshengyan…
mp.weixin.qq.com/s?__biz=MzA…
geek.csdn.net/news/detail…
www.jianshu.com/p/216b03c22…
zhuanlan.zhihu.com/p/25213586
joyrun.github.io/2016/08/08/…
www.cnblogs.com/larack/p/60…
source.android.com/devices/tec…
blog.csdn.net/high2011/ar…
gityuan.com/2015/10/03/…
www.ayqy.net/blog/androi…
developer.android.com/studio/prof…
zhuanlan.zhihu.com/p/26043999

相關文章
相關標籤/搜索