Android內存泄漏 ——檢測、解決和避免

albertq7f3 8年前發布 | 53K 次閱讀 Android Android開發 移動開發

作為開發人員,在我們的日常開發中,為了構建更好的應用程序,我們需要考慮很多事情以保證應用運行在正軌上,其中之一是要確保我們的應用程序不會崩潰。應用崩潰的一個常見原因是內存泄漏。這方面的問題可以以各種形式表現出來。在大多數情況下,我們看到內存使用率穩步上升,直到應用程序不能分配更多的資源,并不可避免地崩潰。在Java中這往往導致一個OutOfMemoryException異常被拋出。在某些罕見的情況下,泄露的類甚至可以逗留很長時間來接收已注冊的回調,這會導致一些非常奇怪的錯誤,并往往拋出臭名昭著的IllegalStateException異常

為了幫助他人在代碼分析上減少花費時間,我將介紹內存泄漏的幾個例子,闡述在Android Studio中如何檢查它們,當然最重要的是如何將其解決。

聲明

在這篇文章中的代碼示例的目的是為了促進大家對內存管理有更深的了解,特別是在java。其通用的體系結構,線程管理和代碼示例的 HTTP 請求處理在真實的生產環境并不是理想的,這些示例僅僅為了說明一個問題:在Android中,內存泄漏是一件要考慮的事情。

監聽器注冊

這真的不應該是個問題,但我經常看到各種注冊方法的調用,但他們對應的注銷方法卻無處可尋。這是泄漏的潛在來源,因為這些方法明確設計成互相抵消。如果沒有調用注銷方法,被引用的對象已經被終止后,監聽實例可能會持有該對象很長的時間,從而導致泄漏內存。在Android中,如果該對象是一個Activity對象,是特別麻煩的,因為他們往往擁有大量的數據。讓我告訴你,可能是什么樣子。

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
}

在這個例子中,我們讓Android的 LocationManager通知我們位置更新。我們所需要做的就是獲取系統服務本身和設置一個回調來接收更新。在這里,我們在Activity中實現了位置監聽接口,這意味著LocationManager將持有該Activity的引用。現在,如果該設備被旋轉,新的Activity將被創建并取代已經注冊位置更新接口的舊的Activity。由于系統服務存活時間肯定比任何Activity都要長,LocationManager仍然持有以前的Activity的引用,這使GC不可能回收依賴于以前的Activity的資源,從而導致內存泄漏。如果反復旋轉設備,將導致大量的不可回收的Activity填滿內存,最終導致OutOfMemoryException異常

但為了解決內存泄漏,我們首先必須要能夠找到它。幸運的是,Android Studio有一個叫做 Android Monitor的內置工具,我們可以用它來 觀察除應用內存使用情況。我們需要做的僅僅是打開Android Monitor 并轉到對應tab,看看使用了多少內存和內存實時分配情況。

Android內存泄漏 ——檢測、解決和避免

任何導致資源分配的交互都在這里反映出來,使之成為跟蹤應用程序的資源使用情況的理想場所。為了找到內存泄露,當我們懷疑在某個時間點內存被泄露時,我們需要知道在該時間點包含了那些內存。對于這個特殊的例子,我們所要做的就是啟動我們的應用程序,然后旋轉設備一次,然后調用Dump Java Heap操作(在Memory的旁邊,從左邊數起第三個圖標)。這將生成一個HPROF文件,其中包含我們調用該操作時的一個內存快照。幾秒鐘后,Android Studio 會自動打開該文件,給我們更易于分析內存的直觀表示。

我不會去深入有關如何分析巨大的內存堆。相反,我會把你的注意力引導到 Analyzer Tasks(下面截圖中的右上角)。為了檢測上面的例子中引入的內存泄漏,你所需要做的檢測是檢查泄露的Activity(Detect Leaked Activities),點擊播放按鈕然后在Analysis Results下面就會顯示泄露的Activity情況。

Android內存泄漏 ——檢測、解決和避免

如果我們選中泄露的Activity,可以得到一個引用樹,該引用樹可以檢測持有該Activity的引用。通過尋找深度為零的實例,我們發現位置管理器中的實例mListener,是我們的Activity不能被GC回收的原因。回到我們的代碼,我們可以看到,這個引用是由于我們在requestLocationsUpdates方法中設置Activity作為位置更新回調導致的。通過閱讀位置管理器文檔,問題很快變得清晰,為了取消回調設置,我們簡單地調用removeUpdates方法就行了。在我們的例子,因為我們注冊更新是在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
}

重新構建程序并執行與上述相同的內存分析,無論旋轉多少次設備,應該都不會導致Activity泄漏。

內部類

