Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 內存洩露總結

Android 內存洩露總結

編輯:關於Android編程

本篇總結了Android開發常見過程中的內存洩漏問題。

集合類洩漏

??集合類如果僅僅有添加元素的方法,而沒有相應的刪除機制,導致內存被占用。如果這個集合類是全局性的變量 (比如類中的靜態屬性,全局性的 map 等即有靜態引用或 final 一直指向它),那麼沒有相應的刪除機制,很可能導致集合所占用的內存只增不減。

單例造成的內存洩漏

由於單例的靜態特性使得其生命周期跟應用的生命周期一樣長,所以如果使用不恰當的話,很容易造成內存洩漏。比如下面一個典型的例子,

public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
    this.context = context;
}
public static AppManager getInstance(Context context) {
        if (instance == null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

這是一個普通的單例模式,當創建這個單例的時候,由於需要傳入一個Context,所以這個Context的生命周期的長短至關重要:

1、如果此時傳入的是 Application 的 Context,因為 Application 的生命周期就是整個應用的生命周期,所以這將沒有任何問題。

2、如果此時傳入的是 Activity 的 Context,當這個 Context 所對應的 Activity 退出時,由於該 Context 的引用被單例對象所持有,其生命周期等於整個應用程序的生命周期,所以當前 Activity 退出時它的內存並不會被回收,這就造成洩漏了。

正確的方式應該改為下面這種方式:

public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
    this.context = context.getApplicationContext();// 使用Application 的context
    }
    public static AppManager getInstance(Context context) {
        if (instance == null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

或者這樣寫,連 Context 都不用傳進來了:

在你的 Application 中添加一個靜態方法,getContext() 返回 Application 的 context,

...

context = getApplicationContext();

...
   /**
     * 獲取全局的context
     * @return 返回全局context對象
     */
    public static Context getContext(){
        return context;
    }

public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager() {
        this.context = MyApplication.getContext();// 使用Application 的context
    }
    public static AppManager getInstance() {
        if (instance == null) {
        instance = new AppManager();
        }
        return instance;
    }
}

非靜態內部類創建靜態實例造成的內存洩漏

有的時候我們可能會在啟動頻繁的Activity中,為了避免重復創建相同的數據資源,可能會出現這種寫法:

      public class MainActivity extends AppCompatActivity {
       private static TestResource mResource = null;
       @Override
       protected void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           setContentView(R.layout.activity_main);
           if(mManager == null){
           mManager = new TestResource();
           }
           //...
       }
           class TestResource {
           //...
           }
      }

這樣就在Activity內部創建了一個非靜態內部類的單例,每次啟動Activity時都會使用該單例的數據,這樣雖然避免了資源的重復創建,不過這種寫法卻會造成內存洩漏,因為非靜態內部類默認會持有外部類的引用,而該非靜態內部類又創建了一個靜態的實例,該實例的生命周期和應用的一樣長,這就導致了該靜態實例一直會持有該Activity的引用,導致Activity的內存資源不能正常回收。正確的做法為:

將該內部類設為靜態內部類或將該內部類抽取出來封裝成一個單例,如果需要使用Context,請按照上面推薦的使用Application 的 Context。當然,Application 的 context 不是萬能的,所以也不能隨便亂用,對於有些地方則必須使用 Activity 的 Context,對於Application,Service,Activity三者的Context的應用場景如下:

Context用法

其中: NO1表示 Application 和 Service 可以啟動一個 Activity,不過需要創建一個新的 task 任務隊列。而對於 Dialog 而言,只有在 Activity 中才能創建<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxoMiBpZD0="匿名內部類">匿名內部類

android開發經常會繼承實現Activity/Fragment/View,此時如果你使用了匿名類,並被異步線程持有了,那要小心了,如果沒有任何措施這樣一定會導致洩露

    public class MainActivity extends Activity {
        ...
        Runnable ref1 = new MyRunable();
        Runnable ref2 = new Runnable() {
            @Override
            public void run() {

            }
        };
         ...
    }

ref1和ref2的區別是,ref2使用了匿名內部類。我們來看看運行時這兩個引用的內存:

引用

可以看到,ref1沒什麼特別的。

但ref2這個匿名類的實現對象裡面多了一個引用:

this$0這個引用指向MainActivity.this,也就是說當前的MainActivity實例會被ref2持有,如果將這個引用再傳入一個異步線程,此線程和此Acitivity生命周期不一致的時候,就造成了Activity的洩露。

Handler 造成的內存洩漏

??Handler 的使用造成的內存洩漏問題應該說是最為常見了,很多時候我們為了避免 ANR 而不在主線程進行耗時操作,在處理網絡任務或者封裝一些請求回調等api都借助Handler來處理,但 Handler 不是萬能的,對於 Handler 的使用代碼編寫一不規范即有可能造成內存洩漏。另外,我們知道 Handler、Message 和 MessageQueue 都是相互關聯在一起的,萬一 Handler 發送的 Message 尚未被處理,則該 Message 及發送它的 Handler 對象將被線程 MessageQueue 一直持有。

由於 Handler 屬於 TLS(Thread Local Storage) 變量, 生命周期和 Activity 是不一致的。因此這種實現方式一般很難保證跟 View 或者 Activity 的生命周期保持一致,故很容易導致無法正確釋放。

舉個例子:

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

        // Post a message and delay its execution for 10 minutes.
            mLeakyHandler.postDelayed(new Runnable() {
              @Override
              public void run() { /* ... */ }
            }, 1000 * 60 * 10);

            // Go back to the previous Activity.
            finish();
            }
    }

