1. 程式人生 > >APP效能優化系列:記憶體優化-記憶體洩露詳解

APP效能優化系列:記憶體優化-記憶體洩露詳解

  做了較長時間的android開發了,發現其實android應用開發入門容易,但是進階或者成為高階工程師,需要具備的基礎能力還是非常高的:效能優化、記憶體洩露、apk瘦身、熱修復等等,這些都非常的考驗一個人的能力。android成長之路還很長,自己會持續的走下去。本文主要介紹android記憶體洩露方面的知識。其實要真的理解記憶體洩露,需要對JVM、java語言有一定的瞭解,在這個基礎上就比較容易理解本文了。

一.記憶體洩露概念

  在java中,如果一個物件沒有可用價值了,但又被其他引用所指向,那麼這個物件對於gc來說就不是一個垃圾, 所以不會對其進行回收,但是我們認為這應該是個垃圾,應該被gc回收的。這個物件得不到gc的回收, 就會一直存活在堆記憶體中,佔用記憶體,就跟我們說的霸著茅坑不拉屎的道理是一樣的。這樣就導致了記憶體的洩露。

  為什麼會記憶體洩露呢,根本原因就是一個永遠不會被使用的物件,因為一些引用沒有斷開,沒有滿足GC條件,導致不會被回收,這就造成了記憶體洩露。比如在Activity中註冊了一個廣播接收器,但是在頁面關閉的時候進行unRegister,就會出現記憶體溢位的現象。如果我們的java執行很久,而這種記憶體洩露不斷的發生,最後就沒記憶體可用了,最終就是我們看到的OOM錯誤。雖然android的記憶體洩露做到了應用程式級別的洩露(android中的每個應用程式都是獨立執行在單獨程序中的,每個應用程序都由虛擬機器指定了一個記憶體上限值,一旦記憶體佔用值超過這個上限值,就會發生oom錯誤,程序被強制kill掉,kill掉的程序記憶體會被系統回收),但是對於一名開發工程師,絕對不能放過任何的記憶體洩露。

二.出現記憶體洩露的場合及解決方案

1.activity中handler間斷的post訊息,會造成activity洩露

1.1出現場合:

public class SampleActivity extends Activity {

  private final Handler mLeakyHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      // ...
    }
  }

  @Override
  protected void onCreate
(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 延時10分鐘傳送一個訊息 mLeakyHandler.postDelayed(new Runnable() { @Override public void run() { } }, 60 * 10 * 1000); } }

1.2原因分析:

  當這個Activity被finished後,延時傳送的訊息會繼續在主執行緒的訊息佇列中存活10分鐘,直到他們被處理。這個訊息持有這個Activity的Handler引用,這個Handler有隱式地持有他的外部類(在這個例子中是SampleActivity)。直到訊息被處理前,這個引用都不會被釋放。因此Activity不會被垃圾回收機制回收,洩露他所持有的應用程式資源。注意,第15行的匿名Runnable類也一樣。匿名類的非靜態例項持有一個隱式的外部類引用,因此context將被洩露。

具體的引用順序:
MessageQueue->Message->Runnable->Handler->Activity。所以這就導致當前activity與MessageQueue一直有關聯,導致LeakActivity的物件不能被gc回收,從而導致記憶體洩露。

1.3解決方案:

  為了解決這個問題,Handler的子類應該定義在一個新檔案中或使用靜態內部類。靜態內部類不會隱式持有外部類的引用。所以不會導致它的Activity洩露。如果你需要在Handle內部呼叫外部Activity的方法,那麼讓Handler持有一個Activity的弱引用(WeakReference)以便你不會意外導致context洩露。為了解決我們例項化匿名Runnable類可能導致的記憶體洩露,我們將用一個靜態變數來引用他(因為匿名類的靜態例項不會隱式持有他們外部類的引用)。

public class SampleActivity extends Activity {
    /**
    * 匿名類的靜態例項不會隱式持有他們外部類的引用
    */
    private static final Runnable sRunnable = new Runnable() {
            @Override
            public void run() {
            }
        };

    private final MyHandler mHandler = new MyHandler(this);

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

        // 延時10分鐘傳送一個訊息.
        mHandler.postDelayed(sRunnable, 60 * 10 * 1000);

