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

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

5. AsyncTask對象java

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

    可是AsyncTask確實須要額外注意一下。它的泄露原理和前面Handler,Thread泄露的原理差很少,它的生命週期和Activity不必定一致。面試

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

    可是,問題是如何終止?工具

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

    它的說明有這麼一句話:this

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

    是的,就是不靠譜。url

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

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

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

相關文章
相關標籤/搜索