背景
开发的一个项目遇到一个崩溃,当应用在播放视频和音频时,一旦音频焦点被其他应用抢夺,应用直接崩溃,崩溃信息为
android.view.ViewRootImpl$CalledFromWrongThreadException
Only the original thread that created a view hierarchy can touch its views.
很明显,有代码在子线程操作了UI。通过崩溃行数定位到是项目引用的JZVD
视频播放库的一处回调中,操作了UI导致的,见图一。
那么,为什么这处回调会在子线程执行呢?下面内容建议搭配AudioManager
源码进行查看
调研
(以下代码为sdk30中的AudioManager
源码)
题外话:
AudioManager
属于系统的音频服务类,所以需要在sdk的AudioManager
类中断点,但是发现在真机上总是无法进入断点,尝试修改compileSdkVersion
和targetSdkVersion
等测试后发现,在模拟器上运行软件,然后在AS中打开...\SDK\sources\android-30\android\media\AudioManager.java
就可以成功进入断点,其中android-30
要和模拟器的安卓版本一致
通过查看函数调用,发现是AudioManager
类中的内部类ServiceEventHandlerDelegate
中调用处理的,见图二。
而ServiceEventHandlerDelegate
是AudioManager
的成员变量,会在你的应用第一次获取AudioManager
音频服务时创建,即图三。
在图二中的代码可以看出来,如果音频服务的第一次创建时是在子线程中,并且调用了Looper.loop()
,那么ServiceEventHandlerDelegate
中的mHandler
所使用的looper
即为子线程的,这样后续收到的的消息便会在子线程中进行回调。那么具体的消息是在哪里发送的呢?
由上图图四可以看到,是AudioManager
的一个成员变量mAudioFocusDispatcher
发送了所有的焦点改变信息。
而mAudioFocusDispatcher
是在public int requestAudioFocus(@NonNull AudioFocusRequest afr, @Nullable AudioPolicy ap)
这个函数中进行注册的,见图五。
由此可见,当系统底层的音频焦点发生改变,变会通知到mAudioFocusDispatcher
,然后在mAudioFocusDispatcher
中的dispatchAudioFocusChange(int focusChange, String id)
回调中进行通知。
但是,在图四的回调中,可以看到系统会先判断一下findFocusRequestInfo(id)
获得的FocusRequestInfo fri
的mHandler
是否等于null
,等于null
的时候才会通知成员变量ServiceEventHandlerDelegate
中的mHandler
。那么FocusRequestInfo fri
的mHandler
是哪里来的呢?
图五中还有一处标注的registerAudioFocusRequest(afr)
,即图六。
这个函数用于将
FocusRequestInfo
放到mAudioFocusIdListenerMap
中,而FocusRequestInfo
的构造方法中判断了一下AudioFocusRequest afr
的getOnAudioFocusChangeListenerHandler()
函数返回的Handler
是否为null
,如果为null
,则FocusRequestInfo
的mHandler
也为null
。
而在AudioFocusRequest
中,只要我们通过setOnAudioFocusChangeListener
设置了监听,它的mListenerHandler
都为null
,见图七。
总结
总的来说,我们在应用第一次获取AudioManager
实例的时候,创建了一个ServiceEventHandlerDelegate
实例,构造参数中的handler
为null
,所以需要判断Looper.myLooper()
是否为null,如果为null
则会在主线程创建mHandler
,如果不为空,则会使用当前的Looper
,如果当前Looper
是在子线程,那么后续的消息都会在子线程中收到。项目中我们一般会通过mAudioManager.requestAudioFocus(mFocusRequest)
进行监听,如图八。
这样会一步步调用到图五所示的函数,该函数中的registerAudioFocusRequest
函数里,通过getIdForAudioFocusListener
将当前listener存到了mAudioFocusIdListenerMap
,然后把mAudioFocusDispatcher
设置为底层音频服务焦点分发的回调。当焦点改变时,则会通过mServiceEventHandlerDelegate
的mHandler
发送Message
,告知当前的焦点信息。mServiceEventHandlerDelegate
收到后,通过findFocusRequestInfo
获取当前需要通知出去的回调,即mAudioFocusIdListenerMap
中对应的值,然后我们上层的监听便收到了相应的焦点改变信息。
最终通过断点,发现我们使用的一个第三方SDK在应用启动时进行了自动初始化,它在子线程中获取了ApplicationContext级别的音频服务,从而导致JZVD
的onAudioFocusChangeListener
在子线程中执行,里面的函数操作了UI导致了崩溃。解决方法是要么让第三方SDK修改初始化方法,把全局音频服务放到主线程中获取,要么修改项目中所有的音频焦点监听的回调,全部切换到主线程中操作UI