在該 SampleActivity 中聲明了一個延遲10分鐘執行的消息 Message,mLeakyHandler 將其 push 進了消息隊列 MessageQueue 裡。當該 Activity 被 finish() 掉時,延遲執行任務的 Message 還會繼續存在於主線程中,它持有該 Activity 的 Handler 引用,所以此時 finish() 掉的 Activity 就不會被回收了從而造成內存洩漏(因 Handler 為非靜態內部類,它會持有外部類的引用,在這裡就是指 SampleActivity)。

修復方法:在 Activity 中避免使用非靜態內部類,比如上面我們將 Handler 聲明為靜態的,則其存活期跟 Activity 的生命周期就無關了。同時通過弱引用的方式引入 Activity,避免直接將 Activity 作為 context 傳進去,見下面代碼:

public class SampleActivity extends Activity {

  /**
   * Instances of static inner classes do not hold an implicit
   * reference to their outer class.
   */
  private static class MyHandler extends Handler {
    private final WeakReference mActivity;

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

    @Override
    public void handleMessage(Message msg) {
      SampleActivity activity = mActivity.get();
      if (activity != null) {
        // ...
      }
    }
  }

  private final MyHandler mHandler = new MyHandler(this);

  /**
   * Instances of anonymous classes do not hold an implicit
   * reference to their outer class when they are "static".
   */
  private static final Runnable sRunnable = new Runnable() {
      @Override
      public void run() { /* ... */ }
  };

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

    // Post a message and delay its execution for 10 minutes.
    mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

    // Go back to the previous Activity.
    finish();
  }
}

綜述,即推薦使用靜態內部類 + WeakReference 這種方式。每次使用前注意判空。

前面提到了 WeakReference,所以這裡就簡單的說一下 Java 對象的幾種引用類型。

Java對引用的分類有 Strong reference, SoftReference, WeakReference, PhatomReference 四種。

reference

在Android應用的開發中,為了防止內存溢出,在處理一些占用內存大而且聲明周期較長的對象時候,可以盡量應用軟引用和弱引用技術。

軟/弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。利用這個隊列可以得知被回收的軟/弱引用的對象列表,從而為緩沖器清除已失效的軟/弱引用。

假設我們的應用會用到大量的默認圖片,比如應用中有默認的頭像,默認游戲圖標等等,這些圖片很多地方會用到。如果每次都去讀取圖片,由於讀取文件需要硬件操作,速度較慢,會導致性能較低。所以我們考慮將圖片緩存起來,需要的時候直接從內存中讀取。但是,由於圖片占用內存空間比較大,緩存很多圖片需要很多的內存,就可能比較容易發生OutOfMemory異常。這時,我們可以考慮使用軟/弱引用技術來避免這個問題發生。以下就是高速緩沖器的雛形:

首先定義一個HashMap,保存軟引用對象。

private Map > imageCache = new HashMap > ();

再來定義一個方法,保存Bitmap的軟引用到HashMap。

這裡寫圖片描述

使用軟引用以後,在OutOfMemory異常發生之前,這些緩存的圖片資源的內存空間可以被釋放掉的,從而避免內存達到上限,避免Crash發生。

如果只是想避免OutOfMemory異常的發生,則可以使用軟引用。如果對於應用的性能更在意,想盡快回收一些占用內存比較大的對象,則可以使用弱引用。

另外可以根據對象是否經常使用來判斷選擇軟引用還是弱引用。如果該對象可能會經常使用的,就盡量用軟引用。如果該對象不被使用的可能性更大些,就可以用弱引用。

ok,繼續回到主題。前面所說的,創建一個靜態Handler內部類,然後對 Handler 持有的對象使用弱引用,這樣在回收時也可以回收 Handler 持有的對象,但是這樣做雖然避免了 Activity 洩漏,不過 Looper 線程的消息隊列中還是可能會有待處理的消息,所以我們在 Activity 的 Destroy 時或者 Stop 時應該移除消息隊列 MessageQueue 中的消息。

