1. 程式人生 > >Android設定中音量條拖動異常解決方法

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稍後上傳


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