Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android內存洩漏查找和解決

Android內存洩漏查找和解決

編輯:關於Android編程

一.內存洩漏概念

1.什麼是內存洩漏?
用動態存儲分配函數動態開辟的空間,在使用完畢後未釋放,結果導致一直占據該內存單元。直到程序結束。即所謂的內存洩漏。其實說白了就是該內存空間使用完畢之後未回收

2.內存洩漏會導致的問題
內存洩露就是系統回收不了那些分配出去但是又不使用的內存, 隨著程序的運行,可以使用的內存就會越來越少,機子就會越來越卡,直到內存數據溢出,然後程序就會掛掉,再跟著操作系統也可能無響應。

(在我們平時寫應用的過程中,可能會無意的寫了一些存在內存洩漏的代碼,如果沒有專業的工具,對內存洩漏的原理也不熟悉,要查內存洩漏出現在哪裡是比較困難的)接下來先看一個內存洩漏的例子

二.內存洩漏的例子

內存洩漏例子1

這個例子存在的問題應該很容易能看出來,使用了handler延遲一定時間執行Runnable代碼塊,而在Activity結束的時候又沒有釋放執行的代碼塊,導致了內存洩漏。那麼只要在Activity結束onDestroy的時候,釋放延遲執行的代碼塊不就可以了,確實是,那麼再看一看下面的例子。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPjxpbWcgYWx0PQ=="內存洩漏例子2" src="https://www.android5.online/Android/UploadFiles_5356/201702/2017022311275033.png" title="\" />

這段代碼是實際開發中存在內存洩漏的實例,稍微進行簡化得到的。內存洩漏的關鍵點在哪裡,怎麼去解決,先留著這個問題,看下面一節的內容:”失效”的private修飾符。

三.Java中”失效”的private修飾符

相信大家都用過內部類,Java允許在一個類裡面定義另一個類,類裡面的類就是內部類,也叫做嵌套類。一個簡單的內部類實現可以如下

class OuterClass {
    class InnerClass{
    }
}

下面回頭看上面寫的例子:

內存洩漏例子1

這其實是一個我們在編程中經常用到的場景,就是在一個內部類裡面訪問外部類的private成員變量或者方法,這是可以的。
這是為什麼,不是private修飾的成員只能被成員所述的類才能訪問麼?難道private真的失效了麼?
其實是編譯器幫我們做了一些我們看不到的工作,下面我們通過反編譯把這些看不到的工作都扒出來看看


反編譯後

1.下面這一份是通過 dex2jar + jad 進行反編譯得到的近似源碼的java類

反編譯源碼1

可以看到這份反編譯出來的代碼,比我們編寫的源碼,要多了一些東西,在內部類MyRunnable裡面多了一個MainActivity的成員變量,並且,在構造函數裡面獲得了外部類的引用。

2.再看看下面這一份文件,這是通過 apktool 反編譯出來的 smali指令語言
在這裡MainActivity分成了兩個文件,分別是MainActivity.smaliMainActivity$MyRunnable.smali。下面貼出的兩份文件比較長,簡單浏覽一遍即可,詳細看下面的解析,了解這份文件跟源碼的對應關系。

MainActivity:

.class public Lcom/gexne/car/leaktest/MainActivity;
.super Landroid/app/Activity;
.source "MainActivity.java"


# annotations
.annotation system Ldalvik/annotation/MemberClasses;
    value = {
        Lcom/gexne/car/leaktest/MainActivity$MyRunnable;
    }
.end annotation


# instance fields
.field private handler:Landroid/os/Handler;

.field private test:Ljava/lang/String;


# direct methods
.method public constructor ()V
    .locals 1

    .prologue
    .line 18
    invoke-direct {p0}, Landroid/app/Activity;->()V

    .line 20
    const-string v0, "TEST_STR"

    iput-object v0, p0, Lcom/gexne/car/leaktest/MainActivity;->test:Ljava/lang/String;

    .line 21
    new-instance v0, Landroid/os/Handler;

    invoke-direct {v0}, Landroid/os/Handler;->()V

    iput-object v0, p0, Lcom/gexne/car/leaktest/MainActivity;->handler:Landroid/os/Handler;

    return-void
.end method

