[Android]Android記憶體洩漏你所要知道的一切(翻譯)
以下內容為原創,歡迎轉載,轉載請註明
來自天天部落格:http://www.cnblogs.com/tiantianbyconan/p/7235616.html
Android記憶體洩漏你所要知道的一切
原文:https://blog.aritraroy.in/everything-you-need-to-know-about-memory-leaks-in-android-apps-655f191ca859
寫一個Android app很簡單,但是寫一個超高品質的記憶體高效的Android app並不簡單。從我的個人經驗來說,我曾經比較關注和感興趣構建我app的新特性,功能和UI元件。
我主要傾向於工作在具有更多視覺衝擊力的東西,而不是花時間在沒有人一眼會注意到的東西上面。我開始養成了避免或者給app優化等事情更低優先順序的習慣(檢測和修復記憶體洩漏就是其中的一個)。
這自然而然導致我承擔起了技術負債,從長遠來看,它開始影響我的應用的效能和質量。我滿滿地改變了我的心態,比起去年我更多地以“效能為重點”。
記憶體洩漏的概念對很多的開發者來說是非常艱鉅的。他們覺得這是困難,耗時,無聊和不必要的,但幸運的是,這些都不是真的。一旦你開始深入,你將絕對會愛上它。
在本文中,我將嘗試讓這個話題儘可能地簡單,這樣即使是新的開發者也能從他們的職業生涯一開始就能構建高質量和高效能的Android apps。
垃圾收集器是你的朋友,但不一直是
Java是一個強大的語言。在Android中,我們不會(有時候我們會)像 C 或者 C++ 那樣去寫程式碼來讓我們自己去管理整個記憶體分配和釋放。
現在浮現在我腦中的第一個問題就是,既然Java有了一個內建專用的記憶體管理系統,它會在我們不需要時清理記憶體,那麼為什麼我們還需要關心這個呢。是垃圾收集器不夠完善?
不,當然不是。垃圾收集器的工作原理就是如此。但是我們自己程式設計錯誤有時就會阻止垃圾收集器收集大量不必要的記憶體。
所以基本上,都是我們自己的錯誤才導致了一切的混亂。垃圾收集器是Java最好的成就之一,它值得被尊重。
垃圾收集器“更多的一點”
在進一步之前,你需要了解一點垃圾收集器的工作原理。它的原理非常簡單,但是它的內部有時候非常複雜。但是不用擔心,我們將主要關注簡單的部分。
每一個Android(或者Java)應用程式都有一個從物件開始獲取例項化、方法被呼叫的起點。所以我們可以認為這個起點是記憶體樹的“root”。一些物件直接保持了一個對“root”的引用,並且從它們中例項化其它物件,保持這些物件的引用等等。
因而,形成了建立記憶體樹的引用鏈。所以,垃圾收集器從GC roots開始,然後直接或間接遍歷物件連結到根。在這個過程的最後,存在一些GC從來沒有訪問到的物件。
這些是你的垃圾(或者dead objects),這些物件就是我們所鍾愛的垃圾收集器有資格去收集的。
到目前為止,這似乎是一個童話故事,但讓我們深入瞭解一下開始真正的樂趣。
Bonus: 如果你希望學習更多關於垃圾收集器,我強烈推薦你看下 這裡 和 這裡 。
那麼現在,什麼是記憶體洩漏呢?
知道現在,你有了一個簡單想法的垃圾收集器,那麼,在Android apps中記憶體管理師怎麼工作的。現在,讓我們關注於更詳細的記憶體洩漏這個話題。
簡單來說,記憶體洩漏發生在當你長時間持有一個已經達到目的的物件。實際的概念就這麼簡單。
每個物件都有它自己的生命,之後它需要說拜拜,然後釋放記憶體。但是如果一些物件持有這個物件(直接或間接),那麼垃圾收集器就無法收集它。這就是我們的朋友,記憶體洩漏。
但是有個好訊息就是你不需要擔心你app中的每一處的記憶體洩漏。並不是所有的記憶體洩漏都會傷害你的app。
有一些洩漏真的非常小(洩漏了幾千位元組的記憶體),有些存在於Android framework本身(是的,你沒看錯),這些你不能也不需要去修復。他們通常對於你的app影響很小並且你可以安全地忽略它們。
但是也存在其它的可以讓你的應用程式崩潰,使它像地獄一樣滯留,並將其逐字縮小。這些是你要關注的東西。
為什麼你真的需要解決記憶體洩漏?
沒有人希望使用一個緩慢的、遲鈍的、吃很多記憶體、每用幾分鐘就會crash的app。對於使用者長時間使用,它真的會建立一個糟糕的體驗,然後你就有永遠失去使用者的可能性。
隨著使用者繼續使用你的app,堆記憶體也不斷地增長,如果你的app中有記憶體洩漏,那麼GC就無法釋放你無用的記憶體。所以你app的堆記憶體就會經常增長,直到達到一個死亡的點,這時將沒有更多的記憶體分配給你的app,從而導致可怕的 OutOfMemoryError 並最終讓你的應用程式崩潰。
你還要必須記住一件事情,垃圾收集器是一個繁重的過程,垃圾收集器跑得越少,對你的app就越好。
App正在被使用,對記憶體保持著增長狀態,一個小的GC將啟動並嘗試清除剛剛死亡的物件。現在這些小的GC同時執行著(在單獨的執行緒上),並且不會減緩你的app(2-5ms的暫停)。
但是如果你的app內部有嚴重的記憶體洩漏的問題,那麼這些小的GC無法去回收這些記憶體,並且堆還在持續增長,從而迫使更大的GC被啟動,這通常是一個"停止世界 的GC",它會暫停整個app主執行緒(大約50-100ms),從而使你的app嚴重滯後,甚至有時幾乎不可用。
所以現在你知道這些記憶體洩漏可能對你的應用程式產生的影響,以及為什麼需要立即修復它們,為使用者提供他們應得的最佳體驗。
怎麼去檢測這些記憶體洩漏?
目前為止,你應該相當信服你需要修復這些隱藏在你app中的記憶體洩漏。但是怎麼去實際檢測它們呢?
不錯的是,對於這點Android Studio提供了一個非常有用且強大的工具,Monitors。這個顯示器不僅展示了記憶體使用,同樣還有網路、CPU、GPU使用(更多資訊檢視這裡)。
當你在使用和除錯你的app時,應該密切關注這個記憶體監視器。記憶體洩漏的第一症狀是當你持續使用你的app時記憶體使用圖表經常增長,並且從不下降,甚至在你切換到後臺後。
Allocation Tracker可以派上用場,你可以使用它來檢查分配給應用程式中不同型別物件的記憶體百分比。
但是這本身還不夠,因為你現在需要使用Dump Java Heap選項來建立一個實際表示給定時間記憶體快照的heap dump。看起來是一個無聊和重複性的工作,對吧?對,它確實是。
我們工程師往往是懶惰的,這點 LeakCanary 來救援了。這個庫隨著你的app一起執行。在需要時dump出記憶體,尋找潛在的記憶體洩漏並且通過一個清晰有用的stack trace來尋找洩漏的根源。
LeakCanary 讓任何人在他們的app中檢測洩漏變得超級簡單。我不能再感謝 Py(來自 Square)寫了如此驚人和拯救了生命的庫了。獎勵!
Bonus: 如果你想詳細學習怎麼充分使用這個庫,看這裡。
一些實際常見的記憶體洩漏情況並怎麼去解決它們
從我們經驗來看,有幾個最常見的可能會導致記憶體洩漏的場景,它們都非常相似,你會在日常的Android開發中遇到這些情況。
一旦你知道這些記憶體洩漏發生在什麼時候,什麼地方,怎麼發生,你就可以更容易對此進行修復。
Unregistered Listeners
有很多場景,你在Activity(或者Fragment)中進行了一個監聽器的註冊,但是忘記把它反註冊掉。如果運氣不好,這個很容易導致一個巨大的記憶體洩漏。一般情況下,這些監聽器是平衡的,所以如果你在某些地方註冊了它,你也需要在那裡反註冊它。
現在我們來看一個簡單的例子。假設你要在你的app中接收到位置的更新,你要做的事就是拿到一個 LocationManager系統服務,然後為位置更新註冊一個listener。
<span style="color:#000000"><code>private void registerLocationUpdates(){
mManager = (LocationManager) getSystemService(LOCATION_SERVICE);
mManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
TimeUnit.MINUTES.toMillis(1),
100,
this);
}</code></span>
你在Activity本身實現了listener介面,因此 LocationManager 持有了一個它的引用。現在你的Activity是時候要銷燬了,Android Framework將會呼叫它的 onDestroy() 方法,但是垃圾收集器將不能從記憶體中把這個例項刪除,因為 LocationManager 仍然持有了它的強引用。
解決方案非常簡單。僅僅在 onDestroy() 方法中反註冊掉listener,這個很好實現。這是我們大多數人忘記甚至不知道的。
<span style="color:#000000"><code>@Override
public void onDestroy() {
super.onDestroy();
if (mManager != null) {
mManager.removeUpdates(this);
}
}</code></span>
內部類
內部類在Java中非常常見,由於它的簡潔性,Android開發者經常使用在各種任務中。但是由於不恰當的使用,這些內部類也導致了潛在的記憶體洩漏。
讓我們再在一個簡單例子的幫助下看看,
<span style="color:#000000"><code>public class BadActivity extends Activity {
private TextView mMessageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_bad_activity);
mMessageView = (TextView) findViewById(R.id.messageView);
new LongRunningTask().execute();
}
private class LongRunningTask extends AsyncTask<Void, Void, String> {
@Override
protected String doInBackground(Void... params) {
return "Am finally done!";
}
@Override
protected void onPostExecute(String result) {
mMessageView.setText(result);
}
}
}</code></span>
這是一個非常簡單的Activity,它在後臺(也許是複雜的資料庫查詢或者一個緩慢的網路請求)啟動了一個耗時任務。在Task完成時,結果被展示在 TextView。看起來一切都很好?
不,當然不是。問題在於非靜態內部類持有一個外部類的隱式引用(也就是Activity本身)。現在如果我們旋轉了螢幕或者如果這個耗時的任務比Activity生命長,那麼它不會讓垃圾收集器把整個Activity例項從記憶體回收。一個簡單的錯誤導致了一個巨大的記憶體洩漏。
但是解決方案還是非常簡單,看了你就明白了,
<span style="color:#000000"><code>public class GoodActivity extends Activity {
private AsyncTask mLongRunningTask;
private TextView mMessageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_good_activity);
mMessageView = (TextView) findViewById(R.id.messageView);
mLongRunningTask = new LongRunningTask(mMessageView).execute();
}
@Override
protected void onDestroy() {
super.onDestroy();
mLongRunningTask.cancel(true);
}
private static class LongRunningTask extends AsyncTask<Void, Void, String> {
private final WeakReference<TextView> messageViewReference;
public LongRunningTask(TextView messageView) {
this.messageViewReference = new WeakReference<>(messageView);
}
@Override
protected String doInBackground(Void... params) {
String message = null;
if (!isCancelled()) {
message = "I am finally done!";
}
return message;
}
@Override
protected void onPostExecute(String result) {
TextView view = messageViewReference.get();
if (view != null) {
view.setText(result);
}
}
}
}</code></span>
如你所見,我把非靜態內部類改成了靜態內部類,這樣靜態內部類就不會持有任何外部類的隱式引用。但是我們不能通過靜態上下文去訪問外部類的非靜態變數(比如 TextView),所以我們不得不通過構造方法傳遞我們需要的物件引用到內部類。
我強烈推薦使用 WeakReference 包裝這個物件引用來防止進一步的記憶體洩漏。你需要開始學習關於在Java中各個可用的引用型別。
匿名類
匿名類是很多開發者最喜歡的,因為它們被定義的方式使得用它們編寫程式碼非常容易和簡潔。但是根據我的經驗這些匿名類是記憶體洩漏最常見的原因。
匿名類沒有什麼,但是非靜態內部類會由於前面我講到過的同樣的理由引發潛在的記憶體洩漏。你已經在app的一系列地方用到了它,但是你不知道如果錯誤的使用可能會對你app的效能有嚴重的影響。
<span style="color:#000000"><code>public class MoviesActivity extends Activity {
private TextView mNoOfMoviesThisWeek;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_movies_activity);
mNoOfMoviesThisWeek = (TextView) findViewById(R.id.no_of_movies_text_view);
MoviesRepository repository = ((MoviesApp) getApplication()).getRepository();
repository.getMoviesThisWeek()
.enqueue(new Callback<List<Movie>>() {
@Override
public void onResponse(Call<List<Movie>> call,
Response<List<Movie>> response) {
int numberOfMovies = response.body().size();
mNoOfMoviesThisWeek.setText("No of movies this week: " + String.valueOf(numberOfMovies));
}
@Override
public void onFailure(Call<List<Movie>> call, Throwable t) {
// Oops.
}
});
}
}</code></span>
這裡,我們使用了一個非常流行的庫 Retrofit 來執行一個網路請求並把結果顯示在 TextView 上。很明顯,Callable 物件持有了一個外部Activity類的引用。
如果這個網路請求執行速度非常慢,並且在呼叫結束之前 Activity 因為某種情況被旋轉了螢幕或者被銷燬,那麼整個Activity例項都會被洩漏。
不管是否必須,使用靜態內部類來代替匿名內部類通常是明智之舉。不是我突然告訴你完全停止使用匿名類,而是你必須要懂得判斷什麼時候能用什麼時候不能用。
Bitmaps
在你app中看到的所有圖片都沒關係,除了 Bitmap ,它包含了影象的整個畫素資料。
這些 bitmaps 物件一般非常重,如果處理不當可能會引發明顯的記憶體洩漏,並最終讓你的app因為 OutOfMemoryError而崩潰。你在app中使用的圖片相關的 bitmap 記憶體 都會由Android Framework 自身自動管理,如果你手動處理 Bitmap,確保在使用後進行 recycle()。
你還必須學會怎麼去正確地管理這些bitmaps,載入大的Bitmap時通過壓縮,以及使用bitmap快取池來儘可能減少記憶體的佔用。這裡 有一個理解 bitmap 處理的很好的資源。
Contexts
另一個相當常見的記憶體洩漏是濫用 context 例項。Context 只是一個抽象類,它有很多類(比如 Activity,Application,Service 等等)繼承它並提供它們自己的功能。
如果你要在 Android 中完成任務,那麼 Context 物件就是你的老闆。
但是這些 contexts 有一些不同之處。非常重要的一點是理解 Activity級別的Context 和 Application級別的Context 之間的區別,分別用在什麼情況下。
在錯誤的地方使用 Activity context 會持有整個 Activity 的引用並引發潛在的記憶體洩漏。這裡有篇很好的文章作為開始。
總結
現在你肯定知道垃圾收集器是怎麼工作的,什麼是記憶體洩漏,它們如何對你的app產生重大的影響。你也學習了怎樣檢測和修復這些記憶體洩漏。
沒有任何藉口,從現在開始讓我們開始構建一個高質量,高效能的 Android app。檢測和修復記憶體洩漏不僅會讓你的app的使用者體驗更好,而且會慢慢地讓你成為一個更好的開發者。
本文最初發表於 TechBeacon.