以下內容為原創,歡迎轉載,轉載請注明
來自天天博客:http://www.cnblogs.com/tiantianbyconan/p/7235616.html
Android內存泄漏你所要知道的一切
寫一個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。
private void registerLocationUpdates(){
mManager = (LocationManager) getSystemService(LOCATION_SERVICE);
mManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
TimeUnit.MINUTES.toMillis(1),
100,
this);
}
你在Activity本身實現了listener接口,因此 LocationManager 持有了一個它的引用。現在你的Activity是時候要銷毀了,Android Framework將會調用它的 onDestroy() 方法,但是垃圾收集器將不能從內存中把這個實例刪除,因為 LocationManager 仍然持有了它的強引用。
解決方案非常簡單。僅僅在 onDestroy() 方法中反注冊掉listener,這個很好實現。這是我們大多數人忘記甚至不知道的。
@Override
public void onDestroy() {
super.onDestroy();
if (mManager != null) {
mManager.removeUpdates(this);
}
}
內部類
內部類在Java中非常常見,由於它的簡潔性,Android開發者經常使用在各種任務中。但是由於不恰當的使用,這些內部類也導致了潛在的內存泄漏。
讓我們再在一個簡單例子的幫助下看看,
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);
}
}
}
這是一個非常簡單的Activity,它在后台(也許是復雜的數據庫查詢或者一個緩慢的網絡請求)啟動了一個耗時任務。在Task完成時,結果被展示在 TextView。看起來一切都很好?
不,當然不是。問題在於非靜態內部類持有一個外部類的隱式引用(也就是Activity本身)。現在如果我們旋轉了屏幕或者如果這個耗時的任務比Activity生命長,那么它不會讓垃圾收集器把整個Activity實例從內存回收。一個簡單的錯誤導致了一個巨大的內存泄漏。
但是解決方案還是非常簡單,看了你就明白了,
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);
}
}
}
}
如你所見,我把非靜態內部類改成了靜態內部類,這樣靜態內部類就不會持有任何外部類的隱式引用。但是我們不能通過靜態上下文去訪問外部類的非靜態變量(比如 TextView),所以我們不得不通過構造方法傳遞我們需要的對象引用到內部類。
我強烈推薦使用 WeakReference 包裝這個對象引用來防止進一步的內存泄漏。你需要開始學習關於在Java中各個可用的引用類型。
匿名類
匿名類是很多開發者最喜歡的,因為它們被定義的方式使得用它們編寫代碼非常容易和簡潔。但是根據我的經驗這些匿名類是內存泄漏最常見的原因。
匿名類沒有什么,但是非靜態內部類會由於前面我講到過的同樣的理由引發潛在的內存泄漏。你已經在app的一系列地方用到了它,但是你不知道如果錯誤的使用可能會對你app的性能有嚴重的影響。
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.
}
});
}
}
這里,我們使用了一個非常流行的庫 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.