记录使用第三方sdk导致AudioManager.OnAudioFocusChangeListener在子线程回调的探究

背景

开发的一个项目遇到一个崩溃,当应用在播放视频和音频时,一旦音频焦点被其他应用抢夺,应用直接崩溃,崩溃信息为

android.view.ViewRootImpl$CalledFromWrongThreadException
Only the original thread that created a view hierarchy can touch its views.

很明显,有代码在子线程操作了UI。通过崩溃行数定位到是项目引用的JZVD视频播放库的一处回调中,操作了UI导致的,见图一。

图一

记录使用第三方sdk导致AudioManager.OnAudioFocusChangeListener在子线程回调的探究_第1张图片

那么,为什么这处回调会在子线程执行呢?下面内容建议搭配AudioManager源码进行查看

调研

(以下代码为sdk30中的AudioManager源码)

题外话:AudioManager属于系统的音频服务类,所以需要在sdk的AudioManager类中断点,但是发现在真机上总是无法进入断点,尝试修改compileSdkVersiontargetSdkVersion等测试后发现,在模拟器上运行软件,然后在AS中打开...\SDK\sources\android-30\android\media\AudioManager.java就可以成功进入断点,其中android-30要和模拟器的安卓版本一致

通过查看函数调用,发现是AudioManager类中的内部类ServiceEventHandlerDelegate中调用处理的,见图二。

图二

记录使用第三方sdk导致AudioManager.OnAudioFocusChangeListener在子线程回调的探究_第2张图片

ServiceEventHandlerDelegateAudioManager的成员变量,会在你的应用第一次获取AudioManager音频服务时创建,即图三。

图三

图三
在图二中的代码可以看出来,如果音频服务的第一次创建时是在子线程中,并且调用了Looper.loop(),那么ServiceEventHandlerDelegate中的mHandler所使用的looper即为子线程的,这样后续收到的的消息便会在子线程中进行回调。那么具体的消息是在哪里发送的呢?

图四

记录使用第三方sdk导致AudioManager.OnAudioFocusChangeListener在子线程回调的探究_第3张图片
由上图图四可以看到,是AudioManager的一个成员变量mAudioFocusDispatcher发送了所有的焦点改变信息。
mAudioFocusDispatcher是在public int requestAudioFocus(@NonNull AudioFocusRequest afr, @Nullable AudioPolicy ap)这个函数中进行注册的,见图五。

图五

记录使用第三方sdk导致AudioManager.OnAudioFocusChangeListener在子线程回调的探究_第4张图片

由此可见,当系统底层的音频焦点发生改变,变会通知到mAudioFocusDispatcher,然后在mAudioFocusDispatcher中的dispatchAudioFocusChange(int focusChange, String id)回调中进行通知。
但是,在图四的回调中,可以看到系统会先判断一下findFocusRequestInfo(id)获得的FocusRequestInfo frimHandler是否等于null,等于null的时候才会通知成员变量ServiceEventHandlerDelegate中的mHandler。那么FocusRequestInfo frimHandler是哪里来的呢?
图五中还有一处标注的registerAudioFocusRequest(afr),即图六。

图六

记录使用第三方sdk导致AudioManager.OnAudioFocusChangeListener在子线程回调的探究_第5张图片这个函数用于将FocusRequestInfo放到mAudioFocusIdListenerMap中,而FocusRequestInfo的构造方法中判断了一下AudioFocusRequest afrgetOnAudioFocusChangeListenerHandler()函数返回的Handler是否为null,如果为null,则FocusRequestInfomHandler也为null
而在AudioFocusRequest中,只要我们通过setOnAudioFocusChangeListener设置了监听,它的mListenerHandler都为null,见图七。

图七

记录使用第三方sdk导致AudioManager.OnAudioFocusChangeListener在子线程回调的探究_第6张图片

总结

总的来说,我们在应用第一次获取AudioManager实例的时候,创建了一个ServiceEventHandlerDelegate实例,构造参数中的handlernull,所以需要判断Looper.myLooper()是否为null,如果为null则会在主线程创建mHandler,如果不为空,则会使用当前的Looper,如果当前Looper是在子线程,那么后续的消息都会在子线程中收到。项目中我们一般会通过mAudioManager.requestAudioFocus(mFocusRequest)进行监听,如图八。

图八

记录使用第三方sdk导致AudioManager.OnAudioFocusChangeListener在子线程回调的探究_第7张图片
这样会一步步调用到图五所示的函数,该函数中的registerAudioFocusRequest函数里,通过getIdForAudioFocusListener将当前listener存到了mAudioFocusIdListenerMap,然后把mAudioFocusDispatcher设置为底层音频服务焦点分发的回调。当焦点改变时,则会通过mServiceEventHandlerDelegatemHandler发送Message,告知当前的焦点信息。mServiceEventHandlerDelegate收到后,通过findFocusRequestInfo获取当前需要通知出去的回调,即mAudioFocusIdListenerMap中对应的值,然后我们上层的监听便收到了相应的焦点改变信息。

最终通过断点,发现我们使用的一个第三方SDK在应用启动时进行了自动初始化,它在子线程中获取了ApplicationContext级别的音频服务,从而导致JZVDonAudioFocusChangeListener在子线程中执行,里面的函数操作了UI导致了崩溃。解决方法是要么让第三方SDK修改初始化方法,把全局音频服务放到主线程中获取,要么修改项目中所有的音频焦点监听的回调,全部切换到主线程中操作UI

你可能感兴趣的:(android)