.method static synthetic access$000(Lcom/gexne/car/leaktest/MainActivity;)Ljava/lang/String;
    .locals 1
    .param p0, "x0"    # Lcom/gexne/car/leaktest/MainActivity;

    .prologue
    .line 18
    iget-object v0, p0, Lcom/gexne/car/leaktest/MainActivity;->test:Ljava/lang/String;

    return-object v0
.end method


# virtual methods
.method protected onCreate(Landroid/os/Bundle;)V
    .locals 4
    .param p1, "savedInstanceState"    # Landroid/os/Bundle;

    .prologue
    .line 32
    invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

    .line 33
    const/high16 v0, 0x7f040000

    invoke-virtual {p0, v0}, Lcom/gexne/car/leaktest/MainActivity;->setContentView(I)V

    .line 34
    iget-object v0, p0, Lcom/gexne/car/leaktest/MainActivity;->handler:Landroid/os/Handler;

    new-instance v1, Lcom/gexne/car/leaktest/MainActivity$MyRunnable;

    invoke-direct {v1, p0}, Lcom/gexne/car/leaktest/MainActivity$MyRunnable;->(Lcom/gexne/car/leaktest/MainActivity;)V

    const-wide/16 v2, 0x2710

    invoke-virtual {v0, v1, v2, v3}, Landroid/os/Handler;->postDelayed(Ljava/lang/Runnable;J)Z

    .line 36
    invoke-virtual {p0}, Lcom/gexne/car/leaktest/MainActivity;->finish()V

    .line 37
    return-void
.end method

在上面MainActivity.smali文件中,可以看到.field代表的是成員變量,.method代表的是方法,2個成員變量分別是Handler和String,方法則有3個分別是構造函數、onCreate()、access$000()
嗯?在MainActivity中我們並沒有定義access$000()這種方法,它是一個靜態方法,接收一個MainActivity實例作為參數,並且返回MainActivity的test成員變量,所以,它出現的目的就是為了得到MainActivity的私有屬性。

MainActivity$MyRunnable.smali:

.class Lcom/gexne/car/leaktest/MainActivity$MyRunnable;
.super Ljava/lang/Object;
.source "MainActivity.java"

# interfaces
.implements Ljava/lang/Runnable;


# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
    value = Lcom/gexne/car/leaktest/MainActivity;
.end annotation

.annotation system Ldalvik/annotation/InnerClass;
    accessFlags = 0x0
    name = "MyRunnable"
.end annotation


# instance fields
.field final synthetic this$0:Lcom/gexne/car/leaktest/MainActivity;


# direct methods
.method constructor (Lcom/gexne/car/leaktest/MainActivity;)V
    .locals 0
    .param p1, "this$0"    # Lcom/gexne/car/leaktest/MainActivity;

    .prologue
    .line 23
    iput-object p1, p0, Lcom/gexne/car/leaktest/MainActivity$MyRunnable;->this$0:Lcom/gexne/car/leaktest/MainActivity;

    invoke-direct {p0}, Ljava/lang/Object;->()V

    return-void
.end method


# virtual methods
.method public run()V
    .locals 2

    .prologue
    .line 26
    const-string v0, "test"

    iget-object v1, p0, Lcom/gexne/car/leaktest/MainActivity$MyRunnable;->this$0:Lcom/gexne/car/leaktest/MainActivity;

    # getter for: Lcom/gexne/car/leaktest/MainActivity;->test:Ljava/lang/String;
    invoke-static {v1}, Lcom/gexne/car/leaktest/MainActivity;->access$000(Lcom/gexne/car/leaktest/MainActivity;)Ljava/lang/String;

    move-result-object v1

    invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

    .line 27
    return-void
.end method

MyRunnable.smali文件中用同樣的方法觀察,發現多了一個成員變量MainActivity,方法分別是構造函數、run(),根據smali指令的含義可以看到構造函數是接收了一個MainActivity作為參數的,而run()方法中獲取外部類中的test變量,則是調用access$000()方法獲取。如果想了解smali指令語言可以自行google,這裡不詳細講解。通過上面兩個文件,重新還原一下源碼。

復原反編譯代碼

