記一次小程序開發的踩坑之旅

問題

最近跟着慕課網上的課程在作一個網易雲音樂小程序,遇到了一個進度條回跳的 bug,這裏記錄一下踩坑和解決的過程。html

具體狀況見下圖:小程序

預期行爲:在拖拽進度條以後,直接到達拖拽以後的位置異步

實際行爲:在拖拽進度條以後,會首先回跳到拖拽以前的位置,而後再跳到拖拽以後的位置。ide

模擬調試的 bug

代碼邏輯

不管如何,先來看一下代碼的邏輯:函數

頁面結構以下,左右兩個 text 顯示時間就不說了,主要是中間的進度條。這個進度條沒有使用小程序原生提供的 slider 來作,而是採用 movable-area 和 movable-view 相結合的方式,movable-area 劃出了一塊可供滑動的區域,而 movable-view 則是中間能夠拖拽的滑塊。拖拽滑塊的時候會有個 x 來記錄拖拽距離,同時綁定 onXChange 事件監聽 x 的變化,綁定 onTouchEnd 事件監聽拖拽鬆手的動做。另外,下面還有一個 progress 組件,這個是用來顯示進度的,已經播放的進度給個白色樣式。性能

<view class="container">
  <text class="time">{{showtime.currentTime}}</text>
  <view class="control">
    <movable-area class="movable-area">
      <movable-view class="movable-view" direction="horizontal" 
      damping="1000" x="{{movableDist}}"
      bindchange="onXchange" bindtouchend="onTouchEnd">  
      </movable-view>  
    </movable-area>
    <progress percent="{{progress}}" stroke-width="4" backgroundColor="#969696" activeColor="#fff"></progress>
  </view>
  <text class="time">{{showtime.totalTime}}</text>  
</view>

一旦肯定 x 的變化來源於用戶的拖拽,就在onXChange 里根據比例關係設置好進度。這裏要注意的是,在用戶拖拽沒鬆手的時候先不進行 setData 渲染視圖層的操做 —— 由於用戶可能會頻繁進行拖拽,咱們要避免頻繁的 setData 帶來的性能損耗。因此,這裏只是把數據保存下來,等待渲染。測試

onXchange(event){
    if(event.detail.source == "touch"){  
        ratio = event.detail.x / (movableAreaWidth - movableViewWidth) 
        this.data.progress = ratio * 100    
        this.data.movableDist = event.detail.x
    }
},

用戶一旦鬆手,基本就能夠肯定他已經把滑塊拖拽到了目標位置,這時候就進行正式的 setData 操做,同時調用 seek 方法讓歌曲跳轉到對應的位置去播放優化

onTouchEnd(){      
    let toSec = totalSec * ratio
    this.setData({
        progress:this.data.progress,
        movableDist: this.data.movableDist,
        ['showtime.currentTime']: this.timeFormat(toSec)
    })   
    backgroundAudioManager.seek(toSec)   
},

目前來看,好像並無什麼問題。不過別忘了,咱們還有一個 onTimeUpdate 在監聽歌曲的播放:this

backgroundAudioManager.onTimeUpdate(() => {
    let currentTime = backgroundAudioManager.currentTime          
    // 獲取當前激活時刻
    let sec = currentTime.toString().split('.')[0]
    // 設置movableview進度
    let movableDist = (movableAreaWidth - movableViewWidth) * currentTime / totalSec
    // 設置progress-bar進度
    let progress = 100 * currentTime / totalSec
    // 賦值
    if(compareSec != sec){
        this.setData({
            movableDist,
            progress,
            ['showtime.currentTime']: this.timeFormat(currentTime)
        })
        compareSec = sec
    }
})

歌曲播放的時候,進度條要跟着走,這個函數就是用來實現該功能的。spa

解決方案

問題就在於:拖拽和歌曲播放是同時進行的,這二者都會對綁定同一個狀態的數據進行修改,可能就是數據的衝突致使了最後渲染時回跳的 bug。

解決的方案很簡單,這裏參考視頻的作法。其實很像 OS 中的進程互斥(這麼說不許確,但能夠近似理解)問題,進度條就至關因而互斥資源,咱們只要保證一個時間段內只有一個操做能夠修改進度條就行了。具體作法是聲明一個變量 isMoving 做爲「鎖」,在拖拽的時候置爲 true,並限制此時 onTimeUpdate 沒法修改數據;而在鬆手後置爲 false,並調用 seek 跳轉到音樂的某個播放位置 A。因爲對 onTimeUpdate 來講,他獲取的 currentTime 也是 A 位置對應的時間,這樣就不會發生衝突了。

修改代碼後再來看一下拖拽效果,發現確實沒有回跳的 bug 了:

你覺得事情就這麼結束了嗎?No~~

真機調試的 bug

在肯定模擬調試沒問題的狀況下,我打開手機進行真機調試,詭異的是,這個 bug 再次出現了,而且機率幾乎是 100%,這怎麼能忍呢?因而繼續想方法解決。

