1.甚么是內存泄漏?
用動態存儲分配函數動態開辟的空間,在使用終了后未釋放,結果致使1直占據該內存單元。直到程序結束。即所謂的內存泄漏。
其實說白了就是該內存空間使用終了以后未回收
2.內存泄漏會致使的問題
內存泄漏就是系統回收不了那些分配出去但是又不使用的內存, 隨著程序的運行,可使用的內存就會愈來愈少,機子就會愈來愈卡,直到內存數據溢出,然后程序就會掛掉,再隨著操作系統也可能無響應。
(在我們平時寫利用的進程中,可能會無意的寫了1些存在內存泄漏的代碼,如果沒有專業的工具,對內存泄漏的原理也不熟習,要查內存泄漏出現在哪里是比較困難的)接下來先看1個內存泄漏的例子
這個例子存在的問題應當很容易能看出來,使用了handler延遲1定時間履行Runnable代碼塊,而在Activity結束的時候又沒有釋放履行的代碼塊,致使了內存泄漏。那末只要在Activity結束onDestroy的時候,釋放延遲履行的代碼塊不就能夠了,確切是,那末再看1看下面的例子。
這段代碼是實際開發中存在內存泄漏的實例,略微進行簡化得到的。內存泄漏的關鍵點在哪里,怎樣去解決,先留著這個問題,看下面1節的內容:”失效”的private修飾符。
相信大家都用過內部類,Java允許在1個類里面定義另外一個類,類里面的類就是內部類,也叫做嵌套類。1個簡單的內部類實現可以以下
class OuterClass {
class InnerClass{
}
}
下面回頭看上面寫的例子:
這實際上是1個我們在編程中常常用到的場景,就是在1個內部類里面訪問外部類的private成員變量或方法,這是可以的。
這是為何,不是private修飾的成員只能被成員所述的類才能訪問么?難道private真的失效了么?
實際上是編譯器幫我們做了1些我們看不到的工作
,下面我們通過反編譯把這些看不到的工作都扒出來看看
1.下面這1份是通過 dex2jar + jad
進行反編譯得到的近似源碼的java類
可以看到這份反編譯出來的代碼,比我們編寫的源碼,要多了1些東西,在內部類MyRunnable里面多了1個MainActivity的成員變量,并且,在構造函數里面取得了外部類的援用。
2.再看看下面這1份文件,這是通過 apktool
反編譯出來的 smali指令語言
在這里MainActivity分成了兩個文件,分別是MainActivity.smali
和MainActivity$MyRunnable.smali
。下面貼出的兩份文件比較長,簡單閱讀1遍便可,詳細看下面的解析,了解這份文件跟源碼的對應關系。
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 <init>()V
.locals 1
.prologue
.line 18
invoke-direct {p0}, Landroid/app/Activity;-><init>()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;-><init>()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;-><init>(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()
這類方法,它是1個靜態方法,接收1個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 <init>(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;-><init>()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文件中用一樣的方法視察,發現多了1個成員變量MainActivity,方法分別是構造函數、run(),根據smali指令的含義可以看到構造函數是接收了1個MainActivity作為參數的,而run()方法中獲得外部類中的test變量,則是調用access$000()方法獲得。如果想了解smali指令語言可以自行google,這里不詳細講授。通過上面兩個文件,重新還原1下源碼。
這段代碼基本上還原了編譯器編譯后指令的履行方式。內部類調用外部類,是通過1個外部類的援用進行調用的(上面紅色框框的兩段代碼是在還原的基礎上加入的,用于解釋內部類調用外部類的方式,調用方式1是我們經常使用的,而到的編譯器編譯后,實際調用方式是2),而外部類的private屬性則通過編譯器生成的我們看不見的靜態方法,通過傳入外部類實例援用獲得出來。
通過還原,我們了解了非靜態內部類跟外部類交互時的工作方式,和非靜態內部類為何會持有外部類的援用。
參考資料:
1. 細話Java:”失效”的private修飾符
2. smali語法簡析
繼續回頭看第1個內存泄漏的例子,略微進行修改
對這段代碼,它會造成內存泄漏,那末對外部類Activity來講,它能夠被釋放嗎?
我們通過dumpsys來查看,了解怎樣查看利用的內存使用情況,怎樣看1個Activity有無被順利釋放掉,而這個Activity能不能被回收。
1.先創建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 <packageName>
來查看內存使用狀態
在沒有打開利用的情況下,該命令返回的數據是這樣的:
3.打開這個利用的MainActivity,再通過命令查看:
可以看到打印出來很多的信息,而對我們查看Activity內存泄漏來講,只需要關注Activities和Views兩個信息便可,在利用中存在的Activity對象有1個,存在的View對象有13個。
4.這時候候我們退出這個Activity,在用命令查看1下:
可以看到,Activity對象和View對象都在極短的時間內被回收掉了。再次打開,退出,屢次嘗試,發現情況都是1樣的。我們可以通過這類方式來簡單判斷1個Activity是不是存在內存泄漏,最后是不是能夠被回收。
5.再運行剛才的泄漏的例子,用命令查看1下:
當我們連續打開退出同1個頁面,然后使用命令查看時,發現Activity存在13個,而View則存在了234個,而且沒有很快被回收,順次判斷應當是存在內存泄漏了。
等待10多秒,再次查看,發現Activity和View的數量都變成了0。
所以,結論是能夠被回收,只要Runnable代碼塊履行終了,釋放了Activity的援用,Activity就可以被回收。
上面的例子,是Handler臨時性內存泄漏,只要Handler post的代碼塊履行終了,被援用的Activity就可以夠釋放。
除臨時性內存泄漏,還有危害更大,直到程序結束才能被釋放的內存泄漏。例如:
對第1個例子,比較容易看出來,MyRunnable內部類持有了Activity的援用,而它本身1直不釋放,致使Activity也1直沒法釋放,使用dumpsys meminfo查看可以驗證,屢次打開后退Activities的數量只會增加不會減少,直得手動結束全部利用。
而第2個例子也不難看出,只是援用鏈略微長了點,TelephonyManager注冊了內部類PhoneStateListener,持有了這個內部類的援用,PhoneStateListener持有了ViewHolder的援用,ViewHolder同時也是1個內部類,持有了ViewAdapter的援用,而ViewAdapter則持有了Activity的援用,最后TelephonyManager又沒有做反注冊的操作,致使了內存泄漏。
很多時候我們寫代碼,都疏忽了釋放工作,特別是寫Java寫多了,都覺得這些資源會自動釋放,不用寫釋放方法,不用操心去做釋放工作,然后內存泄漏就這樣出現了。
參考資料:
1. 使用meminfo分析Android單個進程內存信息
看完上面的例子,了解到非靜態內部類由于持有外部類的援用,極可能會造成泄漏。為何持有了外部類的援用會致使外部類不能被回收?
在解決內存泄漏之前,先了解Java的援用方式。Java有4種援用方式,分別是強援用、弱援用、虛援用、軟援用。這里只介紹強援用和弱援用,更詳細的資料可以自行查找。
1.強援用(Strong Reference),就是我們常常使用的援用,寫法以下
StringBuffer buffer = new StringBuffer();
上面創建了1個StringBuffer對象,并將這個對象的(強)援用存到變量buffer中。強援用最重要的就是它能夠讓援用變得強(Strong),這就決定了它和垃圾回收器的交互。具體來講,如果1個對象可以從GC Roots通過強援用到達時,那末這個對象將不會被GC回收。
2.弱援用(Weak Reference),弱援用簡單來講就是將對象留在內存的能力不是那末強的援用。使用WeakReference,垃圾回收器會幫你來決定援用的對象什么時候回收并且將對象從內存移除。創建弱援用以下
WeakReference<Widget> weakWidget = new WeakReference<Widget>(widget);
使用weakWidget.get()
就能夠得到真實的Widget對象,由于弱援用不能阻擋垃圾回收器對其回收,你會發現(當沒有任何強援用到widget對象時)使用get時突然返回null,所以對弱援用要記得做判空處理后再使用,否則很容易出現NPE異常。
參考資料:
1. GC Roots
2. 理解Java中的弱援用
通過上面介紹的內容,我們了解到內存泄漏產生的緣由是對象在生命周期結束時被另外一個對象通過強援用持有而沒法釋放
釀成的
怎樣解決這個問題,思路就是避免使用非靜態內部類,定義內部類時,要末是放在單獨的類文件中,要末就是使用靜態內部類。由于靜態的內部類不會持有外部類的援用,所以不會致使外部類實例的內存泄漏。當你需要在靜態內部類中調用外部的Activity時,我們可使用弱援用來處理。
這類解決方法,對臨時性內存泄漏適用,其中包括但不限于自定義動畫的更新回調,網絡要求數據后更新頁面的回調等,更具體1點的例子有當我們在頁面觸發了網絡要求加載時,希望它把數據加載終了,當加載終了時如果頁面還在活動狀態則更新顯示內容。其實在Android中很多的內存泄漏都是由于在Activity中使用了非靜態內部類致使的,所以當我們使用時要非靜態內部類時要格外注意。
在Android Studio里面,當你定義1個內部類Handler的時候,會出現貼心提示,This Handler class should be static or leaks might occur,提示你把Handler改成靜態類。
解決了上面的內存泄漏問題,再看看下面這個例子:
這個例子改寫成靜態內部類+弱援用,其實不能完全解決內存泄漏的問題。
為何?只需要加上1句Log便可驗證。
屢次進入退出頁面,看1下打印出來的Log
結果不言而喻,Log愈來愈多了,雖然Activity最后能夠回收,但只是由于弱援用很弱,GC能夠在內存不足的時候回收它,但并沒有完全解決泄漏問題。
使用dumsys meminfo
一樣可以驗證,每次打開Activity并退出,等GC回收掉Activity后,發現Local Binder的數量并沒有減少,而且比上1次多了1。
對注冊到服務中的回調(包括系統服務,自定義服務),使用靜態內部類+弱援用的方式只能部份解決內存泄漏問題,這類問題需要釋放資源時進行反注冊才能根本解決,由于這類服務會長時間存在系統中,注冊了的callback對象會1直存在于服務中,每次callback來了都會履行callback中的代碼塊,只不過履行到弱援用部份由于弱援用獲得到的對象為null而不會履行下1步操作。例如Broadcast,例如systemServer.listen等。
參考資料:
1. Android中Handler引發的內存泄漏
了解完內部類的泄漏和修復方法,再來看1下另外一種泄漏,由context釀成的泄漏。
這也是1個開發中的例子,稍作修改得到。
可以看到,藍色框框內是1個標準的懶漢式單例。單例是我們比較簡單經常使用的1種設計模式,但是如果單例使用不當也會致使內存泄漏。比如這個例子,DashBoardTypeface需要持有1個Context作為成員變量,并且使用該Context創建字體資源。
instance作為靜態對象,其生命周期要擅長普通的對象,其中也包括Activity,當我們退出Activity,默許情況下,系統會燒毀當前Activity,然后當前的Activity被1個單例持有,致使垃圾回收器沒法進行回收,進而產生了內存泄漏。
解決的方法就是不持有Activity的援用,而是持有Application的Context援用。
在任何使用到Context的地方,都要多加注意,例如我們常見的Dialog,Menu,懸浮窗,這些控件都需要傳入Context作為參數的,如果要使用Activity作為Context參數,那末1定要保證控件的生命周期跟Activity的生命周期同步。窗體泄漏也是內存泄漏的1種,就是我們常見的leak window,這類毛病就是依賴Activity的控件生命周期跟Activity不同步釀成的。
1般來講,對非控件類型的對象需要Context參數,最好優先斟酌全局ApplicationContext,來避免內存泄漏。
參考資料:
1. 避免Android中Context引發的內存泄漏
LeakCanary是甚么?它是1個傻瓜化并且可視化的內存泄漏分析工具。
它的特點是簡單,易于發現問題,人人都可參與,只要配置完成,簡單的黑盒測試通過手工點擊就可以夠看到詳細的泄漏路徑。
下面來看1下如何集成:
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);
}
}
這樣已完成最簡單的集成,可以開始進行測試了。
在進行嘗試之前再看1段代碼:
思考完這段代碼的問題后,我們來嘗試1下使用LeakCanary尋覓問題。如上面的配置,配置好利用,安裝后可以看到,利用多了1個入口,如圖所示。
這個入口就是當利用在使用進程中產生內存泄漏,可以從這個入口看到詳細的泄漏位置。
從LeakCanary給出來的分析能輕易找到內存泄漏出現在responseHandler里面,跟剛才思考分析的答案是不是1致呢?如果1致那你對內存泄漏的知識已掌握很多了。
上面這類是最簡單的默許配置,只對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方法。
1個毛病示例,比如監控1個Activity,放在onCreate就會大錯特錯了,那末你每次都會收到Activity的泄漏通知。
更詳細的資料可以到LeakCanary的github倉庫中查看。
參考資料:
1. Android內存泄漏檢測利器:LeakCanary
2. LeakCanary
關于內存泄漏的知識,如何定位內存泄漏,如何修復,已講授完了。
最后做1個總結:
參考資料:
1. Android內存泄漏研究