        // 返回前一個Activity
        finish();
    }

    /**
    * 靜態內部類的例項不會隱式持有他們外部類的引用。
    */
    private static class MyHandler extends Handler {
        private final WeakReference<SampleActivity> mActivity;

        public MyHandler(SampleActivity activity) {
            mActivity = new WeakReference<SampleActivity>(activity);
        }

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

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

  不在一個Activity中使用非靜態內部類, 以防它的生命週期比Activity長。相反,儘量使用持有Activity弱引用的靜態內部類。

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

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

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

  在activity中開啟的執行緒也是一樣,如果activity結束了而執行緒還在跑,一樣會導致activity記憶體洩露,因為”非靜態內部類物件都會持有一個外部類物件的引用”,你建立的執行緒就是activity中的一個內部類,持有activity物件的引用,當activity結束了,但執行緒還在跑,就會導致activity記憶體洩露。

2.1出現場合:

public class MainActivity extends Activity {

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

  private void exampleOne() {
    newThread() {//匿名內部類,非靜態的匿名類會持有外部類的一個隱式引用
      @Override
      publicvoid run() {
        while(true) {
          SystemClock.sleep(1000);
        }
      }
    }.start();
  }

2.2原因分析:

  非靜態的內部類會持有外部類的一個隱式引用,有需要的朋友可以參考下。
  只要非靜態的匿名類物件沒有被回收,MainActivity就不會被回收,MainActivity所關聯的資源和檢視都不會被回收,發生比較嚴重的記憶體洩漏。

2.3解決方案:

  要解決MainActivity的記憶體洩漏問題,只需把非靜態的Thread匿名類定義成靜態的內部類就行了(靜態的內部類不會持有外部類的一個隱式引用)。

3.Thread造成的記憶體洩露

3.1出現場合:具體的場合可見2.

3.2原因分析:

  2中,一旦一個新的Activity建立,那麼就有一個Thread永遠得不到清理回收,發生記憶體洩漏。Threads在Java中是GC roots;意味著Dalvik。Virtual Machine (DVM)會為所有活躍的threads在執行時系統中保持一個硬引用,這會導致threads一直處於執行狀態,垃圾收集器將永遠不可能回收它。

3.3解決方案:

  出於這個原因,我們應當為threads新增一個結束的邏輯,如下程式碼所示:

public class MainActivity extends Activity {
  privateMyThread mThread;

  @Override
  protectedvoid onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    exampleThree();
  }

  privatevoid exampleThree() {
    mThread = new MyThread();
    mThread.start();
  }

  /**
   * Static inner classes don't hold implicit references to their
   * enclosing class, so the Activity instance won't be leaked across
   * configuration changes.
   */
  privatestatic class MyThread extends Thread {
    privateboolean mRunning = false;

    @Override
    publicvoid run() {
      mRunning = true;
      while(mRunning) {
        SystemClock.sleep(1000);
      }
    }

    publicvoid close() {
      mRunning = false;
    }
  }

  @Override
  protectedvoid onDestroy() {
    super.onDestroy();
    mThread.close();
  }
}

  在上述的程式碼中,當Activity結束銷燬時在onDestroy()方法中結束了新建立的執行緒,保證了thread不會發生洩漏。

4.單例+依賴注入

4.1出現場合:

LeakActivity1

public class LeakActivity1 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

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

4.2原因分析:

TestManager——>listener——>activity,TestManager是單例。

  因為TestManager中有static物件,static跟類的生命週期是一樣的,類一載入,static就載入了,類一被銷燬,static才會跟著銷燬(static是存在方法區),這時候jvm會在方法區中儲存變數INSTANCE,然後在堆記憶體開闢空間存放INSTANCE物件,然後把地址值付給INSTANCE變數,使INSTANCE變數就指向這個物件(類似c語言的指標),activity類也是這樣的一種執行關係。
  因為這是一個單例,當app程序被幹掉的時候,堆記憶體中的INSTANCE物件才會被釋放,所以INSTANCE物件的生命週期是很長的,LeakActivity1中,listener持有當前activity的物件,然後testManager.registerListener(listener);執行完,TestManager中的listener就持有activity中listener的物件,而TestManager中的INSTANCE是static的,生命週期長,activity銷燬的時候INSTANCE依然還在,INSTANCE還在,那麼TestManager類中的全域性變數也還是存在的,所以TestManager中的listener變數還在,還一直持有LeakActivity1中的listener物件引用,所以最終是INSTANCE導致LeakActivity1記憶體洩露。

4.3解決方案:

所以,要解決這個問題,可以這樣做,在activity的onDestroy方法中登出註冊的listener.

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

  這樣做後TestManager中的listener不再持有LeakActivity1中的listener物件引用,所以LeakActivity1被銷燬後listener物件也可被回收了。最終,問題又解決了,當然你也可以直接把INSTANCE置null。

三.總結

  其實對於記憶體洩露的分析,用逆向思維來分析更好,從根源一直分析到無法被回收的物件。
  出現記憶體洩露的主要原因是生命週期的不一致造成的:在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. 頁面背景圖
    在佈局和程式碼中設定背景和圖片的時候,如果是純色,儘量使用color;如果是規則圖形,儘量使用shape畫圖;如果稍微複雜點,可以使用9patch圖;如果不能使用9patch的情況下,針對幾種主流解析度的機型進行切圖。
  7. 引用地獄
    Activity中生成的物件原則上是應該在Activity生命週期結束之後就釋放的。Activity物件本身也是,所以應該儘量避免有appliction程序級別的物件來引用Activity級別的物件,如果有的話也應該在Activity結束的時候解引用。如不應用applicationContext在Activity中獲取資源。Service也一樣。
    有的時候我們也會為了程式的效率效能把本來是Activity級裡才用的資源提升到程序級別,比如ImageCache,或者其它DataManager等。
    我只能說,空間和時間是相對的,有的時候需要犧牲時間換取空間,有的時候需要犧牲空間換取時間。記憶體是空間的存在,效能是時間的存在。完美的程式是在一定條件下的完美。
  8. BroadCastReceiver、Service 解綁
    繫結廣播和服務,一定要記得在不需要的時候給解綁。
  9. handler 清理
    在Activity的onDestroy方法中呼叫
    handler.removeCallbacksAndMessages(null);
    取消所有的訊息的處理,包括待處理的訊息;
  10. Cursor及時關閉
    在查詢SQLite資料庫時,會返回一個Cursor,當查詢完畢後,及時關閉,這樣就可以把查詢的結果集及時給回收掉。
  11. I/O流
    I/O流操作完畢,讀寫結束,記得關閉。
  12. 執行緒
    執行緒不再需要繼續執行的時候要記得及時關閉,開啟執行緒數量不易過多,一般和自己機器核心數一樣最好,推薦開啟執行緒的時候,使用執行緒池。執行緒生命週期要跟activity同步。
  13. String/StringBuffer
    當有較多的字元創需要拼接的時候,推薦使用StringBuffer。
  14. 網路請求也是執行緒操作的,也應該與activity生命週期同步,在onDestroy的時候cancle掉請求。
  15. 儘量使用application代替activity和context: Context context = activity.getApplication();這樣就使得context不是指向Activity了,指向全域性的application,這樣就沒記憶體洩露可說了。

五.android中記憶體洩露檢測工具

MAT
LeakCanary

六.一些問題的解答

1.內部類如何不影響外部類的回收呢?
非靜態的內部類會持有外部類的一個隱式引用。
在Java裡面,非靜態的匿名內部類保持了一個對outer class的引用。如果你不夠小心,持有了這個引用,那麼將導致這個Activity一直保留(本應該被垃圾回收器回收)。Activity物件保持了對整個View結構和所有這些Resources的引用,所以你LeakActivity,也就Leak了很多的記憶體。
2.activity onbestrory的時候並沒有被垃圾回收掉
不要認為Java會為你清理runningthread。在上面的例子中,我們會很容易的就想象當用戶離開這個Activity的時候,Activity例項會被垃圾回收器回收,所有在這個Activity中開啟的running thread也會被清理掉。事實上不是這樣的。Java執行緒將會一直存在,直到他們被顯示的關閉或者處理結束,或者被殺掉整個程序。所以,你需要完成可被取消的執行緒機制,在Activity的生命週期的某個地方做適當的處理。