下面幾個方法都可以移除 Message:

public final void removeCallbacks(Runnable r);

public final void removeCallbacks(Runnable r, Object token);

public final void removeCallbacksAndMessages(Object token);

public final void removeMessages(int what);

public final void removeMessages(int what, Object object);

盡量避免使用 static 成員變量

如果成員變量被聲明為 static,那我們都知道其生命周期將與整個app進程生命周期一樣。

這會導致一系列問題,如果你的app進程設計上是長駐內存的,那即使app切到後台,這部分內存也不會被釋放。按照現在手機app內存管理機制,占內存較大的後台進程將優先回收,如果此app做過進程保活,那會造成app在後台頻繁重啟。當手機安裝了你參與開發的app以後一夜時間手機被消耗空了電量、流量,你的app不得不被用戶卸載或者靜默。

這裡修復的方法是:

不要在類初始時初始化靜態成員。可以考慮lazy初始化。 架構設計上要思考是否真的有必要這樣做,盡量避免。如果架構需要這麼設計,那麼此對象的生命周期你有責任管理起來。

避免 override finalize()

1、finalize 方法被執行的時間不確定,不能依賴與它來釋放緊缺的資源。時間不確定的原因是: 虛擬機調用GC的時間不確定 Finalize daemon線程被調度到的時間不確定

2、finalize 方法只會被執行一次,即使對象被復活,如果已經執行過了 finalize 方法,再次被 GC 時也不會再執行了,原因是:

含有 finalize 方法的 object 是在 new 的時候由虛擬機生成了一個 finalize reference 在來引用到該Object的,而在 finalize 方法執行的時候,該 object 所對應的 finalize Reference 會被釋放掉,即使在這個時候把該 object 復活(即用強引用引用住該 object ),再第二次被 GC 的時候由於沒有了 finalize reference 與之對應,所以 finalize 方法不會再執行。

3、含有Finalize方法的object需要至少經過兩輪GC才有可能被釋放。

資源未關閉造成的內存洩漏

對於使用了BraodcastReceiver,ContentObserver,File,游標 Cursor,Stream,Bitmap等資源的使用,應該在Activity銷毀時及時關閉或者注銷,否則這些資源將不會被回收,造成內存洩漏。

一些不良代碼造成的內存壓力

有些代碼並不造成內存洩露,但是它們,或是對沒使用的內存沒進行有效及時的釋放,或是沒有有效的利用已有的對象而是頻繁的申請新內存。

比如: Bitmap 沒調用 recycle()方法,對於 Bitmap 對象在不使用時,我們應該先調用 recycle() 釋放內存,然後才它設置為 null. 因為加載 Bitmap 對象的內存空間,一部分是 java 的,一部分 C 的(因為 Bitmap 分配的底層是通過 JNI 調用的 )。 而這個 recyle() 就是針對 C 部分的內存釋放。 構造 Adapter 時,沒有使用緩存的 convertView ,每次都在創建新的 converView。這裡推薦使用 ViewHolder。

總結

??對 Activity 等組件的引用應該控制在 Activity 的生命周期之內; 如果不能就考慮使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部長生命周期的對象引用而洩露。

盡量不要在靜態變量或者靜態內部類中使用非靜態外部成員變量(包括context ),即使要使用,也要考慮適時把外部成員變量置空;也可以在內部類中使用弱引用來引用外部類的變量。

對於生命周期比Activity長的內部類對象,並且內部類中使用了外部類的成員變量,可以這樣做避免內存洩漏:

1.將內部類改為靜態內部類
2. 靜態內部類中使用弱引用來引用外部類的成員變量

Handler 的持有的引用對象最好使用弱引用,資源釋放時也可以清空 Handler 裡面的消息。比如在 Activity onStop 或者 onDestroy 的時候,取消掉該 Handler 對象的 Message和 Runnable.

在 Java 的實現過程中,也要考慮其對象釋放,最好的方法是在不使用某對象時,顯式地將此對象賦值為 null,比如使用完Bitmap 後先調用 recycle(),再賦為null,清空對圖片等資源有直接引用或者間接引用的數組(使用 array.clear() ; array = null)等,最好遵循誰創建誰釋放的原則。

正確關閉資源,對於使用了BroardcastReceiver,ContentObserver,File,游標 Cursor,Stream,Bitmap等資源的使用,應該在Activity銷毀時及時關閉或者注銷。

保持對對象生命周期的敏感,特別注意單例、靜態對象、全局性集合等的生命周期。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved