詳解Android內存泄露及優化方案

一、常見的內存泄露應用場景?

1、單例的不恰當使用

單例是我們開發中最常見和使用最頻繁的設計模式之一,所以如果使用不當就會導致內存泄露。因為單例的靜態特性使得它的生命周期同應用的生命周期一樣長,如果一個對象已經沒有用處瞭,但是單例還持有它的引用,那麼在整個應用程序的生命周期這個對象都不能正常被回收,從而導致內存泄露。

如:

public class App {
    private static App sInstance;

    private Context mContext;

    private App(Context context) {
        this.mContext = context;
    }

    public static App getInstance(Context context) {
        if (sInstance == null) {
            sInstance = new App(context);
        }
        return sInstance;
    }
}

調用getInstance(Context context)方法時傳入的上下文如果為當前活動Activity或者當前服務的Service以及當前fragment的上下文,當他們銷毀時,這個靜態單例sIntance還會持用他們的引用,從而導致當前活動、服務、fragment等對象不能被回收釋放,從而導致內存泄漏。這種上下文的使用很多時候處理不當就會導致內存泄漏,需要我們多註意編碼規范。

2、靜態變量導致內存泄露

靜態變量存儲在方法區,它的生命周期從類加載開始,到整個進程結束。一旦靜態變量初始化後, 它所持有的引用隻有等到進程結束才會釋放。

如下代碼:

public class MainActivity extends AppCompatActivity {
    private static Info sInfo;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (sInfo != null) {
            sInfo = new Info(this);
        }
    }
}

class Info {
    public Info(Activity activity) {
    }
}

Info 作為 Activity 的靜態成員,並且持有 Activity 的引用,但是 sInfo 作為靜態變量,生命周期 肯定比 Activity 長。所以當 Activity 退出後,sInfo 仍然引用瞭 Activity,Activity 不能被回收, 這就導致瞭內存泄露。 在 Android 開發中,靜態持有很多時候都有可能因為其使用的生命周期不一致而導致內存泄露, 所以我們在新建靜態持有的變量的時候需要多考慮一下各個成員之間的引用關系,並且盡量少地 使用靜態持有的變量,以避免發生內存泄露。當然,我們也可以在適當的時候講靜態量重置為 null, 使其不再持有引用,這樣也可以避免內存泄露。

3、非靜態內部類導致內存泄露

非靜態內部類(包括匿名內部類)默認就會持有外部類的引用,當非靜態內部類對象的生命周期 比外部類對象的生命周期長時,就會導致內存泄露。這類內存泄漏很典型的Handler的使用,這麼一說大傢應該就很熟悉瞭吧,大傢都知道怎麼處理這類內存泄漏。

Handler的使用示例:

    private void start() {
        Message msg = Message.obtain();
        msg.what = 1;
        mHandler.sendMessage(msg);
    }

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == 1) {
                // ui更新
            }
        }
    };

Handler 消息機制,mHandler 會作為成員變量保存在發送的消息 msg 中,即 msg 持有 mHandler 的引用,而 mHandler 是 Activity 的非靜態內部類實例,即 mHandler 持有 Activity 的引 用,那麼我們就可以理解為 msg 間接持有 Activity 的引用。msg 被發送後先放到消息隊列 MessageQueue 中,然後等待 Looper 的輪詢處理(MessageQueue 和 Looper 都是與線程相關聯的, MessageQueue 是 Looper 引用的成員變量,而 Looper 是保存在 ThreadLocal 中的)。那麼當 Activity 退出後,msg 可能仍然存在於消息對列 MessageQueue 中未處理或者正在處理,那麼這樣就會導致 Activity 無法被回收,以致發生 Activity 的內存泄露。

如何避免:
1、采用靜態內部類+弱引用的方式

 private static class MyHandler extends Handler {
        private WeakReference<MainActivity> activityWeakReference;

        public MyHandler(MainActivity activity) {
            activityWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = activityWeakReference.get();
            if (activity != null) {
                if (msg.what == 1) {
                    // 做相應邏輯 
                }
            }
        }
    }

mHandler 通過弱引用的方式持有 Activity,當 GC 執行垃圾回收時,遇到 Activity 就會回收並釋 放所占據的內存單元。這樣就不會發生內存泄露瞭。但是 msg 還是有可能存在消息隊列 MessageQueue 中。

2、Activity 銷毀時就將 mHandler 的回調和發送的消息給移除掉。

  @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }

非靜態內部類造成內存泄露還有一種情況就是使用 Thread 或者 AsyncTask異步調用:
如示例:
Thread :

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

AsyncTask:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) { 
                // UI線程處理
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return null;
            }
        }.execute();
    }
}