這段代碼基本上還原了編譯器編譯後指令的執行方式。內部類調用外部類,是通過一個外部類的引用進行調用的(上面紅色框框的兩段代碼是在還原的基礎上加入的,用於解釋內部類調用外部類的方式,調用方式1是我們常用的,而到的編譯器編譯後,實際調用方式是2),而外部類的private屬性則通過編譯器生成的我們看不見的靜態方法,通過傳入外部類實例引用獲取出來。
通過還原,我們了解了非靜態內部類跟外部類交互時的工作方式,以及非靜態內部類為什麼會持有外部類的引用。

參考資料:
1. 細話Java:”失效”的private修飾符
2. smali語法簡析

四.通過dumpsys查看內存使用情況

繼續回頭看第一個內存洩漏的例子,稍微進行修改

查看內存洩漏1

對於這段代碼,它會造成內存洩漏,那麼對於外部類Activity來說,它能夠被釋放嗎?
我們通過dumpsys來查看,了解怎麼查看應用的內存使用情況,怎麼看一個Activity有沒有被順利釋放掉,而這個Activity能不能被回收。


1.先創建一個空Activity,如下代碼所示,並安裝到設備中

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

2.通過adb shell dumpsys meminfo 來查看內存使用狀況
在沒有打開應用的情況下,該命令返回的數據是這樣的:
dumpsys未打開應用

3.打開這個應用的MainActivity,再通過命令查看:
這裡寫圖片描述

可以看到打印出來很多的信息,而對於我們查看Activity內存洩漏來說,只需要關注Activities和Views兩個信息即可,在應用中存在的Activity對象有一個,存在的View對象有13個。

4.這時候我們退出這個Activity,在用命令查看一下:
這裡寫圖片描述

可以看到,Activity對象和View對象都在極短的時間內被回收掉了。再次打開,退出,多次嘗試,發現情況都是一樣的。我們可以通過這種方式來簡單判斷一個Activity是否存在內存洩漏,最後是否能夠被回收。

5.再運行剛才的洩漏的例子,用命令查看一下:
這裡寫圖片描述

當我們連續打開退出同一個頁面,然後使用命令查看時,發現Activity存在13個,而View則存在了234個,而且沒有很快被回收,依次判斷應該是存在內存洩漏了。
等待10多秒,再次查看,發現Activity和View的數量都變成了0。
這裡寫圖片描述
所以,結論是能夠被回收,只要Runnable代碼塊執行完畢,釋放了Activity的引用,Activity就能被回收。


上面的例子,是Handler臨時性內存洩漏,只要Handler post的代碼塊執行完畢,被引用的Activity就能夠釋放。
除了臨時性內存洩漏,還有危害更大,直到程序結束才能被釋放的內存洩漏。例如:
這裡寫圖片描述

內存洩漏例子2
對於第一個例子,比較容易看出來,MyRunnable內部類持有了Activity的引用,而它自身一直不釋放,導致Activity也一直無法釋放,使用dumpsys meminfo查看可以驗證,多次打開後退Activities的數量只會增加不會減少,直到手動結束整個應用。
而第二個例子也不難看出,只是引用鏈稍微長了點,TelephonyManager注冊了內部類PhoneStateListener,持有了這個內部類的引用,PhoneStateListener持有了ViewHolder的引用,ViewHolder同時也是一個內部類,持有了ViewAdapter的引用,而ViewAdapter則持有了Activity的引用,最後TelephonyManager又沒有做反注冊的操作,導致了內存洩漏。
很多時候我們寫代碼,都忽略了釋放工作,特別是寫Java寫多了,都覺得這些資源會自動釋放,不用寫釋放方法,不用操心去做釋放工作,然後內存洩漏就這樣出現了。

參考資料:
1. 使用meminfo分析Android單個進程內存信息

五.強引用與弱引用

看完上面的例子,了解到非靜態內部類因為持有外部類的引用,很可能會造成洩漏。為什麼持有了外部類的引用會導致外部類不能被回收?

在解決內存洩漏之前,先了解Java的引用方式。Java有四種引用方式,分別是強引用、弱引用、虛引用、軟引用。這裡只介紹強引用以及弱引用,更詳細的資料可以自行查找。


1.強引用(Strong Reference),就是我們經常使用的引用,寫法如下

StringBuffer buffer = new StringBuffer();

上面創建了一個StringBuffer對象,並將這個對象的(強)引用存到變量buffer中。強引用最重要的就是它能夠讓引用變得強(Strong),這就決定了它和垃圾回收器的交互。具體來說,如果一個對象可以從GC Roots通過強引用到達時,那麼這個對象將不會被GC回收。

