網易云音樂開發踩坑之旅
發表時間:2021-1-11
發布人:葵宇科技
瀏覽次數:47
問題
最近跟著慕課網上的課程在做一個網易云音樂小程序,遇到了一個進度條回跳的 bug,這里記錄一下踩坑和解決的過程。
具體情況見下圖:
預期行為:在拖拽進度條之后,直接到達拖拽之后的位置
實際行為:在拖拽進度條之后,會首先回跳到拖拽之前的位置,然后再跳到拖拽之后的位置。
模擬調試的 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
在監聽歌曲的播放:
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
}
})
復制代碼
歌曲播放的時候,進度條要跟著走,這個函數就是用來實現該功能的。
解決方案
問題就在于:拖拽和歌曲播放是同時進行的,這兩者都會對綁定同一個狀態的數據進行修改,可能就是數據的沖突導致了最后渲染時回跳的 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
函數內部打印格式化的 currentTime
和 progress
的值,如果這兩者保持在差不多的水平,那么可以認為它們是同步的,如果某個時刻出現了很大的差距,那么就說明 currentTime
沒有及時進行更新(progress
是通過 onXchange
修改的,不會有問題)。
console.log('currentTime:' + this.timeFormat(backgroundAudioManager.currentTime))
console.log('progress:' + this.data.progress)
復制代碼
打印結果見下圖:
一開始沒有拖拽,所以理所當然, currentTime
和 progress
保持在差不多的水平。然后,注意看紅圈部分,紅圈的時刻我往后拖拽了進度條,所以可以看到 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 就算解決了。當然,可能還會有其它更好的解決方式,后續我會找個時間再看下能不能進行優化和改進,有思路的大佬也歡迎留言指點。小程序的坑著實不少,但是我覺得應該享受這種踩坑后又從坑里爬出來的感覺。最后要特別感謝群里的 @「瘋子」大佬,多虧他的提醒,讓我定位到問題的關鍵部位。