Android學習系列(37)--App除錯記憶體洩露之Context篇(下)

謙虛的天下發表於2014-04-10

    接著《Android學習系列(36)--App除錯記憶體洩露之Context篇(上)》繼續分析。

5. AsyncTask物件

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

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

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

    但是,問題是如何終止?

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

    它的說明有這麼一句話:

// 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是不一定成功的,如果正在執行,它可能會中斷後臺任務。怎麼感覺這話說的這麼不靠譜呢?

    是的,就是不靠譜。

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

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的說明中撂下了一大段英文:

// 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的時候提示出錯:

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

    有兩種解決方案:

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

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

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

  

7. TimerTask物件

    TimerTask物件在和Timer的schedule()方法配合使用的時候極容易造成記憶體洩露。

    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, 1000, 1000);  
  
    } 

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

private void cancelTimer(){  
        if (mTimer != null) {  
            mTimer.cancel();  
            mTimer = null;  
        }  
        if (mTimerTask != null) {  
            mTimerTask.cancel();  
            mTimerTask = null;  
        }
    } 

 

8. Observer物件。

    Observer物件的洩露,也是一種常見、易發現、易解決的洩露型別。

    先看一段正常的程式碼:

    // 其實也非常簡單,只不過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);

    }

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

    private final class SettingsObserver implements Observer {
        public void update(Observable o, Object arg) {
            // todo ...
        }   
    }

     mContentQueryMap = new ContentQueryMap(mCursor, Settings.System.XXX, true, null);
     mContentQueryMap.addObserver(new SettingsObserver());

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

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

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

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

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

// 只是參考,不必死板
/*
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是否退出。

    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%的記憶體洩露情況了。

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

相關文章