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中的静音按钮显示为静音开启的状态,并且点击可以取消静音。当然,实际上并不是这样 (|_|)

有个问题需要交代一下,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 notificationtry {// mICallback can be 0 if muted by AudioServiceif (mICallback != null) {mICallback.linkToDeath(this, 0);}VolumeStreamState.this.mDeathHandlers.add(this);// If the stream is not yet muted by any client, set level to 0if (!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 notificationVolumeStreamState.this.mDeathHandlers.remove(this);// mICallback can be 0 if muted by AudioServiceif (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);}});

这一句就实际上往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发出了取消静音的请求,不过然并卵。

6、『这事儿还是交给同一个人办比较靠谱』

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

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

我们只要使用Application全局Context去获取AudioManager不就没有那么多事儿了么?其实尽可能地引用Application而不是Activity,在很多场合甚至会避免内存泄露。有朋友问起什么时候应该用Application,什么时候应该用Activity,答案很明显,只要是Application可以做到的,就一律不要用Activity,除非引用方的生命周期跟Activity的生命周期一致。

再来回答,为什么系统没有提供获取是否静音的Api这个问题。如果系统确实提供了这个Api,它应该为你提供哪些信息呢?是告诉你系统当前是否静音吗?它告诉你这个有啥意义呢,反正那些别人操作的结果,如果已经静音,你也单方面做不到取消静音;是告诉你你这个应用是否已经发送过静音请求?请求数量你自己完全可以自己记录,为什么还要官方Api提供给你?所以,获取是否处于静音状态这个接口其实意义并不见得有多大。当然,实际上这个api是写在代码中的,只不过被@hide了,我们就当做没有看待好了。

7、 小结

静音的故事讲完了,这个小故事告诉我们一个道理:代码从来都不会骗我们

侯捷先生在《STL源码剖析》一书的扉页上面写道『源码之前,了无秘密』。写程序的时候,我经常会因为运行结果与预期不一致而感到不悦,甚至抱怨这就是『命』,想想也是挺逗的。计算机总是会忠实地执行我们提供的程序,如果你发现它『不听』指挥,显然是你的指令有问题;除此之外,我们的指令还需要经过层层传递,才会成为计算机可以执行的机器码,如果你对系统api的工作原理不熟悉,对系统的工作原理不熟悉,你在组织自己的代码的时候就难免一厢情愿。

至于官方API文档,每次看到它都有看到『课本』一样的感觉。中学的时候,老师最爱说的一句话就是,『课本要多读,常读常新』。官方API呢,显然也是这样。没有头绪的时候,它就是我们救星啊。

作为Android开发者,尽管我不需要做Framework开发,但这并不能说明我不需要对Framework有一定的认识和了解。我们应该在平时的开发和学习当中经常翻阅这些系统的源码,了解它们的工作机制有助于我们更好的思考系统api的应用场景。

关于Android系统源码,如果不是为了深入的研究,我比较建议直接在网上直接浏览:

  • Androidxref,该站点提供了一定程度上的代码跳转支持,以及非常强大的检索功能,是我们查询系统源码的首选。
  • Grepcode也可以检索Android系统源码,与前者不同的是,它只包含Java代码,不过也是尺有所长,grepcode在Java代码跳转方面的支持已经非常厉害了。

    想了解更多干货,请搜索关注公众号:腾讯Bulgy,或搜索微信号:weixinBugly,关注我们


腾讯Bugly

Bugly是腾讯内部产品质量监控平台的外发版本,支持iOS和Android两大主流平台,其主要功能是App发布以后,对用户侧发生的crash以及卡顿现象进行监控并上报,让开发同学可以第一时间了解到app的质量情况,及时修改。目前腾讯内部所有的产品,均在使用其进行线上产品的崩溃监控。

腾讯Bugly经过内部团队4年打磨,目前腾讯内部所有的产品都在使用,基本覆盖了中国市场的移动设备以及网络环境,可靠性有保证。使用Bugly,你就使用了和手机QQ、QQ空间、手机管家相同的质量保障手段。

转载于:https://www.cnblogs.com/bugly/p/5210624.html

从 Android 静音看正确的查bug的姿势?相关推荐

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

    0.写在前面 没抢到小马哥的红包,无心回家了,回公司写篇文章安慰下自己TT..话说年关难过,bug多多,时间久了难免头昏脑热,不辨朝暮,难识乾坤...艾玛,扯远了,话说谁没踩过坑,可视大家都是如何从坑 ...

  2. 睡觉的时候,程序能不能自动查 bug?

    作者 | 杜沁园 等 责编 | 郭芮 出品 | CSDN(ID:CSDNnews) 曾在 Hacker News 上看到过一个 Oracle 工程师处理 bug 的 日常: 先花两周左右时间来理解 2 ...

  3. Android 8.1 频频被曝 Bug,是要赶超苹果吗?

    点击上方"CSDN",选择"置顶公众号" 关键时刻,第一时间送达! Android 8.1被曝严重 Bug,这下看你还想不想吃奥利奥了. 虽然很多采用 Andr ...

  4. php 色彩空间转换,【干货】教你最正确的查色域方式

    原标题:[干货]教你最正确的查色域方式 听到小白用鲁大师去查色域的消息,我的内心是拒绝的.其实色域重不重要因人而异,既然大家想看,今天就科普一下如何查色域. 如果你知道自己显示器的型号,把型号一输入网 ...

  5. 在我们睡觉的时候,程序能不能自动查 bug?

    作者介绍:我和我的 SQL 队(成员:杜沁园.韩玉博.黄宝灵.满俊朋),他们的项目「基于路径统计的 sql bug root cause 分析」获得了 TiDB Hackathon 2019 的三等奖 ...

  6. Android Studio之正确导入SO库

    Android Studio之正确导入SO相关文件 之前一直没有做过第三方的推送消息,这次公司要求采用国内极光第三方推送服务,由于项目还在原型图构思中,因而还可以花点时间看看这方面的东西. 看了下相关 ...

  7. Android 入门eclipse+ADT配置,bug处理及附件下载(一)

    Android 入门eclipse+ADT配置,bug处理及附件下载(一) 由于时代的变化科技的发展手机也发生了飞跃的变换,从以前的只能音频通话的"大哥大"到现在流行的3G手机:具 ...

  8. 利用SQLite在android上实现增删改查

    利用SQLite在android上实现增删改查 方法: 一.直接利用database.execSQL()方法输入完整sql语句进行操作 这种方法适用于复杂的sql语句,比如多表查询等等 这里适合于增删 ...

  9. android 静音与振动

    android 静音与振动 1,设置静音和振动 静音和振动都属于来电后的动作.所以在设置静音和振动时都只是设置一些标识,并往数据库写入相应标识. 文件:packages/apps/settings/s ...

最新文章

  1. 中国科学院院士褚君浩:第四次工业革命和智能时代
  2. 架构师之路 — 数据库设计 — SQL 结构化查询语言
  3. 转载 - 背景透明,文字不透明【学习】
  4. rust实战入门到进阶(2)
  5. Docker框架的使用系列教程(三)
  6. 未找到导入的项目,请确认 Import 声明中的路径正确
  7. docker-compose.yml配置文件详解
  8. 官方正式预热小米10S:哈曼卡顿加持小米有史以来音质最好的手机
  9. java共同方法_java-现有公共方法的NoSuchMethodError
  10. Unity游戏框架设计
  11. 手机处理器排名2019_手机CPU天梯图2020年3月最新版 你的手机处理器排名高吗?...
  12. 美萍2012 激活码 追码成功!
  13. 机器码反编译c语言,如何把任意一段机器码或unicode码反汇编成汇编指令
  14. 微信小程序分析送积分功能如何实现_微信小程序积分商城解决方案(一)
  15. 单向一对多和双向一对多
  16. 新型冠状病毒肺炎分析
  17. Inksape 设置画布像素尺寸及透明背景
  18. Win11关闭代理白名单 小猫咪关闭远程解析功能
  19. java字符转成ncr_NCR字符编码(形如中国)转换为汉字 in JAVA
  20. 年后找工作的你,如何写一封好的简历?

热门文章

  1. 网页鼠标滚动实现图片缩放
  2. 华南理工网络计算机基础知识,2019年华南理工大学网络教育计算机基础随堂练习第一章...
  3. java登陆界面连接数据库_java 登陆界面怎么写,连接数据库后
  4. PAT(甲级)2021年春季考试 7-1 Arithmetic Progression of Primes
  5. 1091 Acute Stroke 需再做
  6. 插槽 查看硬盘状态_摄影路上的“全能”伴侣 | LaCie DJI Copilot 移动硬盘
  7. c#中接口的使用方法图解_C#图解教程 第十五章 接口
  8. 如何发布自己的NPM包(模块)?
  9. 给 Windows 驱动程序安装提速
  10. 1968年12月9日,恩格尔巴特公开演示了世界上第一个鼠标盒子