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



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

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 呢,显然也是这样。没有头绪的时候,它就是我们救星啊。


Read full article from 从 Android 静音看正确的查找 bug 的姿势


No comments:

Post a Comment

Labels

Algorithm (219) Lucene (130) LeetCode (97) Database (36) Data Structure (33) text mining (28) Solr (27) java (27) Mathematical Algorithm (26) Difficult Algorithm (25) Logic Thinking (23) Puzzles (23) Bit Algorithms (22) Math (21) List (20) Dynamic Programming (19) Linux (19) Tree (18) Machine Learning (15) EPI (11) Queue (11) Smart Algorithm (11) Operating System (9) Java Basic (8) Recursive Algorithm (8) Stack (8) Eclipse (7) Scala (7) Tika (7) J2EE (6) Monitoring (6) Trie (6) Concurrency (5) Geometry Algorithm (5) Greedy Algorithm (5) Mahout (5) MySQL (5) xpost (5) C (4) Interview (4) Vi (4) regular expression (4) to-do (4) C++ (3) Chrome (3) Divide and Conquer (3) Graph Algorithm (3) Permutation (3) Powershell (3) Random (3) Segment Tree (3) UIMA (3) Union-Find (3) Video (3) Virtualization (3) Windows (3) XML (3) Advanced Data Structure (2) Android (2) Bash (2) Classic Algorithm (2) Debugging (2) Design Pattern (2) Google (2) Hadoop (2) Java Collections (2) Markov Chains (2) Probabilities (2) Shell (2) Site (2) Web Development (2) Workplace (2) angularjs (2) .Net (1) Amazon Interview (1) Android Studio (1) Array (1) Boilerpipe (1) Book Notes (1) ChromeOS (1) Chromebook (1) Codility (1) Desgin (1) Design (1) Divide and Conqure (1) GAE (1) Google Interview (1) Great Stuff (1) Hash (1) High Tech Companies (1) Improving (1) LifeTips (1) Maven (1) Network (1) Performance (1) Programming (1) Resources (1) Sampling (1) Sed (1) Smart Thinking (1) Sort (1) Spark (1) Stanford NLP (1) System Design (1) Trove (1) VIP (1) tools (1)

Popular Posts