Android學習系列--App調試內存泄露之Context篇



轉自 http://www.cnblogs.com/qianxudetianxia/p/3655475.html

Context作爲最基本的上下文,承載着Activity,Service等最基本組件。當有對象引用到Activity,並不能被回收釋放,必將造成大範圍的對象無法被回收釋放,進而造成內存泄漏。

下面針對一些常用場景逐一分析。

1. CallBack對象的引用

    先看一段代碼:

1
2
3
4
5
6
7
8
9
@Override
protectedvoid onCreate(Bundle state){
  super.onCreate(state);
   
  TextView label =new TextView(this);
  label.setText("Leaks are bad");
   
  setContentView(label);
}

    大家看看有什麼問題嗎?

    沒問題是吧,繼續看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static Drawable sBackground;
   
@Override
protected void onCreate(Bundle state){
  super.onCreate(state);
   
  TextView label =new TextView(this);
  label.setText("Leaks are bad");
   
  if(sBackground ==null){
    sBackground = getDrawable(R.drawable.large_bitmap);
  }
  label.setBackgroundDrawable(sBackground);
   
  setContentView(label);
}

    有問題嗎?

    哈哈,先Hold住一下,先來說一下android各版本發佈的歷史:

1
2
3
4
5
6
/*
2.2        2010-3-20,Froyo
2.3        2010-12-6, Gingerbread
3.0        2011-2-22, Honeycomb
4.0        2011-10-11 Ice Cream Sandwich
*/

    瞭解源碼的歷史,是很有益於我們分析android代碼的。

    好,開始分析代碼。

    首先,查看setBackgroundDrawable(Drawable background)方法源碼裏面有一行代碼引起我們的注意:

1
2
3
4
5
public void setBackgroundDrawable(Drawable background) {
    // ... ...
    background.setCallback(this);
    // ... ...
}

    所以sBackground對view保持了一個引用,view對activity保持了一個引用。

    當退出當前Activity時,當前Activity本該釋放,但是因爲sBackground是靜態變量,它的生命週期並沒有結束,而sBackground間接保持對Activity的引用,導致當前Activity對象不能被釋放,進而導致內存泄露。

    所以結論是:有內存泄露!

    這是Android官方文檔的例子:http://android-developers.blogspot.com/2009/01/avoiding-memory-leaks.html

    到此結束了嗎?

    我發現網上太多直接抄或者間接抄這篇文章,一搜一大片,並且吸引了大量的Android初學者不斷的轉載學習。

    但是經過本人深入分析Drawable源碼,事情發生了一些變化。

    Android官方文檔的這篇文章是寫於2009年1月的,當時的Android Source至少是Froyo之前的。

    Froyo的Drawable的setCallback()方法的實現是這樣的:

1
2
3
public final void setCallback(Callback cb) {
        mCallback = cb;
}

    在GingerBread的代碼還是如此的。

    但是當進入HoneyComb,也就是3.0之後的代碼我們發現Drawable的setCallback()方法的實現變成了:

1
2
3
public final void setCallback(Callback cb) {
        mCallback = new WeakReference<Callback>(cb);
}

    也就是說3.0之後,Drawable使用了軟引用,把這個泄露的例子問題修復了。(至於軟引用怎麼解決了以後有機會再分析吧)

    所以最終結論是,在android3.0之前是有內存泄露,在3.0之後無內存泄露!

    如果認真比較代碼的話,Android3.0前後的代碼改進了大量類似代碼,前面的Cursor篇裏的例子也是在3.0之後修復了。

    從這個例子中,我們很好的發現了內存是怎麼通過回調泄露的,同時通過官方代碼的update也瞭解到了怎麼修復類似的內存泄露。

 

2. System Service對象

    通過各種系統服務,我們能夠做一些系統設計好的底層功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//ContextImpl.java
@Override
public Object getSystemService(String name) {
    ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
    return fetcher == null null : fetcher.getService(this);
}
 
static {
    registerService(ACCESSIBILITY_SERVICE, new ServiceFetcher() {
            public Object getService(ContextImpl ctx) {
            return AccessibilityManager.getInstance(ctx);
            }});
 
    registerService(CAPTIONING_SERVICE, new ServiceFetcher() {
            public Object getService(ContextImpl ctx) {
            return new CaptioningManager(ctx);
            }});
 
    registerService(ACCOUNT_SERVICE, new ServiceFetcher() {
            public Object createService(ContextImpl ctx) {
            IBinder b = ServiceManager.getService(ACCOUNT_SERVICE);
            IAccountManager service = IAccountManager.Stub.asInterface(b);
            return new AccountManager(ctx, service);
            }});
    // ... ...
}

  這些其實就是定義在Context裏的,按理說這些都是系統的服務,應該都沒問題,但是代碼到了各家廠商一改,事情發生了一些變化。

      一些廠商定義的服務,或者廠商自己修改了一些新的代碼導致系統服務引用了Context對象不能及時釋放,我曾經碰到過Wifi,Storage服務都有內存泄露。

     我們改不了這些系統級應用,我們只能修改自己的應用。

     解決方案就是:使用ApplicationContext代替Context。

     舉個例子吧:

1
2
3
4
// For example
mStorageManager = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
改成:
mStorageManager = (StorageManager) getApplicationContext().getSystemService(Context.STORAGE_SERVICE);

 

3. Handler對象

    先看一段代碼:

1
2
3
4
5
6
public class MainActivity extends QActivity {
        // lint tip: This Handler class should be static or leaks might occur
    class MyHandler extends Handler {
        ... ...
    }
}

    Handler泄露的關鍵點有兩個:

    1). 內部類

    2). 生命週期和Activity不一定一致

    第一點,Handler使用的比較多,經常需要在Activity中創建內部類,所以這種場景還是很多的。

    內部類持有外部類Activity的引用,當Handler對象有Message在排隊,則無法釋放,進而導致Activity對象不能釋放。

    如果是聲明爲static,則該內部類不持有外部Acitivity的引用,則不會阻塞Activity對象的釋放。

    如果聲明爲static後,可在其內部聲明一個弱引用(WeakReference)引用外部類。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MainActivity extends Activity {
    private CustomHandler mHandler;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mHandler = new CustomHandler(this);
    }
 
    static class CustomHandlerextends Handler {
        // 內部聲明一個弱引用,引用外部類
        private WeakReference<MainActivity > activityWeakReference;
        public MyHandler(MyActivity activity) {
            activityWeakReference= new WeakReference<MainActivity >(activity);
        }
                // ... ...   
    }
}

    第二點,其實不單指內部類,而是所有Handler對象,如何解決上面說的Handler對象有Message在排隊,而不阻塞Activity對象釋放?

    解決方案也很簡單,在Activity onStop或者onDestroy的時候,取消掉該Handler對象的Message和Runnable。

    通過查看Handler的API,它有幾個方法:removeCallbacks(Runnable r)和removeMessages(int what)等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 一切都是爲了不要讓mHandler拖泥帶水
@Override
public void onDestroy() {
    mHandler.removeMessages(MESSAGE_1);
    mHandler.removeMessages(MESSAGE_2);
    mHandler.removeMessages(MESSAGE_3);
    mHandler.removeMessages(MESSAGE_4);
 
    // ... ...
 
    mHandler.removeCallbacks(mRunnable);
 
    // ... ...
}

    上面的代碼太長?好吧,出大招:

1
2
3
4
5
@Override
public void onDestroy() {
    //  If null, all callbacks and messages will be removed.
    mHandler.removeCallbacksAndMessages(null);
}

    有人會問,當Activity退出的時候,我還有好多事情要做,怎麼辦?我想一定有辦法的,比如用Service等等.

 

4. Thread對象

    同Handler對象可能造成內存泄露的原理一樣,Thread的生命週期不一定是和Activity生命週期一致。

    而且因爲Thread主要面向多任務,往往會造成大量的Thread實例。

    據此,Thread對象有2個需要注意的泄漏點:

    1). 創建過多的Thread對象

    2). Thread對象在Activity退出後依然在後臺執行

    解決方案是:

    1). 使用ThreadPoolExecutor,在同時做很多異步事件的時候是很常用的,這個不細說。

    2). 當Activity退出的時候,退出Thread

    第一點,例子太多,建議大家參考一下afinal中AsyncTask的實現學習。

    第二點,如何正常退出Thread,我在之前的博文中也提到過。示例代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private volatile Thread blinker;
 
public void stop() {
    blinker = null;
}
 
public void run() {
    Thread thisThread = Thread.currentThread();
    while (blinker == thisThread) {
        try {
            thisThread.sleep(interval);
        catch (InterruptedException e){
        }
        repaint();
    }
}

    有人會問,當Activity退出的時候,我還有好多事情要做,怎麼辦?請看上面Handler的分析最後一行。

5. AsyncTask對象

    我N年前去盛大面過一次試,當時面試官極力推薦我使用AsyncTask等系統自帶類去做事情,當然無可厚非。

    但是AsyncTask確實需要額外注意一下。它的泄露原理和前面Handler,Thread泄露的原理差不多,它的生命週期和Activity不一定一致。

    解決方案是:在activity退出的時候,終止AsyncTask中的後臺任務。

    但是,問題是如何終止?

    AsyncTask提供了對應的API:public final boolean cancel (boolean mayInterruptIfRunning)。

    它的說明有這麼一句話:

1
2
// Attempts to cancel execution of this task. This attempt will fail if the task has already completed, already been cancelled, or could not be cancelled for some other reason.
// If successful, and this task has not started when cancel is called, this task should never run. If the task has already started, then the mayInterruptIfRunning parameter determines whether the thread executing this task should be interrupted in an attempt to stop the task.

    cancel是不一定成功的,如果正在運行,它可能會中斷後臺任務。怎麼感覺這話說的這麼不靠譜呢?

    是的,就是不靠譜。

    那麼,怎麼才能靠譜點呢?我們看看官方的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
     protected Long doInBackground(URL... urls) {
         int count = urls.length;
         long totalSize = 0;
         for (int i = 0; i < count; i++) {
             totalSize += Downloader.downloadFile(urls[i]);
             publishProgress((int) ((i / (float) count) * 100));
             // Escape early if cancel() is called
             // 注意下面這行,如果檢測到cancel,則及時退出
             if (isCancelled()) break;
         }
         return totalSize;
     }
 
     protected void onProgressUpdate(Integer... progress) {
         setProgressPercent(progress[0]);
     }
 
     protected void onPostExecute(Long result) {
         showDialog("Downloaded " + result + " bytes");
     }
 }

  官方的例子是很好的,在後臺循環中時刻監聽cancel狀態,防止沒有及時退出。

      爲了提醒大家,google特意在AsyncTask的說明中撂下了一大段英文:

1
// AsyncTask is designed to be a helper class around Thread and Handler and does not constitute a generic threading framework. AsyncTasks should ideally be used for short operations (a few seconds at the most.) If you need to keep threads running for long periods of time, it is highly recommended you use the various APIs provided by the java.util.concurrent pacakge such as Executor, ThreadPoolExecutor and FutureTask.

    可憐我神州大陸幅員遼闊,地大物博,什麼都不缺,就是缺對英語閱讀的敏感。

    AsyncTask適用於短耗時操作,最多幾秒鐘。如果你想長時間耗時操作,請使用其他java.util.concurrent包下的API,比如Executor, ThreadPoolExecutor 和 FutureTask.

    學好英語,避免踩坑!

 

6. BroadcastReceiver對象

    ... has leaked IntentReceiver ... Are you missing a call to unregisterReceiver()?

    這個直接說了,種種原因沒有調用到unregister()方法。

    解決方法很簡單,就是確保調用到unregister()方法

    順帶說一下,我在工作中碰到一種相反的情況,receiver對象沒有registerReceiver()成功(沒有調用到),於是unregister的時候提示出錯:

1
// java.lang.IllegalArgumentException: Receiver not registered ...

    有兩種解決方案:

    方案一:在registerReceiver()後設置一個FLAG,根據FLAG判斷是否unregister()。網上搜到的文章幾乎都這麼寫,我以前碰到這種bug,也是一直都這麼解。但是不可否認,這種代碼看上去確實有點醜陋。

    方案二:我後來無意中聽到某大牛提醒,在Android源碼中看到一種更通用的寫法:

1
2
3
4
5
6
7
8
9
// just sample, 可以寫入工具類
// 第一眼我看到這段代碼,靠,太粗暴了,但是回頭一想,要的就是這麼簡單粗暴,不要把一些簡單的東西搞的那麼複雜。
private void unregisterReceiverSafe(BroadcastReceiver receiver) {
    try {
        getContext().unregisterReceiver(receiver);
    catch (IllegalArgumentException e) {
        // ignore
    }
}

  

7. TimerTask對象

    TimerTask對象在和Timer的schedule()方法配合使用的時候極容易造成內存泄露。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void startTimer(){ 
    if (mTimer == null) { 
        mTimer = new Timer(); 
    
 
    if (mTimerTask == null) { 
        mTimerTask = new TimerTask() { 
            @Override 
            public void run() { 
                // todo
            
        }; 
    
 
    if(mTimer != null && mTimerTask != null 
        mTimer.schedule(mTimerTask, 10001000); 
 
}

  泄露的點是,忘記cancel掉Timer和TimerTask實例。cancel的時機同cursor篇說的,在合適的時候cancel。

1
2
3
4
5
6
7
8
9
10
private void cancelTimer(){ 
        if (mTimer != null) { 
            mTimer.cancel(); 
            mTimer = null
        
        if (mTimerTask != null) { 
            mTimerTask.cancel(); 
            mTimerTask = null
        }
    }

 

8. Observer對象。

    Observer對象的泄露,也是一種常見、易發現、易解決的泄露類型。

    先看一段正常的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 其實也非常簡單,只不過ContentObserver是系統的例子,有必要單獨拿出來提示一下大家,不可掉以輕心
private final ContentObserver mSettingsObserver = new ContentObserver(new Handler()) {
    @Override
    public void onChange(boolean selfChange, Uri uri) {
        // todo
    }
};
 
@Override
public void onStart() {
    super.onStart();
 
    // register the observer
    getContentResolver().registerContentObserver(Settings.Global.getUriFor(
            xxx), false, mSettingsObserver);
}
 
@Override
public void onStop() {
    super.onStop();
 
    // unregister it when stoping
    getContentResolver().unregisterContentObserver(mSettingsObserver);
 
}

  看完示例,我們來看看病例:

1
2
3
4
5
6
7
8
private final class SettingsObserver implements Observer {
    public void update(Observable o, Object arg) {
        // todo ...
    }  
}
 
 mContentQueryMap = new ContentQueryMap(mCursor, Settings.System.XXX, truenull);
 mContentQueryMap.addObserver(new SettingsObserver());

    靠,誰這麼偷懶,把SettingObserver搞個匿名對象傳進去,這可如何是好?

    所以,有些懶是不能偷的,有些語法糖是不能吃的。

    解決方案就是, 在不需要或退出的時候delete這個Observer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private Observer mSettingsObserver;
@Override
public void onResume() {
    super.onResume();
    if (mSettingsObserver == null) {
        mSettingsObserver = new SettingsObserver();
    }  
    mContentQueryMap.addObserver(mSettingsObserver);
}
 
@Override
public void onStop() {
    super.onStop();
    if (mSettingsObserver != null) {
        mContentQueryMap.deleteObserver(mSettingsObserver);
    }  
    mContentQueryMap.close();
}

  注意一點,不同的註冊方法,不同的反註冊方法。

1
2
3
4
5
6
7
8
// 只是參考,不必死板
/*
addCallback             <==>     removeCallback
registerReceiver        <==>     unregisterReceiver
addObserver             <==>     deleteObserver
registerContentObserver <==>     unregisterContentObserver
... ...
*/

 

9. Dialog對象

    android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@438afa60 is not valid; is your activity running?

    一般發生於Handler的MESSAGE在排隊,Activity已退出,然後Handler纔開始處理Dialog相關事情。

    關鍵點就是,怎麼判斷Activity是退出了,有人說,在onDestroy中設置一個FLAG。我很遺憾的告訴你,這個錯誤很有可能還會出來。

    解決方案是:使用isFinishing()判斷Activity是否退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Handler handler = new Handler() {
    public void handleMessage(Message msg) {
        switch (msg.what) {
        case MESSAGE_1:
            // isFinishing == true, 則不處理,儘快結束
            if (!isFinishing()) {
                // 不退出
                // removeDialog()
                // showDialog()
            }  
            break;
        default:
            break;
        }  
        super.handleMessage(msg);
    }  
};

  早完早釋放!

 

10. 其它對象

    以Listener對象爲主,"把自己搭進去了,切記一定要及時把自己放出來"。

 

11. 小結

     結合本文Context篇和前面Cursor篇,我們枚舉了大量的泄露實例,大部分根本原因都是相似的。

     通過分析這些例子後,我們應該能理解APP層90%的內存泄露情況了。

     至於怎麼發現和定位內存泄露,這是另外一個有意思的話題,現在只能說,有方法有工具。


發佈了26 篇原創文章 · 獲贊 19 · 訪問量 22萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章