天天看點

記憶體洩露詳解

1.基本介紹

1.1什麼是記憶體洩露?

Android程序中某些對象(垃圾對象)已經沒有使用價值了,但是它們卻可以直接或間接地被引用到,導緻無法被GC回收。無用的對象占據着記憶體空間,使得實際可使用記憶體變小,導緻應用所需要的記憶體超過這個系統配置設定的記憶體限額,這就造成了記憶體溢出而導緻應用卡頓、最終Crash。

1.2為什麼會産生記憶體洩漏?

android的每個應用程式都會使用一個專有的Dalvik虛拟機執行個體來運作,它是由Zygote服務程序孵化出來的,也就是說每個應用程式都是在屬于自己的程序中運作的。Android為不同類型的程序配置設定了不同的記憶體使用上限(在不同的裝置上會因為RAM大小不同而各有差異),如果程式在運作過程中出現了記憶體洩漏的而造成應用程序使用的記憶體超過了這個上限,則會被系統視為記憶體洩漏,進而被kill掉,這使得僅僅自己的程序被kill掉,而不會影響其他程序(如果是system_process等系統程序出問題的話,則會引起系統重新開機)。

1.3記憶體洩露的危害

  1. 導緻使用者手機可用記憶體變少
  2. 程式出現卡頓
  3. 導緻應用莫名退出
  4. 應用程式Force Close
  5. 使用者流失

2.Android中常見的記憶體洩漏

2.1資源使用了為關閉

BroadcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源的使用,應該在Activity銷毀時及時關閉或者登出,否則這些資源将不會被回收,造成記憶體洩漏。

2.2構造Adapter時,沒有使用緩存的 convertView

在BaseAdapter中提供了方法:

public View getView(intposition, View convertView, ViewGroup parent)
           

來向ListView提供每一個item所需要的view對象。初始時ListView會從BaseAdapter中根據目前的螢幕布局執行個體化一定數量的view對象,同時ListView會将這些view對象緩存起來。當向上滾動ListView時,原先位于最上面的list item的view對象會被回收,然後被用來構造新出現的最下面的list item。這個構造過程就是由getView()方法完成的,getView()的第二個形參 View convertView就是被緩存起來的list item的view對象(初始化時緩存中沒有view對象則convertView是null)。

由此可以看出,如果我們不去使用convertView,而是每次都在getView()中重新執行個體化一個View對象的話,即浪費時間,也造成記憶體垃圾,給垃圾回收增加壓力,如果垃圾回收來不及的話,虛拟機将不得不給該應用程序配置設定更多的記憶體,造成不必要的記憶體開支。ListView回收list item的view對象的過程可以檢視:

android.widget.AbsListView.Java–> void addScrapView(View scrap) 方法。

2.3注冊沒取消造成的記憶體洩漏

一些Android程式可能引用我們的Anroid程式的對象(比如注冊機制)。即使我們的Android程式已經結束了,但是别的引用程式仍然還有對我們的Android程式的某個對象的引用,洩漏的記憶體依然不能被垃圾回收。調用registerReceiver後未調用unregisterReceiver。

比如:假設我們希望在鎖屏界面(LockScreen)中,監聽系統中的電話服務以擷取一些資訊(如信号強度等),則可以在LockScreen中定義一個 PhoneStateListener的對象,同時将它注冊到TelephonyManager服務中。對于LockScreen對象,當需要顯示鎖屏界面的時候就會建立一個LockScreen對象,而當鎖屏界面消失的時候LockScreen對象就會被釋放掉。

但是如果在釋放 LockScreen對象的時候忘記取消我們之前注冊的PhoneStateListener對象,則會導緻LockScreen無法被垃圾回收。如果不斷的使鎖屏界面顯示和消失,則最終會由于大量的LockScreen對象沒有辦法被回收而引起OutOfMemory,使得system_process 程序挂掉。

雖然有些系統程式,它本身好像是可以自動取消注冊的(當然不及時),但是我們還是應該在我們的程式中明确的取消注冊,程式結束時應該把所有的注冊都取消掉

2.4單例造成的記憶體洩漏

單例模式非常受開發者的喜愛,不過使用的不恰當的話也會造成記憶體洩漏,由于單例的靜态特性使得單例的生命周期和應用的生命周期一樣長,這就說明了如果一個對象已經不需要使用了,而單例對象還持有該對象的引用,那麼這個對象将不能被正常回收,這就導緻了記憶體洩漏。

建立這個單例的時候,由于需要傳入一個Context,是以這個Context的生命周期的長短至關重要:

1. 傳入的是Application的Context:這将沒有任何問題,因為單例的生命周期和Application的一樣長

2. 傳入的是Activity的Context:當這個Context所對應的Activity退出時,由于該Context和Activity的生命周期一樣長(Activity間接繼承于Context),是以目前Activity退出時它的記憶體并不會被回收,因為單例對象持有該Activity的引用。

  1. 執行個體代碼:
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;
    }
}
           

4.解決方案

public class AppManager {
    private static AppManager instance;
    private Context context;

    private AppManager(Context context) {
        this.context = context.getApplicationContext();
    }

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

這樣不管傳入什麼Context最終将使用Application的Context,而單例的生命周期和應用的一樣長,這樣就防止了記憶體洩漏

2.5非靜态内部類建立靜态執行個體造成的記憶體洩漏

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

public class MainActivity extends Activity {
    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,請使用ApplicationContext

2.6 Handler造成的記憶體洩漏

這種建立Handler的方式會造成記憶體洩漏,由于mHandler是Handler的非靜态匿名内部類的執行個體,是以它持有外部類Activity的引用,我們知道消息隊列是在一個Looper線程中不斷輪詢處理消息,那麼當這個Activity退出時消息隊列中還有未處理的消息或者正在處理消息,而消息隊列中的Message持有mHandler執行個體的引用,mHandler又持有Activity的引用,是以導緻該Activity的記憶體資源無法及時回收,引發記憶體洩漏

public class MainActivity extends Activity {

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        loadData();
    }

    private void loadData(){
       //...request
       Message message = Message.obtain();
      mHandler.sendMessage(message);
    }
}
           

解決方案:

public class MainActivity extends Activity {
    private MyHandler mHandler = new MyHandler(this);
    private TextView mTextView ;

    private static class MyHandler extends Handler {
        //建立一個靜态Handler内部類,然後對Handler持有的對象使用弱引用
        //這樣在回收時也可以回收Handler持有的對象,這樣雖然避免了Activity洩漏
        private WeakReference<Context> reference;

        public MyHandler(Context context) {
            reference = new WeakReference<>(context);
        }

        @Override
        public void handleMessage(Message msg) {
             MainActivity activity = (MainActivity) reference.get();
             if(activity != null){
                 activity.mTextView.setText("");
             }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTextView = (TextView)findViewById(R.id.textview);
        loadData();
    }

    private void loadData() {
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }

    //Looper線程的消息隊列中還是可能會有待處理的消息,
    //是以我們在Activity的Destroy時或者Stop時應該移除消息隊列中的消息
    @Override
    protected void onDestroy() {
        super.onDestroy();
        //移除消息隊列中所有消息和所有的Runnable
        mHandler.removeCallbacksAndMessages(null);
    }
}
           

建立一個靜态Handler内部類,然後對Handler持有的對象使用弱引用,這樣在回收時也可以回收Handler持有的對象,這樣雖然避免了Activity洩漏,不過Looper線程的消息隊列中還是可能會有待處理的消息,是以我們在Activity的Destroy時或者Stop時應該移除消息隊列中的消息,使用mHandler.removeCallbacksAndMessages(null);是移除消息隊列中所有消息和所有的Runnable。當然也可以使用mHandler.removeCallbacks();或mHandler.removeMessages();來移除指定的Runnable和Message。

2.7線程造成的記憶體洩漏

//——————test1
    new AsyncTask<Void, Void, Void>() {
      @Override
      protected Void doInBackground(Void... params) {
        SystemClock.sleep();
        return null;
      }
    }.execute();
//——————test2
    new Thread(new Runnable() {
      @Override
      public void run() {
        SystemClock.sleep();
      }
    }).start();
           

上面的異步任務和Runnable都是一個匿名内部類,是以它們對目前Activity都有一個

隐式引用。如果Activity在銷毀之前,任務還未完成,那麼将導緻Activity的記憶體資

源無法回收,造成記憶體洩漏。

正确的做法還是使用靜态内部類的方式,如下:

//test1
static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
    private WeakReference<Context> weakReference;

    public MyAsyncTask(Context context) {
        weakReference = new WeakReference<>(context);
    }

    @Override
    protected Void doInBackground(Void... params) {
        SystemClock.sleep();
        return null;
    }

    @Override
    protected void onPostExecute(Void aVoid) {
        super.onPostExecute(aVoid);
        MainActivity activity = (MainActivity) weakReference.get();
        if (activity != null) {
           //...
        }
    }
}

//test2
static class MyRunnable implements Runnable{
    @Override
    public void run() {
        SystemClock.sleep();
    }
}
//——————
new Thread(new MyRunnable()).start();
new MyAsyncTask(this).execute();
           

這樣就避免了Activity的記憶體資源洩漏,當然在Activity銷毀時候也應該取消相應的任

務AsyncTask.cancel(),避免任務在背景執行浪費資源。

2.8集合中對象沒清理造成的記憶體洩漏

Vector v = new Vector();

for (int i = ; i < ; i++) {
    Object o = new Object();
    v.add(o);
    o = null;
}
           

通常把一些對象的引用加入到了集合中,當我們不需要該對象時,并沒有把它的引用從

集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就更

嚴重了。

循環申請Object對象,并将所申請的對象放入一個Vector中,如果僅僅釋放對象本身,

但因為Vector仍然引用該對象,是以這個對象對GC來說是不可回收的。是以,如果對象

加入到Vector後,還必須從Vector中删除,最簡單的方法就是将Vector對象設定為nul