天天看點

PhoneStateListener 引起的記憶體洩露

由于開了StrictMode,今天看log發現類似這種錯誤:

E/StrictMode: android.os.StrictMode$InstanceCountViolation: class com.XXX.XXX.XXX.XXX.LiveVideoActivity; instances=2; limit=1
E/StrictMode: android.os.StrictMode$InstanceCountViolation: class com.XXX.XXX.XXX.XXX.LiveVideoActivity; instances=3; limit=1
E/StrictMode: android.os.StrictMode$InstanceCountViolation: class com.XXX.XXX.XXX.XXX.LiveVideoActivity; instances=4; limit=1
E/StrictMode: android.os.StrictMode$InstanceCountViolation: class com.XXX.XXX.XXX.XXX.LiveVideoActivity; instances=5; limit=1
……
           

activity的執行個體在穩步增長,本來猜測是有些地方沒有釋放Context導緻,記憶體洩露,檢查了下代碼,把所有可能引起這個問題的地方都處理了,但是還沒是有問題,沒轍了,開始問度娘,問bing(就這個問題的搜尋結果有感 如果通路不了google,建議還是用bing....)終于在 Stack Overflow 找到了同樣的情況。

http://stackoverflow.com/questions/5956132/android-strictmode-instancecountviolation

大神給出了找出記憶體洩露的方法 ,如下:

after backing out from MyActivity and seeing the log message
    make a heap dump (.hprof)
    open it in Eclipse Memory Analyzer
    run OQL: select * from instanceof full.package.name.of.MyActivity
    select all with Ctrl+Click or Shift+Click
    right click and Merge Shortest Path to GC Roots > with all references
           

最終抓到了 hprof檔案 ,分析方法在這篇文章中找到:

http://blog.csdn.net/ccwwff/article/details/7817139

運作cmd打開指令行,cd到\ android-sdk-windows\tools所在目錄,并輸入指令hprof-conv xxxxx.hprof yyyyy.hprof,其中xxxxx.hprof為原始檔案,yyyyy.hprof為轉換過後的檔案。轉換過後的檔案自動放在android-sdk-windows\tools 目錄下。
           

這裡有個問題,其實是在

<pre name="code" class="plain">hprof-conv是在   \ android-sdk-windows\platform-tools   裡
           

最終發現問題出在 PhoneStateListener 上。

原因是,我做了個需求 播放器在背景繼續保持運作,是以我需要監聽通話狀态,再來電話的時候,暫停播放 ,免得播放器搶占了音頻(此處有待商榷,應該有别的解決方案)。

我在網上随便查了下PhoneStateListener的用法,然後抄了下。。。但是沒人告訴我,這玩意會引起記憶體洩露。。。是以啊,随随便便查到的東西,千萬不要直接用到代碼裡,得仔細研究下。。。

老規矩 繼續查,在網上查到了這個東西需要解除監聽,大神說要這樣用:

You should try cleaning up on onPause and then onResume re-instate the stuff you need. This will help clean up some memory pressure and leaks. 
           

解除監聽的方法是:

telephonyManager.listen(telePhonePhoneStateListener,PhoneStateListener.LISTEN_NONE);
           

但是我的需求是在背景也得繼續監聽啊,是以,我把這個方法扔到了onDestroy()裡。。。試了下,沒什麼卵用。。。沒辦法繼續。。。

找到了這個:

http://www.07net01.com/2015/08/898417.html

在項目開發過程中通過ddms的堆看到記憶體一直持續在增長,很容易想到發生記憶體洩露,引用沒有被釋放,通過dump 最終發現是 PhoneStateListener 内部對自己有一個強引用的handler,如果是在主線程中引用的PhoneStateListener,那麼他将釋放不掉,引發記憶體洩露。 
解決方法就好的是在子線程中建立 PhoneStateListener ,其次是在PhoneStateListener中使用弱引用。比如需要activity的對象。那麼就要在建立PhoneStateListener的時候将傳入activity的弱引用 WeakRef ,這樣就不用擔心記憶體洩露的問題,不過這種弱引用的方式用起來簡單一些,但是他還有個小問題,就是PhoneStateListener自己是釋放不掉的,雖然他不再持有外部的一些引用,那麼就要求不要在PhoneStateListener裡面有過多的資源建立。
           

想試了下弱引用,發現沒搞懂這裡怎麼使用弱引用。。。

恩,那就用子線程,代碼如下:

moniterTekePhone = new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                sonThreadlooper = Looper.myLooper();
                telephonyManager = (TelephonyManager)mActivity.getSystemService(Context.TELEPHONY_SERVICE);
                telePhonePhoneStateListener = new TelePhonePhoneStateListener(mDoTelePhonyWorkListener);
                telephonyManager.listen(telePhonePhoneStateListener,PhoneStateListener.LISTEN_CALL_STATE);
                Looper.loop();
             }
        });
<pre name="code" class="java">moniterTekePhone.start();
           

銷毀用,在onDestroy()中調用:

public void destoryMoniterTelePhone(){
        if(telephonyManager != null){
            telephonyManager.listen(telePhonePhoneStateListener,PhoneStateListener.LISTEN_NONE);
            telephonyManager = null;
        }
        if(moniterTekePhone != null && moniterTekePhone.isAlive()){
            if(sonThreadlooper != null){
                try{
                    sonThreadlooper.quit();
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
            sonThreadlooper = null;
        }
    }
           

試了下,還是未能成功解決問題。。。依然維持了對目前activity的引用。

百思不得其解,估計是binder的問題。

繼續百度,發現有另一種寫法:監聽廣播

IntentFilter intentFilterPhone = new IntentFilter();
        intentFilterPhone.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
        phoneReceiver.setTelePhonyWorkListener(mDoTelePhonyWorkListener);
        mActivity.registerReceiver(phoneReceiver, intentFilterPhone);
           

重寫廣播:

public class PhoneReceiver extends BroadcastReceiver {

    DoTelePhonyWorkListener telePhonyWorkListener;

    public  void setTelePhonyWorkListener(DoTelePhonyWorkListener listener){
        telePhonyWorkListener = listener;
    }

    public interface DoTelePhonyWorkListener{
        void callStateIdle();
        void callStateRinging();
        void callStateOffHook();
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(Intent.ACTION_NEW_OUTGOING_CALL)) {
            // 如果是去電(撥出)
        } else {
            TelephonyManager tm = (TelephonyManager) context
                    .getSystemService(Service.TELEPHONY_SERVICE);
            // 設定一個監聽器
            tm.listen(listener, PhoneStateListener.LISTEN_CALL_STATE);
        }
    }

    PhoneStateListener listener = new PhoneStateListener() {

        @Override
        public void onCallStateChanged(int state, String incomingNumber) {
            // state 目前狀态 incomingNumber,貌似沒有去電的API
            super.onCallStateChanged(state, incomingNumber);
            switch (state) {
            case TelephonyManager.CALL_STATE_RINGING:
                if( telePhonyWorkListener != null){
                    telePhonyWorkListener.callStateRinging();
                }
                break;
            case TelephonyManager.CALL_STATE_IDLE:
                if(telePhonyWorkListener != null){
                    telePhonyWorkListener.callStateIdle();
                }
                break;
            case TelephonyManager.CALL_STATE_OFFHOOK:
                if(telePhonyWorkListener != null){
                    telePhonyWorkListener.callStateOffHook();
                }
                break;
            }
        }

    };
}
           

最後 在onDestroy中解除注冊廣播。

這會兒是真的解決了。。。。