日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > Android >内容正文

Android

从 Android 静音看正确的查找 bug 的姿势

發布時間:2025/7/14 Android 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 从 Android 静音看正确的查找 bug 的姿势 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

0、寫在前面

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


1、實現個靜音的功能

話說,有那么一天, PM:『我這里有個需求,很簡單很簡單那種』 RD:『哦,需要做三天』 PM:『真的很簡單很簡單那種』 RD:『哦,現在需要做六天了』 對呀,靜音功能多簡單,點一下,欸,靜音了;再點一下,欸,不靜音了;再點一下,欸。。。 我一看API,是挺簡單的: [Java]?純文本查看?復制代碼 ?
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中的靜音按鈕顯示為靜音開啟的狀態,并且點擊可以取消靜音。當然,實際上并不是這樣 (|_|) 有問題需要提一下,Android api并沒有提供獲取當前音頻通道是否靜音的api(為什么沒有?你。。你居然問我為什么?你為什么這么著急?往后看就知道啦),所以我在進入app加載view時,要根據本地存儲的靜音狀態來初始化view的狀態: [Java]?純文本查看?復制代碼 ?
boolean persistedMute = mute.getContext().getSharedPreferences("volume", Context.MODE_PRIVATE).getBoolean("Volume.Mute", false); muteButton.setChecked(persistedMute);
而這個字段是在用戶點擊了muteButton之后被存入SharedPreference當中的。 不可能啊,到這里毫無懸念可言啊,肯定是沒有問題的呀。
接著看,這時候我們要取消靜音了,調用的代碼就是下面這段代碼: [Java]?純文本查看?復制代碼 ?
private void setMuteEnabled(boolean enabled){ ????AudioManager mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE); ????mAudioManager.setStreamMute(AudioManager.STREAM_MUSIC, enabled); }
然后,app一臉不屑的看都不看灑家一眼,依舊不吱聲。 坑爹呢吧!!自行腦補我摔手機的場景
3、『你可以告訴我該靜音或者不靜音,但聽不聽那是我的事兒』
我這么無辜,寥寥幾行代碼,能犯什么錯誤呢?所以問題一定出在官方的API上。 AudioManager.java [Java]?純文本查看?復制代碼 ?
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 /** ?* 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 [Java]?純文本查看?復制代碼 ?
1 2 3 4 5 6 7 8 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 [Java]?純文本查看?復制代碼 ?
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 /** @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 [Java]?純文本查看?復制代碼 ?
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 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 [Java]?純文本查看?復制代碼 ?
01 02 03 04 05 06 07 08 09 10 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是靜音計數當中發起請求一方的唯一身份標識。 ?

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

對呀,有名片啊,問題是我這是同一個app啊,同一個啊……問題出在哪里了呢。 剛才我們知道了,其實靜音請求計數是以AudioManager當中的一個叫mICallBack的家伙為唯一標識的,這個家伙是哪里來的呢?

?

AudioManager.java [Java]?純文本查看?復制代碼 ?
1 private final IBinder mICallBack = new Binder();
我們發現,其實對于同一個AudioManager來說,這個mICallBack一定是同一個。反過來說,我們在操作靜音和取消靜音時沒有效果,應該就是因為我們的mICallBack不一樣,如果是這樣的話,那么說明AudioManager也不一樣。。。 操曰:『天下英雄,唯使君與操耳』 玄德大驚曰:『操耳是哪個嘛?』 正當我收起我驚呆了的下巴的時候,我回過神來,準備對AudioManager的身世一探究竟。且說,AudioManager是怎么來的? [Java]?純文本查看?復制代碼 ?
1 AudioManager mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
那么這個getSystemService又是什么來頭??經過一番查證,我們發現,其實這個方法最終是在ContextImpl這個類當中得以實現:

?

ContextImpl.java [Java]?純文本查看?復制代碼 ?
1 2 3 4 5 @Override public Object getSystemService(String name) { ????ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name); ????return fetcher == null ? null : fetcher.getService(this); }
那么問題的關鍵就在與我們拿到的這個ServiceFetcher實例了。且看它的get方法實現: ContextImpl.ServiceFetcher

? ???

[Java]?純文本查看?復制代碼 ?
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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返回一個。再看看下面的片段,這個問題就很清楚了: [Java]?純文本查看?復制代碼 ?
1 2 3 4 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會有什么問題。 [Java]?純文本查看?復制代碼 ?
1 AudioManager mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
這段代碼是在View當中的,換句話說,getContext返回的是初始化View時傳入的Context。初始化這個View傳入的Context是我們唯一的Activity。這時,我不說,大家也會猜到下面的內容了: 靜音時的Activity實例和第二次進入引用時取消靜音時的Activity根本不可能是同一個實例,因此這兩個操作是不相干的。由于系統只要收到任意的靜音請求都會使對應的音頻通道進入靜音狀態,因此即使我們用另一個AudioManager發出了取消靜音的請求,不過然并卵。


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

有了前面的分析,解決方法其實也就浮水而出了: [Java]?純文本查看?復制代碼 ?
1 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代碼跳轉方面的支持已經非常厲害了。

轉載于:https://www.cnblogs.com/krislight1105/p/5203164.html

總結

以上是生活随笔為你收集整理的从 Android 静音看正确的查找 bug 的姿势的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。