直播短影片系統原始碼,一步步實現快取機制
設計的伊始談到,為了保證解耦, 我們希望快取機制 不能修改播放器原始碼 ,但 MediaPlayer 如何在不改原始碼的情況下,將自身的快取載入邏輯交給我們的 CacheService 呢?
如下述程式碼中所展示的,這種實現似乎無法避免:
public class MyMediaPlayer extends MediaPlayer { public final CacheService mProxy; @Override public void setDataSource(String url) { // super.setDataSource(url); mProxy.setDataSource(url); } }
必須承認,這也是一種與播放器的耦合,不能修改播放器原始碼 的設定似乎並不符合常理。
這裡體現出了作者本身優秀的創造力,透過建立一個裝置的本地代理服務 CacheService,在將影片資源的url交給播放器之前,先進行本地的一次轉換,並將初始的url作為引數,拼接在本地代理的url上:
1.建立本地代理:比如 http://127.0.0.1:8090
2.拿到要快取的影片地址,比如 https://xxx.mp4
3.拼接為新的地址:http://127.0.0.1:8090/https://xxx.mp4
拿到新的 url 並交給任意播放器後,播放器的載入都指向本地服務的新地址——即透過 Socket 連線建立的本地服務 CacheService,後者透過解析出請求中真正的 https://xxx.mp4 地址,建立對應的下載任務,並從下載的檔案快取中,讀取 buffer 返回給播放器;同時,監控整個流程的 CacheService 響應式地回撥過程中所有大大小小的事件。
經過這樣設計,整個流程的呼叫變得非常簡單:
public class MainActivity extends Activity { public final MediaPlayer mPlayer; @Override public void playVideo(String url) { final String proxyUrl = VideoUtils.getProxyUrl(url); // url = https://xxx.mp4 // proxyUrl = http://127.0.0.1:8090/https://xxx.mp4 mPlayer.setDataSource(proxyUrl); } }
接下來,筆者透過虛擬碼的形式,簡單闡述下建立本地代理連線的過程。
上文提到的本地服務 CacheService在建立時,會自動初始化一個本地代理伺服器,配置ip和自動分配埠號,這之後,服務完成初步建立,並立即開啟一個執行緒,等待接收客戶端的後續連線。
// 實際類名 HttpProxyCacheServer.java public final class CacheService { private CacheService(Config config) { // 初始化ip和埠號 InetAddress inetAddress = InetAddress.getByName("127.0.0.1"); this.serverSocket = new ServerSocket(0, 8, inetAddress); this.port = serverSocket.getLocalPort(); // 開啟新的執行緒,等待後續接收客戶端的連線 this.waitConnectionThread = new Thread(new WaitRequestsRunnable()); this.waitConnectionThread.start(); } }
本地服務建立完畢,當使用者嘗試播放音影片時,播放器實際上訪問類似 http://127.0.0.1:8009/https://xxx.mp4 的地址,這時我們的 CacheService 中接到了對應的訊息。
針對每一次請求,我們都能解析到真實音影片檔案的地址(https://xxx.mp4),為了提高複用性,我們宣告一個HttpProxyCache類,為每一個音影片配置一個對應的 HttpProxyCache 以進行管理:
class HttpProxyCache extends ProxyCache { // 影片資源的url地址 private final HttpUrlSource source; // 影片資源的本地檔案資訊 private final FileCache cache; }
實際上還不夠,我們還需要針對每個音影片快取過程的回撥進行管理,因此,基於此再封裝一層,使用 HttpProxyCacheServerClients 管理一個音影片資源:
final class HttpProxyCacheServerClients { private final String url; // 影片資源url private volatile HttpProxyCache proxyCache; // 快取資訊 private final List<CacheListener> listeners = new CopyOnWriteArrayList<>(); // 快取監聽 }
簡單概括一下,針對一次新的音影片資源載入,會構建一個新的 HttpProxyCacheServerClients,內部除了相關資訊的成員,還包含了 HttpProxyCache 物件用於讀取和載入快取。
抽象地看待音影片的源,分為 遠端音影片資源 和 本地音影片資源,當不使用快取時,必然會從遠端進行下載,並不斷將音影片的流透過 Socket 向播放器傳輸。
這裡我們將 源 抽象為 Source:
public interface Source { // 建立開啟資源 void open(long offset) throws ProxyCacheException; // 獲取音影片的長度 long length() throws ProxyCacheException; // 不斷讀取音影片資料 int read(byte[] buffer) throws ProxyCacheException; // 關閉釋放資源 void close() throws ProxyCacheException; }
對於遠端載入的完整流程,本質上就是建立、開啟、讀取和關閉一個遠端連線 HttpURLConnection的過程,核心程式碼如下:
public class HttpUrlSource implements Source { @Override public void open(long offset){ HttpURLConnection connection = openConnection(offset, -1); } @Override public int read(byte[] buffer){ return inputStream.read(buffer, 0, buffer.length); } // ... }
更多的時候,無論音影片資源是否已下載,我們都希望透過快取統一載入管理:
1、檔案已下載:直接讀取本地檔案,將資料透過Socket不斷傳回給播放器;
2、檔案未下載:新建一個本地檔案,並開啟遠端下載任務,下載過程中,資料流不斷湧入本地檔案,本地檔案大小、下載進度的變更都會響應式通知上層;除此之外,新的音影片流資料會透過Socket不斷傳回給播放器,播放器也會不斷的推進播放進度。
由此可見,無論檔案是否下載,快取流程都是圍繞 本地快取檔案 進行的,這也符合軟體開發中的 唯一可信源 的概念。