足球资料库数据/孙祥/nba五佳球/足球直播哪个平台好 - cctv5今日现场直播

首頁 > 知識庫 > 正文

從 Android 靜音看正確的查找 bug 的姿勢
2016-03-01 16:22:07   來源: mengyidan1988   評論:0 點擊:

本文轉自:騰訊Bugly 0、寫在前面 沒搶到小馬哥的紅包,無心回家了,回公司寫篇文章安慰下自己TT。。話說年關難過,bug 多多,時間久了難免頭昏腦熱,不辨朝暮,難識乾坤。。。艾瑪,扯遠了,話說誰沒踩過坑,可視大家都是如何從坑里爬出來的呢? 1、實現個靜音的功能 話說,有那么一天, 引用PM:『我這里有個需求,很簡單很簡單那種』 RD:『哦,
本文轉自:騰訊Bugly



0、寫在前面

沒搶到小馬哥的紅包,無心回家了,回公司寫篇文章安慰下自己TT。。話說年關難過,bug 多多,時間久了難免頭昏腦熱,不辨朝暮,難識乾坤。。。艾瑪,扯遠了,話說誰沒踩過坑,可視大家都是如何從坑里爬出來的呢?

1、實現個靜音的功能

話說,有那么一天,

引用
PM:『我這里有個需求,很簡單很簡單那種』

RD:『哦,需要做三天』

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

RD:『哦,現在需要做六天了』


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

我一看API,是挺簡單的:
private void setMuteEnabled(boolean enabled){    AudioManager mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);    mAudioManager.setStreamMute(AudioManager.STREAM_MUSIC, enabled);}

是吧,多簡單,三分鐘搞定。不過說真的,這并不是什么好兆頭,太簡單了,簡單到令人窒息啊!

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

話說,過了幾天,

引用
QA:『如果我先開啟靜音,然后退出我們的 app 再進來,盡管頁面顯示靜音狀態,但我無法取消靜音啊』

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


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

1. 打開 app,開啟靜音
2. 點擊返回鍵,直到 app 進入后臺運行
3. 重新點擊 app 的 icon,啟動 app,此時期望 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多簡單,惹得騷年盡難眠。

3、『你可以告訴我該靜音或者不靜音,但聽不聽那是我的事兒』

我這么無辜,寥寥幾行代碼,能犯什么錯誤呢?所以問題一定出在官方的 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.


就是說,我們可以發送任意次靜音請求,而想要取消靜音,還得發出同樣次數的取消靜音請求才可以真正取消靜音。
引用
好像找到答案了。不對呀,我以你的人格擔保,我只發了一次靜音請求啊,怎么取消靜音就這么費勁呢!


4、『這是我的名片』

突然,嗯,就是在這時,我想起前幾天我那本被茶水泡了的《深入理解 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 deathprivate int mMuteCount; // Number of active mutes for this clientVolumeDeathHandler(IBinder cb) {    mICallback = cb;}……}

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

5、『其實,剛才不是我』

對呀,有名片啊,問題是我這是同一個 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
@Overridepublic 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);            }});

這一句就實際上往 SYSTEMSERVICEMAP.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 發出了取消靜音的請求,不過然并卵。

6、『這事兒還是交給同一個人辦比較靠譜』

有了前面的分析,解決方法其實也就浮水而出了:
AudioManager mAudioManager = (AudioManager) getContext().getApplicationContext().getSystemService(Context.AUDIO_SERVICE);

我們只要使用 Application 全局 Context 去獲取 AudioManager 不就沒有那么多事兒了么?

再來回答,為什么系統沒有提供獲取是否靜音的 Api 這個問題。如果系統確實提供了這個 Api,它應該為你提供哪些信息呢?是告訴你系統當前是否靜音嗎?它告訴你這個有啥意義呢,反正那些別人操作的結果,如果已經靜音,你也單方面做不到取消靜音;是告訴你你這個應用是否已經發送過靜音請求?請求數量你自己完全可以自己記錄,為什么還要官方 Api 提供給你?所以,獲取是否處于靜音狀態這個接口其實意義并不見得有多大。

7、結語

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

侯捷先生在《STL源碼剖析》一書的扉頁上面寫道『源碼之前,了無秘密』。寫程序的時候,我經常會因為運行結果與預期不一致而感到不悅,甚至抱怨這就是『命』,想想也是挺逗的。計算機總是會忠實地執行我們提供的程序,如果你發現它『不聽』指揮,顯然是你的指令有問題;除此之外,我們的指令還需要經過層層傳遞,才會成為計算機可以執行的機器碼,如果你對系統 api 的工作原理不熟悉,對系統的工作原理不熟悉,你在組織自己的代碼的時候就難免一廂情愿。

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

作為 Android 開發者,盡管我不需要做 Framework 開發,但這并不能說明我不需要對 Framework 有一定的認識和了解。我們應該在平時的開發和學習當中經常翻閱這些系統的源碼,了解它們的工作機制有助于我們更好的思考系統 api 的應用場景。

關于 Android 系統源碼,如果不是為了深入的研究,我比較建議直接在網上直接瀏覽:
  • Androidxref (http://androidxref.com/),該站點提供了一定程度上的代碼跳轉支持,以及非常強大的檢索功能,是我們查詢系統源碼的首選。
  • Grepcode (http://grepcode.com/) 也可以檢索Android系統源碼,與前者不同的是,它只包含Java代碼,不過也是寸有所長,grepcode在Java代碼跳轉方面的支持已經非常厲害了。

相關熱詞搜索:android 移動開發 bug mobile 移動開發

上一篇:淺談12306核心模型設計思路和架構設計
下一篇: 緩存管理方案 AutoLoadCache3.1發布,優化“拿來主義”機制

分享到: 收藏