一個優秀程序員不可避免的問題:內存泄漏

前言

內存泄漏,一個說大不大說下不小的瑕疵。做爲開發者,咱們都很清楚內存泄漏是咱們代碼問題致使的。可是話說回來,泄漏後果會很嚴重嘛?這很差說,若是咱們不泄漏Bitmap這種大內存的對象,那麼修補內存泄漏就像雞肋同樣,「食之無味,棄之惋惜」。 就好比說咱們項目組,近2000w的DAU,只要不明顯影響用戶體驗,一切以上需求爲主…java

可是這做爲一個996福報碼農,不能只挖坑,不填坑,畢竟技術債都是要還的。因此今天我們來聊一聊Android中的內存泄漏。這篇文章總結翻譯了外國友人的一篇文章:原文以下android

techbeacon.com/app-dev-tes…app

1、理論

先上一張圖:ide

解釋一下這張圖,每一個Android(或Java)應用程序都有一個起點(GC Root),從這個點中實例化對象、調用方法。。一些對象直接引用GC Root,另外一些對象又引用了這些對象。所以,造成了引用鏈,就像上圖同樣。所以垃圾收集器從GC Root開始並遍歷直接或間接連接到GC Root的對象。在此過程結束時,脫離GC Root的對象/對象鏈將被回收。性能

接下來我們再想另外一個問題:學習

什麼是內存泄漏?

有了上圖,理解內存泄漏的概念就很簡單,說白了就是:長生命週期對象A持有了短生命週期的對象B,那麼只要A不脫離GC Root的鏈,那麼B對象永遠沒有可能被回收,所以B就泄漏了。優化

有什麼危害?

危害的話,如開篇所說。若是泄漏的內存很小,幾字節,幾kb….對於如今的機器性能,就像星爵打滅霸…「傷害」基本無視。可是若是泄漏的足夠多,普通的GC沒法回收這些泄漏的內存,那麼堆將持續增長,當堆足夠大的時候,就會觸發「stop-the-world」 GC,直接在主線程進行耗時的GC。this

主線程進行耗時操做,每個android開發者都明白這意味着什麼….spa

因此內存泄漏足夠嚴重,其危害仍是很嚴重的。線程

2、實踐

對於咱們平常開發來講,有比較多的場景稍不注意就會存在內存泄漏的風險。讓咱們一塊兒留意一下:

2.一、內部類Inner classes

內部類存在內存泄漏的風險,是一個老生常談的話題。說白了就是由於咱們在new一個內部類時,編譯器會在編譯時讓這個內部類的實例持有外部對象。

這也就是,爲啥咱們的內部類能夠引用到外部類變量、方法的緣由。

上段代碼:

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);
        }
    }
}
複製代碼

你們應該都能看出這裏的問題吧。做爲非靜態內部類的LongRunningTask,會持有BadActivity。而且LongRunningTask是一個長時間任務,也就是說,在這個任務沒有完成時,BadActivity是不會被回收的,所以咱們的BadActivity就被泄漏了。那麼怎麼改呢?

解決原理

首先我不能讓LongRunningTask持有BadActivity。那麼咱們須要使用靜態內部類(static class)。這樣的確不會持有BadActivity,可是問題來了,咱們LongRunningTask不持有BadActivity,也就意味着沒辦法引用到BadActivity中的變量,那麼咱們的更新UI的操做就作不了,也就是說仍是要顯示的傳一個BadActivity中咱們須要的變量進來…可是這樣有形成了一樣的泄漏問題。

所以,咱們須要對傳入的變量使用WeakReference進行包一層。但發生GC的時候,告訴GC收集器「我」能夠被回收。

上改造後的代碼:

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);
            }
        }
    }
}
複製代碼

2.二、匿名類 Anonymous classes

這一類和2.1很相似。本質都是持有外部對象的引用。

上一段很常見的代碼:

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.
                    }
                });
    }
}
複製代碼

2.三、註冊Listener

SingleInstance.setMemoryLeakListener(new OnMemoryLeakListener(){
	//…..
})
複製代碼

這裏寫了段很常見的僞碼,一個單例的對象,register了一個Listener,而且這個Listener被單例的一個成員變量引用。

OK,那麼問題很明顯了。單例做爲靜態變量,確定是一直存在的。而其內部持有了Listener,而Listener做爲一個匿名類,有持有了外部對象的引用。所以這條GC鏈上的全部對象都不會被釋放。

解決也很簡單,適當的時機,在單例中將Listener的引用置爲null。這樣,Listener和單例之間的引用關係斷了,Listener鏈上的全部內容就能夠被正常釋放掉了。也就是我們常作的在onDestory()進行unRegisterListener的操做。

相似不注意的內容,還包括Lambda。不過有一點值得注意的,在Kotlin的Lambda中,若是咱們沒有使用外部對象的變量或者方法,那麼Kotlin在編譯時,這個Lambda是不會持有外部對象的引用的。也算是Kotlin的一些優化吧

2.四、Contexts

上下文的濫用,也是泄漏的大客戶。不過你們針對這類問題應該比較熟悉。

好比:長時間存活的對象,不建議持有Activity的context,而是使用ApplicationContext。若是ApplicationContext沒辦法完成業務,那麼就須要好好考慮一下:這個長時間存活的對象,爲何必需要持有Activity的context。它設計的是否合理,是否它應該是一個長時間存活的對象(好比單例)。

尾聲

關於內存泄漏,仍是須要我們平時多注意,對本身寫的每一行代碼都多思考。畢竟這東西「不是病,但疼起來真要命」。

我是一個應屆生,最近和朋友們維護了一個公衆號,內容是咱們在從應屆生過渡到開發這一路所踩過的坑,以及咱們一步步學習的記錄,若是感興趣的朋友能夠關注一下,一同加油~

我的公衆號:鹹魚正翻身
相關文章
相關標籤/搜索