從 Android 靜音看正確的查bug的姿式?

0、寫在前面

沒搶到小馬哥的紅包,無意回家了,回公司寫篇文章安慰下本身TT。。話說年關難過,bug多多,時間久了不免頭昏腦熱,不辨朝暮,難識乾坤。。。艾瑪,扯遠了,話說誰沒踩過坑,可視你們都是如何從坑裏爬出來的呢?快來關注騰訊Bugly乾貨分享吧!

一、實現個靜音的功能

話說,有那麼一天,java

PM:『我這裏有個需求,很簡單很簡單那種』android

RD:『哦,須要作三天』api

PM:『真的很簡單很簡單那種』緩存

RD:『哦,你又說了一遍很簡單,那麼如今須要作六天了』微信

對呀,靜音功能多簡單,點一下,欸,靜音了;再點一下,欸,不靜音了;再點一下,欸。。。app

我一看API,是挺簡單的:ide

private void setMuteEnabled(boolean enabled){
    AudioManager mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
    mAudioManager.setStreamMute(AudioManager.STREAM_MUSIC, enabled);
}

是吧,多簡單,三分鐘搞定。不過說真的,這並非什麼好兆頭,太簡單了,簡單到使人窒息啊!學習

二、『您好,我是京東快遞,您有一個bug簽收一下』

話說,過了幾天,fetch

QA:『若是我先開啓靜音,而後退出咱們的app再進來,儘管頁面顯示靜音狀態,但我沒法取消靜音啊』ui

RD:『必定是你的用法有問題!』

固然,我也挺心虛的啊,由於這段代碼我總共花了三分鐘,說有bug,我也不敢不信吶。咱們再來細細把剛纔的場景理一遍:

  1. 打開app,開啓靜音

  2. 點擊返回鍵,直到app進入後臺運行

  3. 從新點擊app的icon,啓動app,此時指望app中的靜音按鈕顯示爲靜音開啓的狀態,而且點擊能夠取消靜音。固然,實際上並非這樣 (|_|)

有個問題須要交代一下,Android api並無提供獲取當前音頻通道是否靜音的api(爲何沒有?你。。你竟然問我爲何?你爲何這麼着急?日後看就知道啦),因此我在進入app加載view時,要根據本地存儲的靜音狀態來初始化view的狀態:

boolean persistedMute = mute.getContext().getSharedPreferences("volume", Context.MODE_PRIVATE).getBoolean("Volume.Mute", false);
muteButton.setChecked(persistedMute);

而這個字段是在用戶點擊了muteButton以後被存入SharedPreference當中的。

不可能啊,到這裏毫無懸念可言啊,確定是沒有問題的呀。

接着看,這時候咱們要取消靜音了,調用的代碼就是下面這段代碼:

private void setMuteEnabled(boolean enabled){
    AudioManager mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
    mAudioManager.setStreamMute(AudioManager.STREAM_MUSIC, enabled);
}

而後,app一臉不屑的看都不看灑家一眼,依舊不吱聲。

坑爹呢吧!!自行腦補我摔手機的場景

正是:自古bug多簡單,惹得騷年盡難眠。😱

三、『你能夠告訴我該靜音或者不靜音,但聽不聽那是個人事兒』

我這麼無辜,寥寥幾行代碼,能犯什麼錯誤呢?因此問題必定出在官方的API上。

AudioManager.java

    /**     * Mute or unmute an audio stream.     * <p>     * The mute command is protected against client process death: if a process     * with an active mute request on a stream dies, this stream will be unmuted     * automatically.     * <p>     * The mute requests for a given stream are cumulative: the AudioManager     * can receive several mute requests from one or more clients and the stream     * will be unmuted only when the same number of unmute requests are received.     * <p>     * For a better user experience, applications MUST unmute a muted stream     * in onPause() and mute is again in onResume() if appropriate.     * <p>     * This method should only be used by applications that replace the platform-wide     * management of audio settings or the main telephony application.     * <p>This method has no effect if the device implements a fixed volume policy     * as indicated by {@link #isVolumeFixed()}.     *     * @param streamType The stream to be muted/unmuted.     * @param state The required mute state: true for mute ON, false for mute OFF     *     * @see #isVolumeFixed()     */
    public void setStreamMute(int streamType, boolean state) {
        IAudioService service = getService();        try {
            service.setStreamMute(streamType, state, mICallBack);
        } catch (RemoteException e) {
            Log.e(TAG, "Dead object in setStreamMute", e);
        }
    }

咱們摘出最關鍵的一句,你們一塊兒來樂呵樂呵。。。。

The mute requests for a given stream are cumulative: the AudioManager
can receive several mute requests from one or more clients and the stream
will be unmuted only when the same number of unmute requests are received.

就是說,咱們能夠發送任意次靜音請求,而想要取消靜音,還得發出一樣次數的取消靜音請求才能夠真正取消靜音。

好像找到答案了。不對呀,我以你的人格擔保,我只發了一次靜音請求啊,怎麼取消靜音就這麼費勁呢!

四、『這是個人名片』

忽然,嗯,就是在這時,我想起前幾天我那本被茶水泡了的《深刻理解Android》卷③提到,其實每一個app均可以發送靜音請求,並且各自都是單獨計數的。那麼問題來了,每一個app發靜音請求的惟一身份標識是啥嘞?

仍是要看設置靜音的接口方法:

AudioManager.java

   public void setStreamMute(int streamType, boolean state) {
        IAudioService service = getService();        try {
            service.setStreamMute(streamType, state, mICallBack);
        } catch (RemoteException e) {
            Log.e(TAG, "Dead object in setStreamMute", e);
        }
    }

這個service實際上是AudioService的一個實例,固然,其實AudioManager自己全部操做都是轉發給AudioService的。

AudioService.java

    /** @see AudioManager#setStreamMute(int, boolean) */
    public void setStreamMute(int streamType, boolean state, IBinder cb) {        if (mUseFixedVolume) {            return;
        }        if (isStreamAffectedByMute(streamType)) {            if (mHdmiManager != null) {                synchronized (mHdmiManager) {                    if (streamType == AudioSystem.STREAM_MUSIC && mHdmiTvClient != null) {                        synchronized (mHdmiTvClient) {                            if (mHdmiSystemAudioSupported) {
                                mHdmiTvClient.setSystemAudioMute(state);
                            }
                        }
                    }
                }
            }
            mStreamStates[streamType].mute(cb, state);
        }
    }

最後一行咱們看到實際上設置靜音須要傳入cb也就是AudioManager傳入的mICallBack,以及是靜音仍是取消靜音的操做state,而這個mute方法本質上也是調用了VolumeDeathHandler的mute方法,咱們直接看這個方法的源碼:

AudioService.VolumeDeathHandler

public void mute(boolean state) {    boolean updateVolume = false;    if (state) {        if (mMuteCount == 0) {            // Register for client death notification
            try {                // mICallback can be 0 if muted by AudioService
                if (mICallback != null) {
                    mICallback.linkToDeath(this, 0);
                }
                VolumeStreamState.this.mDeathHandlers.add(this);                // If the stream is not yet muted by any client, set level to 0
                if (!VolumeStreamState.this.isMuted()) {
                    updateVolume = true;
                }
            } catch (RemoteException e) {                // Client has died!
                binderDied();                return;
            }
        } else {
            Log.w(TAG, "stream: "+mStreamType+" was already muted by this client");
        }
        mMuteCount++;
    } else {        if (mMuteCount == 0) {
            Log.e(TAG, "unexpected unmute for stream: "+mStreamType);
        } else {
            mMuteCount--;            if (mMuteCount == 0) {                // Unregister from client death notification
                VolumeStreamState.this.mDeathHandlers.remove(this);                // mICallback can be 0 if muted by AudioService
                if (mICallback != null) {
                    mICallback.unlinkToDeath(this, 0);
                }                if (!VolumeStreamState.this.isMuted()) {
                    updateVolume = true;
                }
            }
        }
    }    if (updateVolume) {
        sendMsg(mAudioHandler,
        MSG_SET_ALL_VOLUMES,
        SENDMSG_QUEUE,        0,        0,
        VolumeStreamState.this, 0);
    }
}

