从 Android 静音看正确的查找 bug 的姿势
发布于 7 个月前 作者 Bugly_Tony 26374 次浏览 来自 技术

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,我也不敢不信呐。我们再来细细把刚才的场景理一遍:QA首先在app内开启了静音,调用了我们在前面给出的那段代码,紧接着点返回键退出了app(实际上这时候我们的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一脸不屑的看都不看洒家一眼,依旧不吱声。

坑爹呢吧!!自行脑补我摔手机的场景

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 1.png

如果有缓存的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、结语

记得尽量多用ApplicationContext,多喝水哈。

如果你觉得内容意犹未尽,如果你想了解更多相关信息,请扫描以下二维码,关注我们的公众账号,可以获取更多技术类干货,还有精彩活动与你分享~

Bugly

腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!