閒魚直播flutter化實踐

語言: CN / TW / HK

背景

直播帶貨已成為近年來最熱的“風口”,已成為電商升級的新突破口。閒魚作為國內最大的二手交易平臺市場,直播帶貨也成為推動成交的強烈需求。但是閒魚直播原先接入外部提供的直播sdk,存在以下幾個痛點問題:

  1. 業務定製困難。接外部sdk都存在“改不動,不敢改”困境。目前閒魚直播和sdk提供方業務特點存在一定差異,產品提出的定製化需求難以得到滿足

  2. 雙端一致性難以保證。android,ios各提供一套sdk,兩端表現並非完全一致,存在bug也不敢輕易升級sdk(直播sdk還是2019年版本)

  3. 排查輿情問題難度大。目前直播sdk裡面日誌有哪些,以及日誌系統在哪裡都不清楚,有問題得求助別人排查。

因此閒魚想基於直播sdk的一些能力,做一套跨平臺直播sdk,減少開發維護成本。考慮到閒魚在flutter技術棧上有一定技術積累,新版直播通過Flutter實現跨平臺開發(支援ios,android),再接入閒魚的全鏈路日誌系統,能解決目前閒魚直播面臨的問題。

整體設計

基本原則

在設計開發之初,我們定下了幾條原則:

  1. 複用原生業務無關的核心能力: 這裡主要考慮到開發成本因素。比如底層影片編解碼能力,webview容器等這些,native已經做的相當成熟,如果在flutter重新做,勢必會大大延長開發時間。通過外掛化複用原生層能力,將加快開發進度

  2. native層儘量做薄,flutter層儘量做厚: 我們知道flutter是個移動UI框架,很擅長做UI。上一條原則強調複用原生能力,我們很容易陷入一個誤區:業務邏輯作為中間的地帶(不屬於UI,也不屬於底層核心能力),放到哪都可以。

如果放到native層,那麼flutter就會做的很薄,僅提供有限的介面能力。這樣會帶來一個問題:如果我們想修改一個業務邏輯,實際上還得改三處程式碼(android,ios,flutter),開發成本和原來差不多。

如果放到flutter層,那麼修改業務程式碼只需要改flutter一處即可,真正做到跨平臺。

功能劃分

對照線上老native閒魚直播間的很多功能,我們發現核心部分主要有:影片播放,評論,分享,關注,分享,寶貝口袋等等(下圖左)。這其中難點涉及到如何劃分:哪些是核心能力,必須依賴native能力?哪些能flutter化?

首先我們從視覺角度分析,可將直播間拆解為以下三層:播放渲染層,互動容器層,UI層(見下圖右)。

播放渲染層: 作用是渲染拉流資料,完成影片播放,業務無關。我們可以複用底層拉流和編解碼能力,播控控邏輯上移到flutter層。

互動渲染層: 基於動態容器完成渲染,完成一些互動玩法(例如直播間紅包,拍賣元件等)。這個動態容器是基於native層,但是依賴上層橋能力可以進行上移到flutter,完成動態化。

UI層: UI互動層,比如分享,評論,關注,點贊等以及feed流框架等。這層純UI互動,完全可以用flutter化。

最終我們得到如下新版flutter直播框架圖

關鍵模組實現

播放器

直播必然離不開影片播放,播放器作為核心能力,方案選型至關重要。目前開源的flutter播放器主要分為以下幾類:

  1. 官方提供的videoPlayer,以及基於官方外掛開發的 chewie,betterplayer,yoyo-player

  2. 基於ijkplayer開發的播放器,如fijkplayer等

播放器型別

介紹

特點

video_player

跨三端播放器(android/ios/web)

底層依賴Exoplayer(android),AVPlayer(ios)

官方支援,相容性好,迭代及時。api使用較為複雜;底層依賴難以切換

ijkplayer系列播放器

基於ijkplayer封裝的播放器外掛

相比官方外掛,迭代頻率低,點贊人數少些