其實這個方法的邏輯比較簡單,若是靜音,那麼mMuteCount++,不然—。這裏面還有一個邏輯處理了發送了靜音請求的app由於crash而沒法發出取消靜音的請求的情形,若是出現這樣的狀況,系統會直接清除這個app發出的全部靜音請求來使系統音頻正常工做。

那麼,mMuteCount是VolumeDeathHandler的成員,而VolumeDeathHandler的惟一性主要體如今傳入的IBinder實例cb上。

AudioService.VolumeDeathHandler

private class VolumeDeathHandler implements IBinder.DeathRecipient {    private IBinder mICallback; // To be notified of client's death
    private int mMuteCount; // Number of active mutes for this client

    VolumeDeathHandler(IBinder cb) {
        mICallback = cb;
    }

    ……
}

結論就是:AudioManager的mICallBack是靜音計數當中發起請求一方的惟一身份標識。

五、『其實,剛纔不是我』

對呀,有名片啊,問題是我這是同一個app啊,同一個啊……問題出在哪裏了呢。

剛纔咱們知道了,其實靜音請求計數是以AudioManager當中的一個叫mICallBack的傢伙爲惟一標識的,這個傢伙是哪裏來的呢?

AudioManager.java

private final IBinder mICallBack = new Binder();

咱們發現,其實對於同一個AudioManager來講,這個mICallBack必定是同一個。反過來講,咱們在操做靜音和取消靜音時沒有效果,應該就是由於咱們的mICallBack不同,若是是這樣的話,那麼說明AudioManager也不同。。。

操曰:『天下英雄,惟使君與操耳』

玄德大驚曰:『操耳是哪一個嘛?』

正當我收起我驚呆了的下巴的時候,我回過神來,準備對AudioManager的身世一探究竟。且說,AudioManager是怎麼來的?

AudioManager mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);

那麼這個getSystemService又是什麼來頭??通過一番查證,咱們發現,其實這個方法最終是在ContextImpl這個類當中得以實現:

ContextImpl.java

    @Override
    public Object getSystemService(String name) {
        ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);        return fetcher == null ? null : fetcher.getService(this);
    }

那麼問題的關鍵就在與咱們拿到的這個ServiceFetcher實例了。且看它的get方法實現:

ContextImpl.ServiceFetcher

        public Object getService(ContextImpl ctx) {
            ArrayList<Object> cache = ctx.mServiceCache;
            Object service;            synchronized (cache) {                if (cache.size() == 0) {                    // Initialize the cache vector on first access.
                    // At this point sNextPerContextServiceCacheIndex
                    // is the number of potential services that are
                    // cached per-Context.
                    for (int i = 0; i < sNextPerContextServiceCacheIndex; i++) {
                        cache.add(null);
                    }
                } else {
                    service = cache.get(mContextCacheIndex);                    if (service != null) {                        return service;
                    }
                }
                service = createService(ctx);
                cache.set(mContextCacheIndex, service);                return service;
            }
        }

若是有緩存的Service實例,就直接取出來返回;若是沒有,調用createService返回一個。再看看下面的片斷,這個問題就很清楚了:

        registerService(AUDIO_SERVICE, new ServiceFetcher() {                public Object createService(ContextImpl ctx) {                    return new AudioManager(ctx);
                }});

這一句就實際上往SYSTEM_SERVICE_MAP.get當中添加了一個與AudioService有關的ServiceFetcher實例,而這個實例裏面竟然直接new了一個AudioManager。

等會兒讓我想會兒靜靜。它在這裏new了一個AudioManager。它怎麼能new了一個AudioManager呢。

按照咱們剛纔的推斷,先後兩次操做AudioManager是不同的,而同一個Context返回的AudioManager只能是一個實例,換句話說,只要咱們每次獲取AudioManager時使用的Context不是同一個實例,那麼AudioManager就不是同一個實例,繼而mICallBack也不是同一個,因此音頻服務會覺得是兩個絕不相干的靜音和取消靜音的請求。

再來看看咱們用的Context會有什麼問題。

AudioManager mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);

