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

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 death
private int mMuteCount; // Number of active mutes for this client

VolumeDeathHandler(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

@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);
            }});

这一句就实际上往 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代码跳转方面的支持已经非常厉害了。

本文系腾讯Bugly独家内容,转载请在文章开头显眼处注明注明作者和出处“腾讯Bugly(http://bugly.qq.com)”

腾讯Bugly 最专业的质量跟踪平台

为您定期分享应用崩溃解决方案

原文发布于微信公众号 - 腾讯Bugly(weixinBugly)

原文发表时间:2016-02-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Android 开发者

在 Android O 上用到 MediaStyle 的提醒功能

46420
来自专栏iOSDevLog

用Kotlin破解Android版微信小游戏-跳一跳成果跳一跳思路源码使用方法参考来源Android 插件 免PC

40760
来自专栏木宛城主

探索MVP(Model-View-Presenter)设计模式在SharePoint平台下的实现

对于SharePoint Developers来说,往往会过多的去关注SharePoint平台和工具,而把设计模式和代码的可测试性放在了一个较低的优先级。这并...

21870
来自专栏项勇

[Android笔记8]使用Zxing实现二维码扫描

32460
来自专栏吾真本

Release It! 第2版目录(中英文对照)

注:其中,最后17个小节标题尚未译完,其余译完的标题尚未定稿,会与正式版有出入。仅供参考。

41520
来自专栏PHP在线

php中关于mysqli和mysql区别的一些知识点分析

一: PHP-MySQL 是 PHP 操作 MySQL 资料库最原始的 Extension ,PHP-MySQLi 的 i 代表 Improvement ,...

305100
来自专栏Java与Android技术栈

一个快速分析android app使用了哪些sdk的工具工具使用实现原理写在最后

前段时间我们要准备开始做移动端广告sdk,我需要了解市面上一些常用的广告sdk使用情况。如果一个个app去分析,那工作量会非常庞大,所以就产生了这个工具Anal...

12020
来自专栏Flutter入门到实战

Android适配全面总结(三)----ROM适配

版权声明:本文为博主原创文章(部分引用他人博文,已加上引用说明),未经博主允许不得转载。https://www.jianshu.com/p/f9c67a4b90...

40310
来自专栏雪胖纸的玩蛇日常

Vue+Django2.0 REST framework打造前后端分离的生鲜电商项目(三)设计数据库以及导入原始数据

53950
来自专栏Android小菜鸡

Android Touch事件传递机制

  Touch事件的传递机制与生活贴近,从父布局开始一步一步的向下分发事件。分发事件时调用boolean dispatchTouchEvent(MotionEv...

28130

扫码关注云+社区

领取腾讯云代金券