2.弱引用(Weak Reference),弱引用簡單來說就是將對象留在內存的能力不是那麼強的引用。使用WeakReference,垃圾回收器會幫你來決定引用的對象何時回收並且將對象從內存移除。創建弱引用如下

WeakReference weakWidget = new WeakReference(widget);

使用weakWidget.get()就可以得到真實的Widget對象,因為弱引用不能阻擋垃圾回收器對其回收,你會發現(當沒有任何強引用到widget對象時)使用get時突然返回null,所以對於弱引用要記得做判空處理後再使用,否則很容易出現NPE異常。

參考資料:
1. GC Roots
2. 理解Java中的弱引用

六.解決內部類的內存洩漏

通過上面介紹的內容,我們了解到內存洩漏產生的原因是對象在生命周期結束時被另一個對象通過強引用持有而無法釋放造成的

怎麼解決這個問題,思路就是避免使用非靜態內部類,定義內部類時,要麼是放在單獨的類文件中,要麼就是使用靜態內部類。因為靜態的內部類不會持有外部類的引用,所以不會導致外部類實例的內存洩露。當你需要在靜態內部類中調用外部的Activity時,我們可以使用弱引用來處理。
這裡寫圖片描述

這種解決方法,對於臨時性內存洩漏適用,其中包括但不限於自定義動畫的更新回調,網絡請求數據後更新頁面的回調等,更具體一點的例子有當我們在頁面觸發了網絡請求加載時,希望它把數據加載完畢,當加載完畢時如果頁面還在活動狀態則更新顯示內容。其實在Android中很多的內存洩露都是由於在Activity中使用了非靜態內部類導致的,所以當我們使用時要非靜態內部類時要格外注意。

在Android Studio裡面,當你定義一個內部類Handler的時候,會出現貼心提示,This Handler class should be static or leaks might occur,提醒你把Handler改成靜態類。

這裡寫圖片描述


解決了上面的內存洩漏問題,再看看下面這個例子:
這裡寫圖片描述

這個例子改寫成靜態內部類+弱引用,並不能完全解決內存洩漏的問題。
為什麼?只需要加上一句Log即可驗證。
這裡寫圖片描述

多次進入退出頁面,看一下打印出來的Log
這裡寫圖片描述

結果顯而易見,Log越來越多了,雖然Activity最後能夠回收,但只是因為弱引用很弱,GC能夠在內存不足的時候回收它,但並沒有完全解決洩漏問題。

使用dumsys meminfo同樣可以驗證,每一次打開Activity並退出,等GC回收掉Activity後,發現Local Binder的數量並沒有減少,而且比上一次多了1。
這裡寫圖片描述

對於注冊到服務中的回調(包括系統服務,自定義服務),使用靜態內部類+弱引用的方式只能部分解決內存洩漏問題,這種問題需要釋放資源時進行反注冊才能根本解決,因為這種服務會長期存在系統中,注冊了的callback對象會一直存在於服務中,每次callback來了都會執行callback中的代碼塊,只不過執行到弱引用部分由於弱引用獲取到的對象為null而不會執行下一步操作。例如Broadcast,例如systemServer.listen等。

參考資料:
1. Android中Handler引起的內存洩露

七.Context造成的洩漏

了解完內部類的洩漏以及修復方法,再來看一下另一種洩漏,由context造成的洩漏。
這裡寫圖片描述

這也是一個開發中的例子,稍作修改得到。

可以看到,藍色框框內是一個標准的懶漢式單例。單例是我們比較簡單常用的一種設計模式,然而如果單例使用不當也會導致內存洩露。比如這個例子,DashBoardTypeface需要持有一個Context作為成員變量,並且使用該Context創建字體資源。
instance作為靜態對象,其生命周期要長於普通的對象,其中也包含Activity,當我們退出Activity,默認情況下,系統會銷毀當前Activity,然後當前的Activity被一個單例持有,導致垃圾回收器無法進行回收,進而產生了內存洩露。

解決的方法就是不持有Activity的引用,而是持有Application的Context引用。

這裡寫圖片描述

