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