內部類在Java中是一個很常見的數據結構。它們很受歡迎,因為它們可以以這樣的方式來定義:即只有外部類可以實例化它們。很多人可能沒有意識到的是這樣的類會持有外部類的隱式引用。隱式引用很容易出錯,尤其是當兩個類具有不同的生命周期。以下是常見的Android Activity寫法。

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);
        }
    }
}

這種特殊的實現在執行上沒有問題。問題是,它保留內存的時間肯定會超過必要的時間。由于BackgroundTask持有一個AsyncActivity隱式引用并運行在另一個沒有取消策略的線程上,它將保留AsyncActivity在內存中的所有資源連接,直到后臺線程終止運行。在HTTP請求的情況下,這可能需要很長的時間,尤其是在速度較慢的連接。

通過執行相同的步驟,如同前面的示例,并確保長時間運行的后臺任務,我們最終會得到下面的分析結果。

Android內存泄漏 ——檢測、解決和避免

從上面的分析中可以看出,BackgroundTask 確實是這種內存泄漏的罪魁禍首。我們第一要務是使用靜態類的實現方式來消除指向Activity的引用,但這樣我們也不能直接訪問 textView 了。因此我們還需要添加一個構造函數,把textView作為參數傳遞進來。最后,我們需要引入AsyncTask文檔中所述的取消策略。考慮到所有這一切,讓我們看看我們的代碼最終呈現。

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);
        }
    }
}

現在,隱式引用已被消除,我們通過構造函數傳遞相關實例,并在合適的地方取消任務。讓我們再運行分析任務,看看這種改變是否消除了內存泄漏。

Android內存泄漏 ——檢測、解決和避免

看來我們還有一些工作要做。根據前一個例子的經驗,我們可以知道在引用樹中高亮標注的實例導致了Activity泄露。那么這是什么回事?我們看一下它的父節點就可以發現resultTextView持有一個mContext引用,毫無疑問,它就是泄露的Activity的引用。那么如何解決這個問題?我們無法消除resultTextView綁定的context引用,因為我們需要在BackgroundTask中使用resultTextView的引用,以便更新用戶界面。為了解決這個問題,一種簡單的方法是使用WeakReference。我們持有的resultTextView引用是強引用,具有防止GC回收的能力。相反,WeakReference不保證其引用的實例存活。當一個實例最后一個強引用被刪除,GC會把其資源回收,而不管這個實例是否有弱引用。下面是使用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);
            }
        }
    }
}

請注意,在onPostExecute我們要檢查空值,判斷實例是否被回收。

最后,再一次運行分析器任務,確認我們的Activity不再被泄露 !

匿名類

這種類型的類和內部類有同樣的缺點,即他們持有外部類的引用。如同內部類,一個匿名類在Activity生命周期之外執行或在其他線程執行工作時,可能會導致內存泄漏。在這個例子中,我將使用流行的HTTP請求庫Retrofit執行API調用,并傳遞響應給對應回調。根據Retrofit homepage上面例子對Retrofit進行配置。我會在Application中持有GitHubService引用,這不是一個特別好的設計,這僅僅服務于這個例子的目的。

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.
                    }
                });
    }
}

這是常見的解決方案,不應該導致任何泄漏。但是,如果我們在慢速連接中執行這個例子,分析結果會有所不同。請記住,直到該線程終止,該Activity會一直被持有,就像在內部類的例子。

Android內存泄漏 ——檢測、解決和避免

根據在內部類的例子中同樣的推理,我們得出一個結論:匿名回調類是內存泄漏的原因。然而,正如內部類的例子,此代碼包含兩個問題。首先,請求沒有取消策略。其次,需要消除對Activity的隱式引用。明顯的解決辦法:我們在內部類的例子做了同樣的事情。

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.
        }
    }
}

根據上述解決方案,運行分析任務,將不會再有Activity的泄露。

結論

后臺任務獨立于Activity的生命周期運行是一件麻煩事。再加上需要協調用戶界面和各種后臺任務之間的數據流,如果你不小心,那將是一個災難。所以要知道你在做什么,以及你的代碼是否對性能有影響。這些基本準則是處理Activity的良好開端:

  • 盡量使用靜態內部類。每個非靜態內部類將持有一個外部類的隱式引用,這可能會導致不必要的問題。使用靜態內部類代替非靜態內部類,并通過弱引用存儲一些必要的生命周期引用。
  • 考慮后臺服務等手段, Android提供了多種在非主線程工作的方法,如HandlerThreadIntentServiceAsyncTask,它們每個都有自己的優缺點。另外,Android提供了一些機制來傳遞信息給主線程以更新UI。譬如,廣播接收器就可以很方便實現這一點。
  • 不要一味依賴垃圾回收器。使用具有垃圾回收功能的語言編碼很容易有這樣的想法:即沒必要考慮內存管理。我們的示例清楚地表明,并非如此。因此,請確保你分配的資源都被預期回收。


 

閱讀原文

 本文由用戶 albertq7f3 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!