引入第三方外掛播放器會帶來幾個問題:造成包體積增大,維護風險未知;閒魚底層播放器並非基exoplayer,這樣造成播放器管理不統一;引入外來庫容易造成庫版本衝突。因此我們決定自研一套基於閒魚環境的播放器。採用方案是基於外接紋理,將native建立的紋理id傳遞到flutter層進行展示,具體原理自行查閱,不在此展開。

其中,我們遇到一個難題:播放器的狀態管理

做過播放器業務的同學都知道,播放器內部是個狀態機。如果呼叫時序不合理,很容易發生crash。常見的做法是在播放器上再封裝一層sdk層,內部做狀態維護和管理,過濾掉不合法的狀態操作。

這次我們決定將sdk層業務邏輯全部上移到flutter層,但是flutter和native直接是通過methodChannel進行通訊的,methodChannel是非同步的。這會造成上層播放狀態和底層的播放狀態是不一致的。

考慮兩種case:

  1. 如果播放器呼叫load方法立馬呼叫play函式會不生效。這是因為播放器會認為非法狀態,直接跳過(下圖左)

  2. 頻繁呼叫play,pause容易造成頁面卡頓。實際上我們真正生效的是最後一次操作(下圖右)

// 影片prepare
void load() async {
...
}
// 播放
void play async() {
...
}


// 暫停
void pause async() {
...
}


// 情況1:載入中立即呼叫play
load();
play(); // 不生效


// 情況2:頻繁點選暫停/播放
play();
pause();
play();
pause();
....

呼叫不生效

執行緒卡頓

針對上述問題存在的問題,我們在flutter層做了更為細緻狀態管理。對於非同步呼叫加入了另外兩種狀態: 過渡態期望態。

例如:

當我們有載入動作load,會將播放器狀態設定為過渡態(load_pending),處於此狀態下將不會響應特定的操作(比如play);

如果此時呼叫了play方法,播放器僅僅會記錄下期望態wantedState=playing;

當載入完成後native會通知flutter層狀態改變,從load_pending->loaded。此時我們會校驗當前狀態是否和期望態是否相等。由於當前為loaded 不等於期望態playing,我們會幫使用者補上一次play操作(見下圖)。

同樣當頻繁呼叫play/pause方法時,我們會將播放器狀態設定成過渡態(play_pending/pause_pending),此狀態不響應播放暫停操作,而僅僅改變使用者的期望態;當native通知flutter層操作狀態改變完成後,再校驗一次期望態是否和當前狀態是否相等,如果不相等再做響應操作

過渡態/期望態

互動容器層

對於帶貨直播來說,互動玩法是個必不可少的重要元素。每逢重點促銷節日,各種運營活動接踵而來。一個不需要跟版,全版本覆蓋的動態層容器有著非常大的吸引力。我們參考了直播sdk中native實現方案,將動態層容器橋接到原生的h5容器(可以理解成webView)。

PlatformView橋接到動態容器層

事實上,我們將動態層容器橋接到native這是遠遠不夠的。我們最需要解決的問題是: 容器依賴的能力注入。

容器依賴能力就是h5互動元件通過jsBridge,獲取到原生native執行時環境資料能力。

原先直播sdk中將apiBridge能力設計成全域性單例,這在feed流的直播場景下存在一定隱患。例如從第一個直播間將滑到第二個直播間過程中,可能同時存在兩個native容器向apiBridge拿資料。如果這個資料是全域性公用的,那沒問題。但是有個apiBridge方法是獲取當前直播間詳情資料(getLiveDetailData),這必然造成有個直播間拿到的資料不正確。

因此我們在設計API Bridge的時候,採用多例項方案。每個直播間分配一個bridge例項(並不耗記憶體),同時為每個直播間建立一個單獨的methdChannel,native容器和bridge單獨走這個通道進行通訊。這樣便能徹底解決上面訊息的隱患問題(下圖左)。