這段代碼是在View當中的,換句話說,getContext返回的是初始化View時傳入的Context。初始化這個View傳入的Context是咱們惟一的Activity。這時,我不說,你們也會猜到下面的內容了:

靜音時的Activity實例和第二次進入引用時取消靜音時的Activity根本不多是同一個實例,所以這兩個操做是不相干的。因爲系統只要收到任意的靜音請求都會使對應的音頻通道進入靜音狀態,所以即便咱們用另外一個AudioManager發出了取消靜音的請求,不過然並卵。

六、『這事兒仍是交給同一我的辦比較靠譜』

有了前面的分析,解決方法其實也就浮水而出了:

AudioManager mAudioManager = (AudioManager) getContext().getApplicationContext().getSystemService(Context.AUDIO_SERVICE);

咱們只要使用Application全局Context去獲取AudioManager不就沒有那麼多事兒了麼?其實儘量地引用Application而不是Activity,在不少場合甚至會避免內存泄露。有朋友問起何時應該用Application,何時應該用Activity,答案很明顯,只要是Application能夠作到的,就一概不要用Activity,除非引用方的生命週期跟Activity的生命週期一致。

再來回答,爲何系統沒有提供獲取是否靜音的Api這個問題。若是系統確實提供了這個Api,它應該爲你提供哪些信息呢?是告訴你係統當前是否靜音嗎?它告訴你這個有啥意義呢,反正那些別人操做的結果,若是已經靜音,你也單方面作不到取消靜音;是告訴你你這個應用是否已經發送過靜音請求?請求數量你本身徹底能夠本身記錄,爲何還要官方Api提供給你?因此,獲取是否處於靜音狀態這個接口其實意義並不見得有多大。固然,實際上這個api是寫在代碼中的,只不過被@hide了,咱們就當作沒有看待好了。

七、 小結

靜音的故事講完了,這個小故事告訴咱們一個道理:代碼歷來都不會騙咱們

侯捷先生在《STL源碼剖析》一書的扉頁上面寫道『源碼以前,了無祕密』。寫程序的時候,我常常會由於運行結果與預期不一致而感到不悅,甚至抱怨這就是『命』,想一想也是挺逗的。計算機老是會忠實地執行咱們提供的程序,若是你發現它『不聽』指揮,顯然是你的指令有問題;除此以外,咱們的指令還須要通過層層傳遞,纔會成爲計算機能夠執行的機器碼,若是你對系統api的工做原理不熟悉,對系統的工做原理不熟悉,你在組織本身的代碼的時候就不免一廂情願。

至於官方API文檔,每次看到它都有看到『課本』同樣的感受。中學的時候,老師最愛說的一句話就是,『課本要多讀,常讀常新』。官方API呢,顯然也是這樣。沒有頭緒的時候,它就是咱們救星啊。

做爲Android開發者,儘管我不須要作Framework開發,但這並不能說明我不須要對Framework有必定的認識和了解。咱們應該在平時的開發和學習當中常常翻閱這些系統的源碼,瞭解它們的工做機制有助於咱們更好的思考系統api的應用場景。

關於Android系統源碼,若是不是爲了深刻的研究,我比較建議直接在網上直接瀏覽:

  • Androidxref,該站點提供了必定程度上的代碼跳轉支持,以及很是強大的檢索功能,是咱們查詢系統源碼的首選。

  • Grepcode也能夠檢索Android系統源碼,與前者不一樣的是,它只包含Java代碼,不過也是尺有所長,grepcode在Java代碼跳轉方面的支持已經很是厲害了。

    想了解更多幹貨,請搜索關注公衆號:騰訊Bulgy,或搜索微信號:weixinBugly,關注咱們


騰訊Bugly

Bugly是騰訊內部產品質量監控平臺的外發版本,支持iOS和Android兩大主流平臺,其主要功能是App發佈之後,對用戶側發生的crash以及卡頓現象進行監控並上報,讓開發同窗能夠第一時間瞭解到app的質量狀況,及時修改。目前騰訊內部全部的產品,均在使用其進行線上產品的崩潰監控。

相關文章
相關標籤/搜索