在任何使用到Context的地方,都要多加注意,例如我們常見的Dialog,Menu,懸浮窗,這些控件都需要傳入Context作為參數的,如果要使用Activity作為Context參數,那麼一定要保證控件的生命周期跟Activity的生命周期同步。窗體洩漏也是內存洩漏的一種,就是我們常見的leak window,這種錯誤就是依賴Activity的控件生命周期跟Activity不同步造成的。

一般來說,對於非控件類型的對象需要Context參數,最好優先考慮全局ApplicationContext,來避免內存洩漏。

參考資料:
1. 避免Android中Context引起的內存洩露

八.使用LeakCanary工具查找內存洩漏

LeakCanary是什麼?它是一個傻瓜化並且可視化的內存洩露分析工具。

它的特點是簡單,易於發現問題,人人都可參與,只要配置完成,簡單的黑盒測試通過手工點擊就能夠看到詳細的洩漏路徑。

下面來看一下如何集成:

dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
 }

創建Application並加入LeakCanary代碼:

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    LeakCanary.install(this);
  }
}

這樣已經完成最簡單的集成,可以開始進行測試了。
在進行嘗試之前再看一段代碼:

這裡寫圖片描述

思考完這段代碼的問題後,我們來嘗試一下使用LeakCanary尋找問題。如上面的配置,配置好應用,安裝後可以看到,應用多了一個入口,如圖所示。

這裡寫圖片描述

這個入口就是當應用在使用過程中發生內存洩漏,可以從這個入口看到詳細的洩漏位置。

這裡寫圖片描述

從LeakCanary給出來的分析能輕易找到內存洩漏出現在responseHandler裡面,跟剛才思考分析的答案是否一致呢?如果一致那你對內存洩漏的知識已經掌握不少了。


上面這種是最簡單的默認配置,只對Activity進行了檢測。但需要檢測的對象肯定不只有Activity,例如Fragment、Service、Broadcast。這需要做更多的配置,在Application中留下RefWatcher的引用,使用它來檢測其他對象。

public class MyApplication extends Application {
    private static RefWatcher sRefWatcher;


    @Override
    public void onCreate() {
        super.onCreate();
        sRefWatcher = LeakCanary.install(this);
    }

    public static RefWatcher getRefWatcher() {
        return sRefWatcher;
    }
}

在有生命周期的對象的onDestroy()中進行監控,例如Service。

public class CoreService extends Service {
    @Override
    public void onDestroy() {
        super.onDestroy();
        MyApplication.getRefWatcher().watch(this);
    }
}

監控需要設置在對象(很快)被釋放的時候,如Activity和Fragment的onDestroy方法。

一個錯誤示例,比如監控一個Activity,放在onCreate就會大錯特錯了,那麼你每次都會收到Activity的洩露通知。

更詳細的資料可以到LeakCanary的github倉庫中查看。

參考資料:
1. Android內存洩漏檢測利器:LeakCanary
2. LeakCanary

九.總結

關於內存洩漏的知識,如何定位內存洩漏,如何修復,已經講解完了。
最後做一個總結:

場景

非靜態內部類的靜態實例
非靜態內部類會維持一個到外部類實例的引用,如果非靜態內部類的實例是靜態的,就會間接長期維持著外部類的引用,阻止被回收掉。 資源對象未關閉
資源性對象如Cursor、File、Socket,應該在使用後及時關閉。未在finally中關閉,會導致異常情況下資源對象未被釋放的隱患。 注冊對象未反注冊
未反注冊會導致觀察者列表裡維持著對象的引用,阻止垃圾回收。 Handler臨時性內存洩露
Handler通過發送Message與主線程交互,Message發出之後是存儲在MessageQueue中的,有些Message也不是馬上就被處理的。在Message中存在一個 target,是Handler的一個引用,如果Message在Queue中存在的時間越長,就會導致Handler無法被回收。如果Handler是非靜態的,則會導致Activity或者Service不會被回收。
由於AsyncTask內部也是Handler機制,同樣存在內存洩漏的風險。
此種內存洩露,一般是臨時性的。

預防

不要維持到Activity的長久引用,對activity的引用應該和activity本身有相同的生命周期。 盡量使用context-application代替context-activity Activity中盡量不要使用非靜態內部類,可以使用靜態內部類和WeakReference代替。

參考資料:
1. Android內存洩漏研究

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved