1. 程式人生 > >Android記憶體優化-記憶體洩漏的幾個場景以及解決方式

Android記憶體優化-記憶體洩漏的幾個場景以及解決方式

一.什麼是記憶體洩漏

在Java程式中,如果一個物件沒有利用價值了,正常情況下gc是會對其進行回收的,但是此時仍然有其他引用指向這個活在堆記憶體中的物件,那麼gc就不會認為這個物件是一個垃圾,那麼就不會對其進行回收,所以它會一直活在堆記憶體中佔用記憶體,這就導致了記憶體洩漏。

總結一下,導致記憶體洩漏的原因就是有一些我們永遠不會使用的物件,仍然有引用指向它(當然這是在強引用的情況下),那麼就不滿足gc回收的條件,從而一直活在堆記憶體中導致記憶體洩漏,這樣的物件多了佔用大量記憶體就會導致App發生oom。

舉幾個例子:比如使用EventBus,肯定是要執行register(),那麼在Fragment或Activity finish的時候,一定不要忘記執行unregister()方法。

二.記憶體洩漏的常見場景以及解決方式

1.Activity中的Handler長期持有activity引用導致activity洩漏

public class MainActivity extends AppCompatActivity {

    private final Handler myHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            //doSomething
        }
    };

    @Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); myHandler.postDelayed(new Runnable() { @Override public void run() { //doSomething } },60*10*1000); } }

由於myHandler延時10分鐘就會發送一條訊息,當activity finish之後,延時傳送的訊息會在主執行緒的訊息佇列中存活10分鐘直到被looper拿到然後給到handler處理。此訊息(new Runnable)隱式持有其外部類handler的引用,myHandler又隱式的持有其外部類Activity的引用,直到訊息被處理完之後,這個引用都不會被釋放。因此Activity即使finish,但仍然不會被gc回收。
引用的順序MessageQueue->Message->Runnable->Handler->Activity,從這個引用鏈得到Activity與MessageQueue關聯,所以Activity物件不能被gc回收,從而導致記憶體洩漏。

解決方式:
為了解決Handler隱式的持有外部類引用,我們應當將Handler定義在一個新檔案或在Activity中使用靜態內部類。因為靜態內部類不會持有外部類的引用,這樣當Activity finish時,Handler不會持有Activity的引用就不會導致Activity記憶體洩漏。如果需要在Handler內部呼叫外部Activity的方法,正確的做法是讓Handler持有一個Activity的弱引用(WeakReference),這樣當gc掃描的時候,這個弱引用的物件就會被回收。
解決了Handler隱式持有外部類Activity引用,Runnable在之前的程式碼中作為匿名內部類隱式持有Handler引用,所以我們在Activity內部定義一個靜態變數引用Runnable類,這是因為匿名類的靜態例項不會隱式持有他們外部類的引用。

public class MainActivity extends AppCompatActivity {

    private final MyHandler mHandler = new MyHandler(this);

    private static Runnable sRunnable = new Runnable() {
        @Override
        public void run() {
            //doSomething
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mHandler.postDelayed(sRunnable, 60 * 10);

        this.finish();
    }

    private static class MyHandler extends Handler {

        private final WeakReference<MainActivity> mActivity;

        public MyHandler(MainActivity activity) {
            this.mActivity = new WeakReference<MainActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = mActivity.get();

            if (activity != null) {
                //doSomething
            }

        }
    }
}

或者

我們也可以這樣做:
在Activity的onDestroy方法中幹掉handler中所有的callback和message:

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

2.非靜態匿名內部類造成記憶體洩漏

在Android中最常見的操作就是當有耗時操作的時候我們不能在主執行緒執行這些操作,否則有可能造成ANR,主執行緒主要是UI操作的主戰場。
比如網路請求或者資料庫查詢這些耗時操作我們需要自己另外開啟執行緒,在子執行緒中執行這些耗時操作。當我們需要開啟的子執行緒比較少的時候,直接new Thread(Runnable)就可以了。如果你經常這樣做的話就說明你沒有注意到有可能會產生記憶體洩漏的問題。
如果Activity結束了,而Thread還在跑,同樣會導致Activity記憶體洩漏,這是因為new Thread作為非靜態內部類物件都會隱式持有一個外部類物件的引用,我們所建立的執行緒就是Activity中的一個內部類,持有Activity物件的引用,所以當Activity 結束了,而子執行緒還在跑就會導致Activity記憶體洩漏。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        testThread();
    }

    private void testThread() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    SystemClock.sleep(1000);
                }
            }
        }).start();
    }

}

new Thread()是匿名內部類,且非靜態。所以會隱式持有外部類的一個引用,只要非靜態匿名類物件沒有被回收,Activity就不會被回收。

解決方式:
同樣把Thread定義為靜態的內部類,這樣就不會持有外部類的引用。

3.單例+依賴注入

LeakActivity.java

public class LeakActivity extends AppCompatActivity {

    private TestManager testManager = TestManager.getInstance();
    private MyListener listener=new MyListener() {
        @Override
        public void doSomeThing() {}
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        testManager.registerListener(listener);
    }

}

TestManager.java

public class TestManager {

    private static final TestManager INSTANCE = new TestManager();
    private MyListener listener;

    public static TestManager getInstance() {
        return INSTANCE;
    }

    public void registerListener(MyListener listener) {
        this.listener = listener;
    }
    public void unregisterListener() {
        listener = null;
    }
}

interface MyListener {
    void doSomeThing();
}

在LeakActivity中TestManager.getInstance()建立物件例項,TestManager中採用單例模式返回一個Testmanager例項變數。
引用鏈:TestManager->listener->Activity

TestManager中的例項變數是static靜態變數,靜態變數和類的生命週期是一樣的。類載入的時候,靜態變數就被載入,類銷燬時,靜態變數也會隨之銷燬。
因為INSTANCE是一個單例,所以和app的生命週期是一樣的。當app程序銷燬時,堆記憶體中的INSTANCE物件才會被釋放,INSTANCE的生命週期非常的長。
而又可以看到程式碼中Activity裡面建立了listener非靜態內部類,所以listener就持有外部類Activity的引用。隨著testManager.registerListener(listener)執行,TestManager中的listener就持有Activity中listener物件,由此形成了一個引用鏈。關鍵在於INSTANCE是一個靜態變數,往往Activity finish的時候,INSTANCE還活著,而INSTANCE依然持有Activity的引用,所以造成了Activity記憶體洩漏。

所以,解決方式:要在Activity的onDestroy()方法中登出註冊的listener

@Override
    protected void onDestroy() {
        testManager.unregisterListener();
        super.onDestroy();
    }

將TestManager中listener與Activity中的listener關聯斷開。

三.總結

出現記憶體洩露的主要原因是生命週期的不一致造成的:在Android中,長時間執行的任務和Acyivity生命週期進行協調會有點困難,如果你不加以小心的話會導致記憶體洩漏。
記憶體洩漏的主要原因在於一個生命週期長的東西間接引用了一個生命週期短的東西,會造成生命週期短的東西無法被回收。反過來,如果是一個生命週期短的東西引用了一個生命週期長的東西,是不會影響生命週期短的東西被回收的。
  物件都是有生命週期的,物件的生命週期有的是程序級別的,有的是Activity所在的生命週期,隨Activity消亡;有的是Service所在的生命週期,隨Service消亡。很多情況下判斷物件是否合理存在的一個很重要的理由就是它實際的生命週期是否符合它本來的生命週期。很多Memory Leak的發生,很大程度上都是生命週期的錯配,本來在隨Activity銷燬的物件變成了程序級別的物件,Memory Leak就無法避免了。

四.避免記憶體洩漏的一些技巧

  1. 使用靜態內部類/匿名類,不要使用非靜態內部類/匿名類.非靜態內部類/匿名類會隱式的持有外部類的引用,外部類就有可能發生洩漏。而靜態內部類/匿名類不會隱式的持有外部類引用,外部類會以正常的方式回收,如果你想在靜態內部類/匿名類中使用外部類的屬性或方法時,可以顯示的持有一個弱引用。
  2. 不要以為Java永遠會幫你清理回收正在執行的threads.在上面的程式碼中,我們很容易誤以為當Activity結束銷燬時會幫我們把正在執行的thread也結束回收掉,但事情永遠不是這樣的!Java threads會一直存在,只有當執行緒執行完成或被殺死掉,執行緒才會被回收。所以我們應該養成為thread設定退出邏輯條件的習慣。
  3. 適當的考慮下是否應該使用執行緒.Android應用框架設計了許多的類來簡化執行後臺任務,我們可以使用與Activity生命週期相關聯的Loaders來執行簡短的後臺查詢任務。如果一個執行緒不依賴與Activity,我們還可以使用Service來執行後臺任務,然後用BroadcastReceiver來向Activity報告結果。另外需要注意的是本文討論的thread同樣使用於AsyncTasks,AsyncTask同樣也是由執行緒來實現,只不過使用了Java5.0新增併發包中的功能,但同時需要注意的是根據官方文件所說,AsyncTask適用於執行一些簡短的後臺任務。
  4. 頻繁的使用static關鍵字修飾
    很多初學者非常喜歡用static類static變數,宣告賦值呼叫都簡單方便。由於static宣告變數的生命週期其實是和APP的生命週期一樣的(程序級別)。大量的使用的話,就會佔據記憶體空間不釋放,積少成多也會造成記憶體的不斷開銷,直至掛掉。static的合理使用一般用來修飾基本資料型別或者輕量級物件,儘量避免修復集合或者大物件,常用作修飾全域性配置項、工具類方法、內部類。
  5. BitMap隱患
    Bitmap的不當處理極可能造成OOM,絕大多數情況應用程式OOM都是因這個原因出現的。Bitamp點陣圖是Android中當之無愧的胖子,所以在操作的時候必須小心。
    及時釋放recycle。由於Dalivk並不會主動的去回收,需要開發者在Bitmap不被使用的時候recycle掉。
    設定一定的壓縮率。需求允許的話,應該去對BItmap進行一定的縮放,通過BitmapFactory.Options的inSampleSize屬性進行控制。如果僅僅只想獲得Bitmap的屬性,其實並不需要根據BItmap的畫素去分配記憶體,只需在解析讀取Bmp的時候使用BitmapFactory.Options的inJustDecodeBounds屬性。
    最後建議大家在載入網路圖片的時候,使用軟引用或者弱引用並進行本地快取,推薦使用android-universal-imageloader或者xUtils。
  6. 引用地獄
    Activity中生成的物件原則上是應該在Activity生命週期結束之後就釋放的。Activity物件本身也是,所以應該儘量避免有appliction程序級別的物件來引用Activity級別的物件,如果有的話也應該在Activity結束的時候解引用。如不應用applicationContext在Activity中獲取資源。Service也一樣。
    有的時候我們也會為了程式的效率效能把本來是Activity級裡才用的資源提升到程序級別,比如ImageCache,或者其它DataManager等。
    我只能說,空間和時間是相對的,有的時候需要犧牲時間換取空間,有的時候需要犧牲空間換取時間。記憶體是空間的存在,效能是時間的存在。完美的程式是在一定條件下的完美。
  7. BroadCastReceiver、Service 解綁
    繫結廣播和服務,一定要記得在不需要的時候給解綁。
  8. handler 清理
    在Activity的onDestroy方法中呼叫
    handler.removeCallbacksAndMessages(null);
    取消所有的訊息的處理,包括待處理的訊息;
  9. Cursor及時關閉
    在查詢SQLite資料庫時,會返回一個Cursor,當查詢完畢後,及時關閉,這樣就可以把查詢的結果集及時給回收掉。
  10. I/O流
    I/O流操作完畢,讀寫結束,記得關閉。
  11. 執行緒
    執行緒不再需要繼續執行的時候要記得及時關閉,開啟執行緒數量不易過多,一般和自己機器核心數一樣最好,推薦開啟執行緒的時候,使用執行緒池。執行緒生命週期要跟activity同步。
  12. 網路請求也是執行緒操作的,也應該與activity生命週期同步,在onDestroy的時候cancle掉請求。