Android设置中音量条拖动异常解决方法

在Android P,设置-->声音中,通过拖动SeekBar设置音量,尤其是铃声音量时存在以下三个问题:

1、滑动条不跟手,存在回弹的现象。

2、偶发性的滑动条所在的位置与实际值不相符。

3、偶发性的,滑动铃声音量条时,闹钟音量也跟着滑动。

对问题日志分析没有获得有价值的信息,通过查看代码发现,滑动条在Settings是一个叫VolumeSeekBarPreference的组件,其中铃声对应的控制器为RingVolumeSeekBarPreferenceController,仔细查看两者代码后发现,VolumeSeekBarPreference将绝大多数的关于SeekBar的操作都委托给了一个SeekBarVolumizer的对象,该对象位于frameworks\base\core\java\android\preference\SeekBarVolumizer.java

经过阅读发现,该对象实现了对SeekBar的监听,存储,更新等操作,并实现了预览音量效果的播放,但是没有实现停止播放,停止播放由SeekBarVolumizer的使用者通过其提供的回调接口及函数自行实现,Settings中是在SoundSettings中实现了该功能,通过实现了CallBack接口来实现相关功能,读者可自行查阅。

SeekBarVolumizer是一个涉及多个线程的类,将其功能性代码都注释掉后,发现SeekBar的滑动是正常的(当然所有功能也都失效了。。。),这可以说明上述问题确实是因为SeekBarVolumizer中的功能性代码引起的。

对代码按照其功能进行阅读分类,发现SeekBarVolumizer主要涉及3个线程,其中各个线程间通过Message或广播实现通信。

  1、UI线程:该线程负责接受SeekBar的滑动事件,并将SeekBar的最新值更新到mLastProgress变量中,然后给mHandler发送一个消息MSG_SET_STREAM_VOLUME消息。UI线程还有一个mUiHandler,用于根据Message中携带的值来更新mLastProgress。

  2、HandlerThread线程:该线程与mHandler绑定,当收到MSG_SET_STREAM_VOLUME消息时,将mLastProgress的值存储到AudioManager中。AudioManager在成功存值后,会发送一个VOLUME_CHANED广播,该广播携带了音量的类型(streamType)和音量值(streamValue)。

  3、Receiver线程:顾名思义这是一个广播接收器(BroadcastReceiver),该广播接收器接收到AudioManager发出的广播后,从中取出音量的类型(streamType)和音量值(streamValue),使用其构建一个MSG_UPDATE_SLIDER消息,并将该消息发送给mUiHandler。

至此完成一个完整的循环,SeekBarVolumizer试图利用消息/广播的发送先于接受的happens-before关系,来确定一个1-2-3-1的时序关系(如下图),从而实现线程安全。在单次孤立的点击事件时,这个是可以良好工作的,但是当连续快速点击(或滑动)时,则会出现问题。

考虑如下情况:

   1、假设滑动了两个值7、6,那么当UI线程将mLastProgress更新为7,并通知mHandler存入该值后,其接着去处理6,此时会将mLastProgress更新为6,UI线程进入空闲状态。

   2、此时值7已被存入,然后mHandler因为CPU时间耗尽被挂起。

   3、Receiver收到广播并从消息中取出数据,数据值为刚存入的7,将该数据发送给mUiHandler

   4、mUiHandler将mLastProgress更新为7,并将SeekBar更新为7。此时读者应该发现了问题,刚才的6因为mHandler挂起还没被存起来,但是就已经被Ui线程更新为了7,  6丢失了,在现象上就是滑到了6,然后又回弹到了7,也就是问题1的发生原因。。。

在上述时序图中,

 黑线代表线程内部的时序,红线代表线程间“期望”的时序,SeekBarVolumizer希望通过线程间Message的接收和处理这种天然的先后顺序,来保证UI线程中过程2始终先于过程1,在慢速单次点击滑动条时,整个过程的时序是这种理想时序;但是因为过程2和过程1都使用了mLastProgress,过程1用其表征progress的当前值,过程2用其更新SeekBar,当快速点击SeekBar或者滑动时,会出现旧值覆盖新值,导致新值丢失的问题,以下列例子为例。

    1、当滑动条从右向左滑动时,依次会触发onProgressChanged方法,并依次将mLastProgress设置为7,6,5,4,3,2,1,0;

    2、假设到手滑到0处是,UI线程将mLastProgress设置为0,并向HandlerThread发送一个消息MSG_SET要求将0存起来,但是此时HandlerThread正在存储3,则MSG_SET入队列等待。

    3、HandlerThread存储为3之后,假设此时HandlerThread的cpu时间耗尽开始等待。

    4、Receiver线程收到广播,并从广播中取出3,将这个3构建一个消息发送给UI线程。

    5、UI线程从消息中取出3,并将mLastProgress更新为3,并更新SeekBar的位置为3。

    6、此时HandlerThread重新获得cpu时间,从mLastProgress中取值并存储,但是此时mLastProgress已经在步骤5中被覆盖掉了,原先的0已经丢失,只能再把3存了一遍。

   因此这个异常导致的现象就是,在滑动SeekBar的过程中,滑动条不跟手,并且会自动往后跳,就像上面的例子中,明明滑到了0,但是最后SeekBar会自己再跳回到3。

