工作之中寫過兩個播放器,大同小異,由于需要的功能不是太多,github上的功能都比較多,正好想著順手練練技術,于是手擼過兩個播放器。播放器這個東西吧,說難不難,因為播放內核基本都是用的ijk,自己寫其實就是在把UI和功能做個封裝,說簡單吧,也沒那么簡單。主要是功能太多了,經常寫著寫著就寫亂了。每次寫都是寫之前胸有成竹,然后越寫腦子越亂。通過這次寫播放器的過程,簡單記錄一下為什么會產生這種問題,以及怎么解決方式,還有播放器設計的一些經驗。
API設計
其實就一句話,功能繁多,難以界定。
咱們先說基礎播放View,基礎播放View一般是由三個部分組成:播放器內核(IPlayer),渲染層(IRender),跟布局(ViewRoot,也就是封裝起來BasePlayView)。由于播放器內核、渲染層的實現都有很多種,所以我們一般來說是把這兩個模塊各自抽象一個接口或者抽象類出來,下邊的文章就用他們的接口名稱代指。現在持有一個IPlayer和IRender的ViewGroup(一般是FrameLayout)將會封裝成一個包含基礎播放能力,但是沒有控制層Ui的BasePlayView。
在這一步中,最容易引起設計混亂的原因就是功能的模塊劃分。
其中BasePlayView是最終對應用層暴露出api的對象,BasePlayView中80%的功能是由Player接口提供的,10%的功能是由Render接口提供的,10%的功能是自己實現的如全屏, 小屏播放。
IPlayer的功能基本是由播放器內核提供的,這就造成了一套繁瑣的功能轉發邏輯,最終暴露的功能——>IPlayer——>播放器內核 。之前都是先設計播放器的接口 在設計Render 然后最后寫BasePlayView。其實這樣的順序是錯誤的,我們應該先設計最終對外暴露的接口,在設計內部接口,有外到里,這樣就會大大減少后期出現設計不合理,功能短缺的問題,這個順序相當于先設計需求,在設計實現。我之前用的順序是先設計實現,在反推需求。很容易造成發現需求短缺 在重新設計接口。這樣很容易打亂原有的設計思路,導致初期設計合理的框架 為了實現某個需求不得不重新設計/生插進去一個功能。前者浪費時間,需要從新縷思路,會忘記之前的思路, 后者可能短期內不會暴露問題,但是會埋雷,必經是不符合前期設計的東西。初期功能盡量設計全 否則后期改動真的太麻煩了。還有就是功能的界定,需不需要我在最基礎的base層去實現,base功能定義是最基本的播放相關功能嗎,有哪些復雜功能需要分層設計,以及分層設計的思路,用繼承分層,還是利用多層View分層。哪些功能定義在控制器 哪些功能定義在播放器實現。這些都是功能劃分上的難點。這些沒有統一的定論,但是必須要有統一規范。
還有釋放的管理,由于設計到多個模塊釋放,需要規定哪些部分是由應用調用釋放的,哪些是內部根據自己處理的。
狀態管理
還是上邊的三個模塊,播放器的狀態是由這三個模塊共同產生的,不規劃好管理職責也會導致后期的代碼邏輯混亂
- 狀態必須由一個set api統一管理,這是最重要的
- 狀態集的定義基礎播放狀態,播放器功能狀態 等等提前定義 我需要管理多少狀態,其他狀態我是交由下層管理,還是通過回調事件分發出去。
- 是否允許疊加狀態,單狀態能滿足大部分情況,但是極少部分情況需要一個疊加狀態,比如播放器 全屏 小屏 普通廳 | 播放 暫停 等待 等等...
- 最好BasePlayView做狀態管理, Player 和Render 自身狀態通過回調通知BasePlayView
- 設計方便下層view 擴展狀態管理的邏輯
狀態切換的switch語句
狀態切換也是比較麻煩的一個事情,需要做大量的判斷,比如僅僅是一個start操作,就得根據是一個空閑播放器、正在播放中、暫停、播放完成、播放錯誤這五個狀態做不通的操作。這還沒考慮網絡狀態,是否在后臺等。
所以如果根據狀態的操作,以及操作產生的狀態切換這一快非常容易產生大量的switch或者if代碼。如果在這里代碼邏輯過于混亂,后期會引起很多難以控制的問題。我的在處理這個問題的時候用了一種類似狀態機的設計理念,但是并不是標準的狀態機。大致思路就是生成一個BaseState類,api是BasePlayView所有可能觸發的操作。比如 播放、暫停、發生錯誤、斷網、播放完成等。播放器持有一個BaseState對量,每當播放器切換一個狀態,都會切換BaseState具體的子類,然后將每當播放器接受到一個事件,則將事件轉發到當前BaseState去實現具體操作。因為此時的BaseState 對象中是知道自己是處于什么狀態的,也就避免了大量的邏輯判斷操作。
功能分層的設計
現在一個完善的播放器的功能種類繁多,在一個或者幾個類中完成所有功能代碼是不顯示,必須分層設計,我之前寫的播放器的功能分成是用繼承實現的,最基礎的播放器——>帶進度條——>手勢控制——>支持無縫銜接
不同功能的播放器 只能通過繼承合適層級的播放器 然后再去添加自己的代碼實現。擴展性很差,其他不熟悉內部實現的同事用起來很難受,只能用我封裝好的。
通過看DK播放器的實現,他使用組合控制器實現的功能分層。用一個沒有UI的根控制器做容器,跟控制器負責所有控制器的統一性功能,以及轉發播放器狀態變化。上述的功能實現就可以改成基礎功能播放器——>支持無縫銜接(這是播放器強相關功能,只能繼承實現)+進度條控制器+手勢控制器。耦合性更低 。通過組合不同的控制器就可實現不同的播放器。
如果把和播放器強相關的功能也用上訴組合的形式 分層實現 我現在的思路是 把所有的api調用改為命令模式,可以自定義命令處理器,鏈式調用 完成功能攔截,或者功能增強,自定義攔截器級別,類似Okhttp 攔截器設計。
管理模塊
這個角色基本上就是管理全局的播放器實例對象,或者提供幾個產生播放器實例的工廠方法。一般全局性的播放設置也放在這里。這里最容易發生的問題就是全局設置功能和播放器的設置API的設置沖突,原則是以播放器的設置為主。然后就是播放器的設置api必須包含全局的設置項,也就是假如全局可以設置ABC三個屬性,那么播放器的設置API最少要包含ABC三個設置API。當然如果設置不是針對個體播放器的可以不用滿足上述條件。
這篇文章基本就是個隨筆,想到哪寫到哪。記錄了下寫一個播放器想到的東西,給以后做功能設計留一些參考資料,不寫出來估計過幾天就忘了,年紀大了記性實在不行了。