作為開(kāi)發(fā)人員翻斟,在我們的日常開(kāi)發(fā)中逾礁,為了構(gòu)建更好的應(yīng)用程序,我們需要考慮很多事情以保證應(yīng)用運(yùn)行在正軌上访惜,其中之一是要確保我們的應(yīng)用程序不會(huì)崩潰嘹履。應(yīng)用崩潰的一個(gè)常見(jiàn)原因是內(nèi)存泄漏。這方面的問(wèn)題可以以各種形式表現(xiàn)出來(lái)债热。在大多數(shù)情況下砾嫉,我們看到內(nèi)存使用率穩(wěn)步上升,直到應(yīng)用程序不能分配更多的資源窒篱,并不可避免地崩潰焕刮。在Java中這往往導(dǎo)致一個(gè)OutOfMemoryException異常被拋出舶沿。在某些罕見(jiàn)的情況下,泄露的類甚至可以逗留很長(zhǎng)時(shí)間來(lái)接收已注冊(cè)的回調(diào)配并,這會(huì)導(dǎo)致一些非常奇怪的錯(cuò)誤括荡,并往往拋出臭名昭著的IllegalStateException異常。
為了幫助他人在代碼分析上減少花費(fèi)時(shí)間荐绝,我將介紹內(nèi)存泄漏的幾個(gè)例子,闡述在Android Studio中如何檢查它們避消,當(dāng)然最重要的是如何將其解決低滩。
聲明
在這篇文章中的代碼示例的目的是為了促進(jìn)大家對(duì)內(nèi)存管理有更深的了解,特別是在java岩喷。其通用的體系結(jié)構(gòu)恕沫,線程管理和代碼示例的 HTTP 請(qǐng)求處理在真實(shí)的生產(chǎn)環(huán)境并不是理想的,這些示例僅僅為了說(shuō)明一個(gè)問(wèn)題:在Android中纱意,內(nèi)存泄漏是一件要考慮的事情婶溯。
監(jiān)聽(tīng)器注冊(cè)
這真的不應(yīng)該是個(gè)問(wèn)題,但我經(jīng)惩得梗看到各種注冊(cè)方法的調(diào)用迄委,但他們對(duì)應(yīng)的注銷方法卻無(wú)處可尋。這是泄漏的潛在來(lái)源类少,因?yàn)檫@些方法明確設(shè)計(jì)成互相抵消叙身。如果沒(méi)有調(diào)用注銷方法,被引用的對(duì)象已經(jīng)被終止后硫狞,監(jiān)聽(tīng)實(shí)例可能會(huì)持有該對(duì)象很長(zhǎng)的時(shí)間信轿,從而導(dǎo)致泄漏內(nèi)存。在Android中残吩,如果該對(duì)象是一個(gè)Activity對(duì)象财忽,是特別麻煩的,因?yàn)樗麄兺鶕碛写罅康臄?shù)據(jù)泣侮。讓我告訴你即彪,可能是什么樣子。
public class LeaksActivity extends Activity implements LocationListener {
private LocationManager locationManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leaks);
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
TimeUnit.MINUTES.toMillis(5), 100, this);
}
// Listener implementation omitted
}
在這個(gè)例子中活尊,我們讓Android的 LocationManager通知我們位置更新祖凫。我們所需要做的就是獲取系統(tǒng)服務(wù)本身和設(shè)置一個(gè)回調(diào)來(lái)接收更新。在這里酬凳,我們?cè)贏ctivity中實(shí)現(xiàn)了位置監(jiān)聽(tīng)接口惠况,這意味著LocationManager將持有該Activity的引用。現(xiàn)在宁仔,如果該設(shè)備被旋轉(zhuǎn)稠屠,新的Activity將被創(chuàng)建并取代已經(jīng)注冊(cè)位置更新接口的舊的Activity。由于系統(tǒng)服務(wù)存活時(shí)間肯定比任何Activity都要長(zhǎng),LocationManager仍然持有以前的Activity的引用权埠,這使GC不可能回收依賴于以前的Activity的資源榨了,從而導(dǎo)致內(nèi)存泄漏。如果反復(fù)旋轉(zhuǎn)設(shè)備攘蔽,將導(dǎo)致大量的不可回收的Activity填滿內(nèi)存龙屉,最終導(dǎo)致OutOfMemoryException異常。
但為了解決內(nèi)存泄漏满俗,我們首先必須要能夠找到它转捕。幸運(yùn)的是,Android Studio有一個(gè)叫做 Android Monitor的內(nèi)置工具唆垃,我們可以用它來(lái) 觀察除應(yīng)用內(nèi)存使用情況五芝。我們需要做的僅僅是打開(kāi)Android Monitor 并轉(zhuǎn)到對(duì)應(yīng)tab,看看使用了多少內(nèi)存和內(nèi)存實(shí)時(shí)分配情況辕万。
任何導(dǎo)致資源分配的交互都在這里反映出來(lái)枢步,使之成為跟蹤應(yīng)用程序的資源使用情況的理想場(chǎng)所。為了找到內(nèi)存泄露渐尿,當(dāng)我們懷疑在某個(gè)時(shí)間點(diǎn)內(nèi)存被泄露時(shí)醉途,我們需要知道在該時(shí)間點(diǎn)包含了那些內(nèi)存。對(duì)于這個(gè)特殊的例子砖茸,我們所要做的就是啟動(dòng)我們的應(yīng)用程序结蟋,然后旋轉(zhuǎn)設(shè)備一次,然后調(diào)用Dump Java Heap操作(在Memory的旁邊渔彰,從左邊數(shù)起第三個(gè)圖標(biāo))嵌屎。這將生成一個(gè)HPROF文件,其中包含我們調(diào)用該操作時(shí)的一個(gè)內(nèi)存快照恍涂。幾秒鐘后宝惰,Android Studio 會(huì)自動(dòng)打開(kāi)該文件,給我們更易于分析內(nèi)存的直觀表示再沧。
我不會(huì)去深入有關(guān)如何分析巨大的內(nèi)存堆尼夺。相反,我會(huì)把你的注意力引導(dǎo)到 Analyzer Tasks(下面截圖中的右上角)炒瘸。為了檢測(cè)上面的例子中引入的內(nèi)存泄漏淤堵,你所需要做的檢測(cè)是檢查泄露的Activity(Detect Leaked Activities),點(diǎn)擊播放按鈕然后在Analysis Results下面就會(huì)顯示泄露的Activity情況顷扩。
如果我們選中泄露的Activity拐邪,可以得到一個(gè)引用樹(shù),該引用樹(shù)可以檢測(cè)持有該Activity的引用隘截。通過(guò)尋找深度為零的實(shí)例扎阶,我們發(fā)現(xiàn)位置管理器中的實(shí)例mListener汹胃,是我們的Activity不能被GC回收的原因《危回到我們的代碼着饥,我們可以看到,這個(gè)引用是由于我們?cè)?strong>requestLocationsUpdates方法中設(shè)置Activity作為位置更新回調(diào)導(dǎo)致的惰赋。通過(guò)閱讀位置管理器文檔宰掉,問(wèn)題很快變得清晰,為了取消回調(diào)設(shè)置赁濒,我們簡(jiǎn)單地調(diào)用removeUpdates方法就行了轨奄。在我們的例子,因?yàn)槲覀冏?cè)更新是在onCreate方法流部,顯然要注銷的地方在onDestroy方法戚绕。
public class LeaksActivity extends Activity implements LocationListener {
private LocationManager locationManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leaks);
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
TimeUnit.MINUTES.toMillis(5), 100, this);
}
@Override
protected void onDestroy() {
locationManager.removeUpdates(this);
super.onDestroy();
}
// Listener implementation omitted
}
重新構(gòu)建程序并執(zhí)行與上述相同的內(nèi)存分析纹坐,無(wú)論旋轉(zhuǎn)多少次設(shè)備枝冀,應(yīng)該都不會(huì)導(dǎo)致Activity泄漏。
內(nèi)部類
內(nèi)部類在Java中是一個(gè)很常見(jiàn)的數(shù)據(jù)結(jié)構(gòu)耘子。它們很受歡迎果漾,因?yàn)樗鼈兛梢砸赃@樣的方式來(lái)定義:即只有外部類可以實(shí)例化它們。很多人可能沒(méi)有意識(shí)到的是這樣的類會(huì)持有外部類的隱式引用谷誓。隱式引用很容易出錯(cuò)绒障,尤其是當(dāng)兩個(gè)類具有不同的生命周期。以下是常見(jiàn)的Android Activity寫(xiě)法捍歪。
public class AsyncActivity extends Activity {
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_async);
textView = (TextView) findViewById(R.id.textView);
new BackgroundTask().execute();
}
private class BackgroundTask extends AsyncTask<Void, Void, String> {
@Override
protected String doInBackground(Void... params) {
// Do background work. Code omitted.
return "some string";
}
@Override
protected void onPostExecute(String result) {
textView.setText(result);
}
}
}
這種特殊的實(shí)現(xiàn)在執(zhí)行上沒(méi)有問(wèn)題户辱。問(wèn)題是,它保留內(nèi)存的時(shí)間肯定會(huì)超過(guò)必要的時(shí)間糙臼。由于BackgroundTask持有一個(gè)AsyncActivity隱式引用并運(yùn)行在另一個(gè)沒(méi)有取消策略的線程上庐镐,它將保留AsyncActivity在內(nèi)存中的所有資源連接,直到后臺(tái)線程終止運(yùn)行变逃。在HTTP請(qǐng)求的情況下必逆,這可能需要很長(zhǎng)的時(shí)間,尤其是在速度較慢的連接揽乱。
通過(guò)執(zhí)行相同的步驟名眉,如同前面的示例,并確保長(zhǎng)時(shí)間運(yùn)行的后臺(tái)任務(wù)凰棉,我們最終會(huì)得到下面的分析結(jié)果损拢。
從上面的分析中可以看出,BackgroundTask 確實(shí)是這種內(nèi)存泄漏的罪魁禍?zhǔn)兹鱿N覀兊谝灰獎(jiǎng)?wù)是使用靜態(tài)類的實(shí)現(xiàn)方式來(lái)消除指向Activity的引用探橱,但這樣我們也不能直接訪問(wèn) textView 了申屹。因此我們還需要添加一個(gè)構(gòu)造函數(shù),把textView作為參數(shù)傳遞進(jìn)來(lái)隧膏。最后哗讥,我們需要引入AsyncTask文檔中所述的取消策略“恚考慮到所有這一切杆煞,讓我們看看我們的代碼最終呈現(xiàn)。
public class AsyncActivity extends Activity {
TextView textView;
AsyncTask task;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_async);
textView = (TextView) findViewById(R.id.textView);
task = new BackgroundTask(textView).execute();
}
@Override
protected void onDestroy() {
task.cancel(true);
super.onDestroy();
}
private static class BackgroundTask extends AsyncTask<Void, Void, String> {
private final TextView resultTextView;
public BackgroundTask(TextView resultTextView) {
this.resultTextView = resultTextView;
}
@Override
protected void onCancelled() {
// Cancel task. Code omitted.
}
@Override
protected String doInBackground(Void... params) {
// Do background work. Code omitted.
return "some string";
}
@Override
protected void onPostExecute(String result) {
resultTextView.setText(result);
}
}
}
現(xiàn)在腐泻,隱式引用已被消除决乎,我們通過(guò)構(gòu)造函數(shù)傳遞相關(guān)實(shí)例,并在合適的地方取消任務(wù)派桩。讓我們?cè)龠\(yùn)行分析任務(wù)构诚,看看這種改變是否消除了內(nèi)存泄漏。
看來(lái)我們還有一些工作要做铆惑。根據(jù)前一個(gè)例子的經(jīng)驗(yàn)范嘱,我們可以知道在引用樹(shù)中高亮標(biāo)注的實(shí)例導(dǎo)致了Activity泄露。那么這是什么回事员魏?我們看一下它的父節(jié)點(diǎn)就可以發(fā)現(xiàn)resultTextView持有一個(gè)mContext引用丑蛤,毫無(wú)疑問(wèn),它就是泄露的Activity的引用撕阎。那么如何解決這個(gè)問(wèn)題受裹?我們無(wú)法消除resultTextView綁定的context引用,因?yàn)槲覀冃枰?strong>BackgroundTask中使用resultTextView的引用虏束,以便更新用戶界面棉饶。為了解決這個(gè)問(wèn)題,一種簡(jiǎn)單的方法是使用WeakReference镇匀。我們持有的resultTextView引用是強(qiáng)引用照藻,具有防止GC回收的能力。相反坑律,WeakReference不保證其引用的實(shí)例存活岩梳。當(dāng)一個(gè)實(shí)例最后一個(gè)強(qiáng)引用被刪除,GC會(huì)把其資源回收晃择,而不管這個(gè)實(shí)例是否有弱引用冀值。下面是使用WeakReference的最終版本:
public class AsyncActivity extends Activity {
TextView textView;
AsyncTask task;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_async);
textView = (TextView) findViewById(R.id.textView);
task = new BackgroundTask(textView).execute();
}
@Override
protected void onDestroy() {
task.cancel(true);
super.onDestroy();
}
private static class BackgroundTask extends AsyncTask<Void, Void, String> {
private final WeakReference<TextView> textViewReference;
public BackgroundTask(TextView resultTextView) {
this.textViewReference = new WeakReference<>(resultTextView);
}
@Override
protected void onCancelled() {
// Cancel task. Code omitted.
}
@Override
protected String doInBackground(Void... params) {
// Do background work. Code omitted.
return "some string";
}
@Override
protected void onPostExecute(String result) {
TextView view = textViewReference.get();
if (view != null) {
view.setText(result);
}
}
}
}
請(qǐng)注意,在onPostExecute我們要檢查空值宫屠,判斷實(shí)例是否被回收列疗。
最后,再一次運(yùn)行分析器任務(wù)浪蹂,確認(rèn)我們的Activity不再被泄露 抵栈!
匿名類
這種類型的類和內(nèi)部類有同樣的缺點(diǎn)告材,即他們持有外部類的引用。如同內(nèi)部類古劲,一個(gè)匿名類在Activity生命周期之外執(zhí)行或在其他線程執(zhí)行工作時(shí)斥赋,可能會(huì)導(dǎo)致內(nèi)存泄漏。在這個(gè)例子中产艾,我將使用流行的HTTP請(qǐng)求庫(kù)Retrofit執(zhí)行API調(diào)用疤剑,并傳遞響應(yīng)給對(duì)應(yīng)回調(diào)。根據(jù)Retrofit homepage上面例子對(duì)Retrofit進(jìn)行配置闷堡。我會(huì)在Application中持有GitHubService引用隘膘,這不是一個(gè)特別好的設(shè)計(jì),這僅僅服務(wù)于這個(gè)例子的目的杠览。
public class ListenerActivity extends Activity {
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_listener);
textView = (TextView) findViewById(R.id.textView);
GitHubService service = ((LeaksApplication) getApplication()).getService();
service.listRepos("google")
.enqueue(new Callback<List<Repo>>() {
@Override
public void onResponse(Call<List<Repo>> call,
Response<List<Repo>> response) {
int numberOfRepos = response.body().size();
textView.setText(String.valueOf(numberOfRepos));
}
@Override
public void onFailure(Call<List<Repo>> call, Throwable t) {
// Code omitted.
}
});
}
}
這是常見(jiàn)的解決方案弯菊,不應(yīng)該導(dǎo)致任何泄漏。但是踱阿,如果我們?cè)诼龠B接中執(zhí)行這個(gè)例子管钳,分析結(jié)果會(huì)有所不同。請(qǐng)記住扫茅,直到該線程終止蹋嵌,該Activity會(huì)一直被持有育瓜,就像在內(nèi)部類的例子葫隙。
根據(jù)在內(nèi)部類的例子中同樣的推理,我們得出一個(gè)結(jié)論:匿名回調(diào)類是內(nèi)存泄漏的原因躏仇。然而恋脚,正如內(nèi)部類的例子,此代碼包含兩個(gè)問(wèn)題焰手。首先糟描,請(qǐng)求沒(méi)有取消策略。其次书妻,需要消除對(duì)Activity的隱式引用船响。明顯的解決辦法:我們?cè)趦?nèi)部類的例子做了同樣的事情。
public class ListenerActivity extends Activity {
TextView textView;
Call call;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_listener);
textView = (TextView) findViewById(R.id.textView);
GitHubService service = ((LeaksApplication) getApplication()).getService();
call = service.listRepos("google");
call.enqueue(new RepoCallback(textView));
}
@Override
protected void onDestroy() {
call.cancel();
super.onDestroy();
}
private static class RepoCallback implements Callback<List<Repo>> {
private final WeakReference<TextView> resultTextView;
public RepoCallback(TextView resultTextView) {
this.resultTextView = new WeakReference<>(resultTextView);
}
@Override
public void onResponse(Call<List<Repo>> call,
Response<List<Repo>> response) {
TextView view = resultTextView.get();
if (view != null) {
int numberOfRepos = response.body().size();
view.setText(String.valueOf(numberOfRepos));
}
}
@Override
public void onFailure(Call<List<Repo>> call, Throwable t) {
// Code omitted.
}
}
}
根據(jù)上述解決方案躲履,運(yùn)行分析任務(wù)见间,將不會(huì)再有Activity的泄露。
結(jié)論
后臺(tái)任務(wù)獨(dú)立于Activity的生命周期運(yùn)行是一件麻煩事工猜。再加上需要協(xié)調(diào)用戶界面和各種后臺(tái)任務(wù)之間的數(shù)據(jù)流米诉,如果你不小心,那將是一個(gè)災(zāi)難篷帅。所以要知道你在做什么史侣,以及你的代碼是否對(duì)性能有影響拴泌。這些基本準(zhǔn)則是處理Activity的良好開(kāi)端:
- 盡量使用靜態(tài)內(nèi)部類。每個(gè)非靜態(tài)內(nèi)部類將持有一個(gè)外部類的隱式引用惊橱,這可能會(huì)導(dǎo)致不必要的問(wèn)題蚪腐。使用靜態(tài)內(nèi)部類代替非靜態(tài)內(nèi)部類,并通過(guò)弱引用存儲(chǔ)一些必要的生命周期引用税朴。
- 考慮后臺(tái)服務(wù)等手段削茁, Android提供了多種在非主線程工作的方法,如HandlerThread掉房,IntentService和AsyncTask茧跋,它們每個(gè)都有自己的優(yōu)缺點(diǎn)。另外卓囚,Android提供了一些機(jī)制來(lái)傳遞信息給主線程以更新UI瘾杭。譬如,廣播接收器就可以很方便實(shí)現(xiàn)這一點(diǎn)哪亿。
- 不要一味依賴?yán)厥掌鳌?/strong>使用具有垃圾回收功能的語(yǔ)言編碼很容易有這樣的想法:即沒(méi)必要考慮內(nèi)存管理粥烁。我們的示例清楚地表明,并非如此蝇棉。因此讨阻,請(qǐng)確保你分配的資源都被預(yù)期回收。