问题2的原因则就比较诡异了,updateSeekBar()方法是具体的执行SeekBar的更新方法。

protected void updateSeekBar() {
        final boolean zenMuted = isZenMuted();//获取当前
        mSeekBar.setEnabled(!zenMuted);//如果为勿扰或静音模式,则经SeekBar设置不可用,否则可用
        if (zenMuted) {//如果是静音或勿扰模式,将mSeekBar设置为上次静音前的音量
            mSeekBar.setProgress(mLastAudibleStreamVolume, true);
        } else if (mNotificationOrRing && mRingerMode == AudioManager.RINGER_MODE_VIBRATE) {
            //如果当前铃声为通知铃声并且当前铃声模式为震动,则将mSeekBar设为0
            mSeekBar.setProgress(0, true);
        } else if (mMuted) {
            //如果当前是静音,则将mSeekBar设为0
            mSeekBar.setProgress(0, true);
        } else {
            mSeekBar.setProgress(mLastProgress > -1 ? mLastProgress : mOriginalStreamVolume, true);
        }

        Log.d(TAG, "mLastProgress  = " + mLastProgress  + ", seekBar's position = " + mSeekBar.getProgress());
    }

在该方法中添加上述日志后,日志打印mLastProgress  = 0 , seekBar's position = 0;这说明seekbar要被设置的位置是0,并且SeekBar也认为自己已经设置到了0,然而实际上SeekBar的小圆点却在滑动条中间的位置,并没有跟随手指滑动过去。

查看setProgress(int, boolean)的方法定义,boolean代表的是开启过渡动画,使滑动条的设置像是手滑过去的一样,因此怀疑是动画出现了问题,(抱着试一试的态度)将setProgress修改为了setProgress(int ),成功修复问题2。

问题3则更加诡异:

在收到音量发生改变的广播时,Receiver使用下列方法进行广播匹配,决定是否利用该广播更新进度条

private void updateVolumeSlider(int streamType, int streamValue) {
            /*
            判断铃声类型与该实例绑定的类型是否一致
              1、如果当前实例绑定的铃声类型是通知类铃声,则判断传入的音量类型是否为通知类铃声,如果也为通知类铃声则认为两者一致
              否则
              2、传入的铃声类型是否与该实例绑定的铃声类型一致,如果一致则返回true
             */
            final boolean streamMatch = mNotificationOrRing ? isNotificationOrRing(streamType)
                    : (streamType == mStreamType);

            if (mSeekBar != null && streamMatch && streamValue != -1) {
                /*
                如果SeekBar存在 并且 广播收到的铃声类型与当前类绑定的也匹配 并且 音量不为-1,则执行下列逻辑
                  1、判断当前铃声是否要静音,如果已经被静音或者传入的值为0,则说明要被静音
                  2、想mUiHandler发送消息来通知音量已经被更改
                 */
                final boolean muted = mAudioManager.isStreamMute(mStreamType)
                        || streamValue == 0;
                mUiHandler.postUpdateSlider(streamValue, mLastAudibleStreamVolume, muted);
            }
        }

一开始怀疑是因为streamMatch的判断有问题,然后就直接使用streamType == mStreamType进行判定,发现没有改变问题,此时就怀疑广播有问题,然后就在onReceive中将收到的广播都打印出来,发现偶发性的,AudioManager会在存入streamType为2(铃声音量)的值后,发出了streamType为2(铃声)、4(闹钟)、5三个广播,并且三个广播中携带的值都是刚才存入的铃声值(诡异的是闹钟的值实际上并没有改变,只是发出了一个虚假的广播),因此问题3是由于AudioManager广播异常导致的,这个问题就不好在SeekBarVolumizer上进行修改,于是转交给多媒体的同事进行修改。

总结:

1、问题1主要是因为mLastProgress起了两个作用,既记录onProgressChange传入的值,也记录广播反馈的值,因此可以通过增加一个mCurrentProgress来记录onProgressChange传入的值,mHandler存值也不再从mLastProgress中读取,改为从mCurrentProgress读取,即mCurrentProgress负责记录要被存入的值,mLastProgress负责记录收到的收到的广播值并利用该值设置SeekBar。

2、问题2当前的解决方法就是讲setProgress(int, boolean)改为setProgress(int),几乎没有什么影响,唯一的区别在于滑动条没有了平滑过渡动画,不过不明显,如果需要进一步修改则需要在SeekBar中进行修改。(使用AndroidStudio编写了一个普通的使用SeekBar的程序,改程序将SeekBar的值存起来并发送广播,广播接收器利用该值更新SeekBar,复现了问题2,不过因为逻辑比较简单,所以复现概率较低,要快速反复滑动才有可能复现,所以确实是SeekBar自身存在异常)

3、该问题是因为AudioManager及其关联服务发出的广播异常导致的,交多媒体修复。

修改后的SeekBarVolumizer.java稍后上传

资源地址:https://download.csdn.net/download/ironlzz/10737739


--------------------- 
作者:ironlzz 
原文:https://blog.csdn.net/ironlzz/article/details/83279659 

猜你喜欢

转载自blog.csdn.net/CodingNotes/article/details/84344706