[Android]Android內存泄漏你所要知道的一切(翻譯)


如下內容爲原創,歡迎轉載,轉載請註明
來自每天博客:http://www.cnblogs.com/tiantianbyconan/p/7235616.html
html

Android內存泄漏你所要知道的一切

原文:https://blog.aritraroy.in/everything-you-need-to-know-about-memory-leaks-in-android-apps-655f191ca859java

寫一個Android app很簡單,可是寫一個超高品質的內存高效的Android app並不簡單。從個人我的經驗來講,我曾經比較關注和感興趣構建我app的新特性,功能和UI組件。android

我主要傾向於工做在具備更多視覺衝擊力的東西,而不是花時間在沒有人一眼會注意到的東西上面。我開始養成了避免或者給app優化等事情更低優先級的習慣(檢測和修復內存泄漏就是其中的一個)。git

這天然而然致使我承擔起了技術負債,從長遠來看,它開始影響個人應用的性能和質量。我滿滿地改變了個人心態,比起去年我更多地以「性能爲重點」。github

內存泄漏的概念對不少的開發者來講是很是艱鉅的。他們以爲這是困難,耗時,無聊和沒必要要的,但幸運的是,這些都不是真的。一旦你開始深刻,你將絕對會愛上它。數據庫

在本文中,我將嘗試讓這個話題儘量地簡單,這樣即便是新的開發者也能從他們的職業生涯一開始就能構建高質量和高性能的Android apps。編程

垃圾收集器是你的朋友,但不一直是

Java是一個強大的語言。在Android中,咱們不會(有時候咱們會)像 C 或者 C++ 那樣去寫代碼來讓咱們本身去管理整個內存分配和釋放。c#

如今浮如今我腦中的第一個問題就是,既然Java有了一個內置專用的內存管理系統,它會在咱們不須要時清理內存,那麼爲何咱們還須要關心這個呢。是垃圾收集器不夠完善?api

不,固然不是。垃圾收集器的工做原理就是如此。可是咱們本身編程錯誤有時就會阻止垃圾收集器收集大量沒必要要的內存。緩存

因此基本上,都是咱們本身的錯誤才致使了一切的混亂。垃圾收集器是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.

相關文章
相關標籤/搜索