阅读更多

2顶
0踩

移动开发
本文转自:腾讯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 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代码跳转方面的支持已经非常厉害了。
  • 大小: 158 KB
来自: 腾讯Bugly
2
0
评论 共 1 条 请登录后发表评论
1 楼 pentiumchen 2016-02-21 10:16
有个疑问,用SharedPreferences记录是否按过静音,如果用户清楚了应用的缓存,还能正确获取静音状态么?

发表评论

您还没有登录,请您登录后再发表评论

相关推荐

  • 用JavaScript控制Excel文件详细说明

    压缩包里有三个网页,详细说明了如何用javascript来控制excel把网页中的信息导出到excel表格中,很好很强大!

  • Javascript----文件操作

    一、功能实现核心:FileSystemObject 对象     要在javascript中实现文件操作功能,主要就是依靠FileSystemobject对象。二、FileSystemObject编程 使用FileSystemObject 对象进行编程很简单,一般要经过如下的步骤: 创建FileSystemObject对象、应用相关方法、访问对象相关属性 。 (一)创建FileSystemObje

  • ActiveXObject函数详解

    什么是 ActiveX 控件? ActiveX 控件广泛用于 Internet。它们可以通过提供视频、动画内容等来增加浏览的乐趣。不过,这些程序可能出问题或者向您提供不需要的内容。在某些情况下,这些程序可被用来以您不允许的方式从计算机收集信息、破坏您的计算机上的数据、在未经您同意的情况下在您的计算机上安装软件或者允许他人远程控制您的计算机。考虑到这些风险,您应该在完全信任发行商的情况

  • ActiveXObject在什么浏览器有效

    ActiveXObject 是一种用于在 Internet Explorer 浏览器中创建对象的 JavaScript 对象。它只能在 Internet Explorer 浏览器中使用,其他浏览器(如 Chrome、Firefox 和 Safari)不支持 ActiveXObject。 如果您想在其他浏览器中使用类似的功能,可以使用 XMLHttpRequest 对象或 Fetch API。 ...

  • ActiveXObject对象使用整理

    一、什么是 ActiveX 控件?         ActiveX 控件广泛用于 Internet。它们可以通过提供视频、动画内容等来增加浏览的乐趣。不过,这些程序可能出问题或者向您提供不需要的内容。在某些情况下,这些程序可被用来以您不允许的方式从计算机收集信息、破坏您的计算机上的数据、在未经您同意的情况下在您的计算机上安装软件或者允许他人远程控制您的计算机。一般软件需要用户单独下载然后执行

  • ActiveXObject('Microsoft.XMLHTTP')出现“没有权限” 跨域权限问题

    ActiveXObject('Microsoft.XMLHTTP')时出现“没有权限”的错误! 问题可能出现在这行    .open("post",url,false)        你在open另一个域的url       也就是说你开着的网页地址是192.168.0.3,就不能open("get","http://locahost/ll.htm")       而只能http:/

  • js 调用 new ActiveXObject('WScript.Shell')报错

    当在网页中点击打印时,会报错,无法打印,解决方法如下: 在浏览器中找到“Internet选项”,在弹出的对话框中进行设置。 “Internet选项”—&gt;“安全”—&gt;“本地Intranet”—&gt;“自定义级别”—&gt;“ActiveX控件和插件”—&gt;“对未标记为可安全执行脚本的ActiveX控件初始化并执行脚本”—&gt;“提示”—“确定”即可。 如果还报错的话,还需进...

  • JavaScript中ActiveXObject函数

    JavaScript中ActiveXObject对象是启用并返回 Automation 对象的引用。使用方法: newObj = new ActiveXObject( servername.typename[, location]) ActiveXObject 对象语法有这些部分:其中newObj是必选项。要赋值为 ActiveXObject 的变量名。servername是必选项。提供该对...

  • 关于js中使用ActiveXObject来写入或读取文件的一些知识

    一、今天看了js中使用ActiveXObject 来读写本地文件的一些东西,特此记录一下   二、注意事项:ActiveXObject 兼容IE内核浏览器,测试的时候需要注意   三、直接上代码吧 &amp;lt;!DOCTYPE html&amp;gt; &amp;lt;html&amp;gt; &amp;lt;head lang=&quot;en&quot;&amp;gt;     &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;   &amp;lt

  • js可以操作服务器上的文件吗,Node.js初级 - 2. 完善本地服务器(如何进行文件操作)...

    上一节已经搭建好了一个服务器,但是还是存在许多问题的:虽然现在可以通过你的请求来给你返回数据,但是这些东西都写在 switch 内,当页面很多时,代码就会变得非常冗余接收到请求后返回的数据现在只能是文字类型,如果请求图片或者音乐等等,则这个方法是无法使用的每次更新服务器端运行的代码都需要重启一次服务,操作非常繁琐,耗费了大量的时间那么该如何处理这些问题呢?使服务器可以读取到文件读取文件是一个操作,...

  • JavaScript 原生ActiveXObject 对象实现 Excel 导入

    本文系原创,转载请注明出处: https://blog.csdn.net/chengbao315/article/details/83713219 在前一篇文章中,我利用 xlsx.js 插件实现了Excel 导入的功能,本周我又接到了新的需求,要求界面加载时自动读取指定 Excel 文件数据,实现导入Excel 功能。开始时,我感觉这有何难?基本上跟之前的设计没有啥区别嘛,把按钮的点击事件处...

  • 使用ActiveX插件时object显示问题,div被object标签遮挡的解决方案

    浏览器中object显示问题起因解决方案方法一: 直接设置属性值方法二:iframe结论 起因 设计要求视频控制面板显示在视频界面上,如下图红框内所示。但是因为object不在文档流之中,所以不论别的元素设置z-index多高,都只会被object元素遮住而无法看到。 object元素代码如下 &lt;object id="EZUIKit" width="400" height="4...

  • 使用activeX插件对象object滚动有重影

      使用activeX插件对象object滚动有重影   &amp;lt;object style=&quot;width:0;&quot; id=&quot;abc&quot; classid=&quot;CLSID:D3E3970F-2927-9680-BBB4-5D0889909DF6&quot; codebase=&quot;activex/OAX339.CAB#version=2,0,3,43&quot;&amp;gt; &amp;lt;/object&amp;gt;   把widt

  • javascript调用系统命令(基于ActiveXObject)

    1.创建可执行cmd命令的窗口var cmd = new ActiveXObject(&quot;WScript.Shell&quot;);var cmdStr = &quot;cmd /c D: &amp;amp;&amp;amp; cd D:/debug/debug &amp;amp;&amp;amp; java -jar getBaseInfo.jar getinfo&quot;;cmd.run(cmdStr, 0);cmd = null;var cmd = n...

  • ActiveXObject 安装

    将后缀名为ocx的文件拷贝至目录 c:\Windows\SysWOW64\。执行如下命令,进行注册:regsvr32 c:\Windows\SysWOW64\x.ocx 转载于:https://www.cnblogs.com/Currention/p/11024354.html

Global site tag (gtag.js) - Google Analytics