以上新建的子線程 Thread 和 AsyncTask 都是匿名內部類對象,默認就隱式的持有外部 Activity 的引用, 導致 Activity 內存泄露。要避免內存泄露的話還是需要像上面 Handler 一樣使用采用靜態內部類+弱引用的方式(如上面Hanlder采用靜態內部類+弱引用的方式)。

4、未取消註冊或回調導致內存泄露

比如我們在 Activity 中註冊廣播,如果在 Activity 銷毀後不取消註冊,那麼這個剛播會一直存在 系統中,同上面所說的非靜態內部類一樣持有 Activity 引用,導致內存泄露。因此註冊廣播後在 Activity 銷毀後一定要取消註冊。

this.unregisterReceiver(mReceiver);

在註冊觀察則模式的時候,如果不及時取消也會造成內存泄露。比如使用 Retrofit+RxJava 註冊網絡請求的觀察者回調,同樣作為匿名內部類持有外部引用,所以需要記得在不用或者銷毀的時候 取消註冊。

5、定時器Timer 和 TimerTask 導致內存泄露

當我們 Activity 銷毀的時,有可能 Timer 還在繼續等待執行 TimerTask,它持有 Activity 的引用不 能被回收,因此當我們 Activity 銷毀的時候要立即 cancel 掉 Timer 和 TimerTask,以避免發生內存 泄漏。

6、集合中的對象未清理造成內存泄露

這個比較好理解,如果一個對象放入到 ArrayList、HashMap 等集合中,這個集合就會持有該對象 的引用。當我們不再需要這個對象時,也並沒有將它從集合中移除,這樣隻要集合還在使用(而 此對象已經無用瞭),這個對象就造成瞭內存泄露。並且如果集合被靜態引用的話,集合裡面那 些沒有用的對象更會造成內存泄露瞭。所以在使用集合時要及時將不用的對象從集合 remove,或 者 clear 集合,以避免內存泄漏。

7、資源未關閉或釋放導致內存泄露

在使用 IO、File 流或者 Sqlite、Cursor 等資源時要及時關閉。這些資源在進行讀寫操作時通常都 使用瞭緩沖,如果及時不關閉,這些緩沖對象就會一直被占用而得不到釋放,以致發生內存泄露。 因此我們在不需要使用它們的時候就及時關閉,以便緩沖能及時得到釋放,從而避免內存泄露。

8、動畫造成內存泄露

動畫同樣是一個耗時任務,比如在 Activity 中啟動瞭屬性動畫(ObjectAnimator),但是在銷毀 的時候,沒有調用 cancle 方法,雖然我們看不到動畫瞭,但是這個動畫依然會不斷地播放下去, 動畫引用所在的控件,所在的控件引用 Activity,這就造成 Activity 無法正常釋放。因此同樣要 在 Activity 銷毀的時候 cancel 掉屬性動畫,避免發生內存泄漏。

  @Override
    protected void onDestroy() {
        super.onDestroy();
        mAnimator.cancel();
    }

9、WebView 造成內存泄露

關於 WebView 的內存泄露,因為 WebView在加載網頁後會長期占用內存而不能被釋放,因此我 們在 Activity 銷毀後要調用它的 destory()方法來銷毀它以釋放內存。
另外在查閱 WebView 內存泄露相關資料時看到這種情況: Webview 下面的 Callback 持有 Activity 引用,造成 Webview 內存無法釋放,即使是調用瞭 Webview.destory()等方法都無法解決問題(Android5.1 之後)

最終的解決方案是:在銷毀 WebView 之前需要先將 WebView 從父容器中移除,然後在銷毀 WebView。

   @Override
    protected void onDestroy() {
        super.onDestroy();
        // 先從父控件中移除
        WebView mWebViewContainer.removeView(mWebView);
        mWebView.stopLoading();
        mWebView.getSettings().setJavaScriptEnabled(false);
        mWebView.clearHistory();
        mWebView.removeAllViews();
        mWebView.destroy();
    }

總結

構造單例的時候盡量別用 Activity 的引用;
靜態引用時註意應用對象的置空或者少用靜態引用;
使用靜態內部類+軟引用代替非靜態內部類;
及時取消廣播或者觀察者註冊;
耗時任務、屬性動畫在 Activity 銷毀時記得 cancel;
文件流、Cursor 等資源及時關閉; Activity 銷毀時 WebView 的移除和銷毀。

下一篇繼續:詳解Android內存優化策略

ps:內存泄漏是開發中的一個痛點,需要我們有很好的良好編碼習慣。奧裡給!!!!!!!!!

到此這篇關於詳解Android內存泄露及優化方案一的文章就介紹到這瞭,更多相關Android內存優化內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: