App調試內存泄露之Context篇(下)

   接着《Android學習系列(36)--App調試內存泄露之Context篇(上)》繼續分析。

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%的內存泄露情況了。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章