我們將apidBridge邏輯徹底flutter化的同時,也提供了動態註冊api Bridge的能力,不需要修改native側程式碼(上圖右)。目前互動層容器已隨flutter直播間上線並且經歷了雙十一的考驗。目前線上執行的h5元件包括紅包元件,拍賣元件,更多直播元件等。

當然我們在實踐的過程也遇到很多坑:

  1. PlatfomView手勢衝突問題,見 《Flutter PlatformView 在閒魚直播業務中的實踐》

  2. PlatfromView黑屏問題。在OppoR15 容器出現動態層黑屏現象,定位原因是platformView背景是透明的情況下,容易復現。通過將官方提供AndroidView替換成PlatformViewLink可以解決這個問題,但是會引起畫面卡頓掉幀。

無縫小窗播放

直播場景下小窗播放並不是什麼新鮮事情。當從直播頁面切到後臺,螢幕上會顯示另外一個小窗顯示直播畫面。android平臺下,實現原理是通過windowManager將渲染層view add到window中去。

我們在實現第一版小窗播放的時候,是通過新起一個播放器的方式將畫面渲染到一個window 的SurfaceView中,由於載入耗時的影響,這會造成播放不連貫。

我們做了進一步的優化,複用原來的直播播放器,切換渲染層surface。將原來的外接紋理surface進行detach,attach小窗surface。這樣影片流並沒有中斷,只是切換了渲染層,做到了真正的無縫切換效果。

訊息通道

直播間存各種各樣訊息需要傳遞,可靠的底層訊息通道至關重要。我們需要抽離出單獨的訊息模組便於業務複用。訊息模組flutter化,抹平端差異。考慮到直播有相當成熟的訊息中介軟體,我們講訊息模組做成單獨的外掛,上層提供統一的訊息處理介面。

其中訊息傳遞免不了native和dart層通訊。考慮到訊息有兩大型別:控制類和資料類,我們採用了雙管道通訊,其中控制訊息類採用methodChannel(基於方法粒度);native層產生的資料訊息採用eventChannel通知到flutter層(頻繁通訊,單向)。

元件化

設計之初我們想做的是一個“直播場”,任何直播源都能接入進來。比如接入淘寶直播,優酷直播,或者閒魚自己的直播等等。但是不同來源的直播就會面臨資料協議不同,UI樣式也無法統一的難題。因此保留元件動態替換能力是非常有必要的。

常見做法是站在元件UI的角度,UI雛形下擁有一定的定製能力,由子類覆寫。比如元件種icon圖示的定製,圖示點選事件處理等等。這種繼承方式會犧牲一定的靈活性,定製化能力不夠強。

而我們站在直播能力的角度,為每個元件賦予不同能力,通過組合的方式進行組裝,這樣靈活性會更強。

目前閒魚直播間大致劃分成上面幾個元件:LivePage/LiveCell/LiveInteractive/LivePlayer/LiveComment等。每個元件會承擔一定能力角色,具體分工如下:

不同元件之間難免會有資料進行共享和通訊,這樣會造成一定的耦合。為了最大程度的降低這種耦合,這邊基於輕量級的Knight框架進行通訊,其基本原理仍然採用觀察者模式。比如:LivePage作為直播間詳情資料提供者,它會將共享資料宣告出去(declare),使用方(LiveCell/LiveInteractive)回通過require方法進行訂閱。當共享資料來源發生變化,會通知到使用方。

效果

目前新版直播間在7.2.40版本已經全量上線,總版本uv佔比達到九成以上,暫無線上輿情問題

經歷了雙十一流量峰值考驗,線上紅包問題取得超預期效果 間,給直播間使用者增長帶來很大促進作用。

最後

這篇文章系統講述了我們閒魚創新小組在flutter直播化所做的一些努力,希望能給大家帶來一些啟發和收穫。未來我們還有許多工作需要進一步探索:1 PlatformView 相容性問題需要解決,以及相關記憶體流暢度優化 2 做一款真正意義上的flutter播放器,將編解碼資料直接對接到flutter層,實現真正意義上的跨平臺。道阻且長,行則將至,行而不輟,則未來可期。

團隊介紹