在前面說過,「調用 seek 跳轉到音樂的某個播放位置 A,對於 onTimeUpdate 來講,他獲取的 currentTime 也是 A 位置對應的時間。」 在真機調試的場景下並非這樣。

延遲更新的問題

咱們假設一下,調用 seek 進行跳轉後,onTimeUpdate 內部獲取的 currentTime 不是當前時間,而仍然是跳轉前的時間,也就是說它的時間沒有更新過來,那麼按照這個時間計算的數據最後渲染到進度條上,咱們看到的就還會是拖拽以前的進度條,而在稍後,時間更新過來了,進度條再次跳回到拖拽以後的位置。若是真的是這樣,或許就能夠解釋回跳的緣由了。那麼怎麼驗證呢?

咱們能夠在 onTimeUpdate 函數內部打印格式化的 currentTimeprogress 的值,若是這二者保持在差很少的水平,那麼能夠認爲它們是同步的,若是某個時刻出現了很大的差距,那麼就說明 currentTime 沒有及時進行更新(progress 是經過 onXchange 修改的,不會有問題)。

console.log('currentTime:' + this.timeFormat(backgroundAudioManager.currentTime))
console.log('progress:' + this.data.progress)

打印結果見下圖:

一開始沒有拖拽,因此理所固然, currentTimeprogress 保持在差很少的水平。而後,注意看紅圈部分,紅圈的時刻我日後拖拽了進度條,因此能夠看到 progress 忽然變大了,可是這時候的 currentTime 居然沒有跟着改變(仍然是一個很小的數)!這就驗證了上面的假設了,由於 currentTime 沒有及時更新,而它又影響着其它數據,因此致使進度條又跳回到以前的位置,而稍後 currentTime 更新了,因此時間又從 00:07 驟增到 02:11,此後才恢復正常。

不過,爲何在真機調試下就會有這個「延遲更新」的問題呢?一開始我還猜測這是由於 seek 是異步的,onTimeUpdate 搶先它執行了,但通過測試發現它實際上是同步的。因此,或許是由於真機調試下有延遲?這個先無論了,如今咱們先看一下怎麼解決這個 bug。

解決方案

問題的根源在於,咱們在 onTimeUpdate 中是拿 currentTime 做爲標準去進行數據修改的,而且認定 currentTime 是正確的數據,但其實,因爲延遲更新的問題,這個數據有時候是錯誤的。因此咱們能夠作一個判斷,一旦發現數據是錯誤的(沒更新過來),咱們就改用 progress 做爲標準去進行數據修改(progress 不會出錯)

PS:爲何不統一以 progress 做爲標準呢?由於在不拖拽的狀況下,progress 是基於 currentTime 進行計算的,因此正常狀況仍是得用 currentTime

如何判斷數據是錯誤的呢?這裏用了一個比較笨+不優雅的方法:在調用 onTimeUpdate 的時候,拿到實際的當前秒數以及基於 progress 計算的理想的當前秒數。通過測試發現,正常狀況下這二者的誤差不會大於2,而在不正常的狀況下(好比截圖紅圈部分),這二者相差會很大,彼此的差距大概就是咱們拖動進度條先後的差距。

這樣,咱們就能夠把代碼改爲:

backgroundAudioManager.onTimeUpdate(() => {
    // 不拖拽的時候才setData
    if(!isMoving){         
        let currentTime = 0
        if(Math.abs(backgroundAudioManager.currentTime - totalSec * this.data.progress/100) < 2){
            console.log('同步')
            currentTime = backgroundAudioManager.currentTime
        } else {
            console.log('不一樣步')
            currentTime = totalSec * this.data.progress/100     
        }
        // 獲取當前激活時刻
        let sec = currentTime.toString().split('.')[0]
        // 設置movableview進度
        let movableDist = (movableAreaWidth - movableViewWidth) * currentTime / totalSec
        // 設置progress-bar進度
        let progress = 100 * currentTime / totalSec
        // 賦值
        if(compareSec != sec){
            this.setData({
                movableDist,
                progress,
                ['showtime.currentTime']: this.timeFormat(currentTime)
            })
            compareSec = sec
        }
    }
})

理論上好像說得過去,實際效果如何呢?真機調試看一下:

由於我是錄屏而後轉成 GIF 的,幀數比較低,可是通過反覆測試,確實沒有進度條回跳的 bug 了。

到這裏,bug 就算解決了。固然,可能還會有其它更好的解決方式,後續我會找個時間再看下能不能進行優化和改進,有思路的大佬也歡迎留言指點。小程序的坑着實很多,可是我以爲應該享受這種踩坑後又從坑裏爬出來的感受。最後要特別感謝羣裏的 @瘋子大佬,多虧他的提醒,讓我定位到問題的關鍵部位。

相關文章
相關標籤/搜索