本文介紹AliPlayerKit策略系統的設計背景、核心概念、內建策略及使用方法。
設計背景
策略系統的設計源於對AUI Kits規模化落地中業務定製與架構演化矛盾的思考。
作為面向客戶的低代碼方案,AUI Kits需支援不同業務方對UI及核心流程的定製。若商務邏輯直接嵌入架構代碼,將造成高度耦合:架構升級時易引發代碼衝突、商務邏輯失效,甚至阻礙版本迭代。
本質上,此類邏輯屬於客戶業務需求而非架構能力。缺乏合理承載機制將導致業務差異侵入架構,威脅系統穩定性與可演化性。
策略系統為此而生:
將業務差異抽象為獨立擴充的策略單元。
解耦商務邏輯與架構核心。
業務方可在策略層擴充,避免侵入架構。
該機制使業務能力沉澱為系統化、可複用的體系,形成 可迭代、可複用、可擴充 的能力基礎,為AliPlayerKit長期演化提供架構支撐。
概念介紹
什麼是策略系統?
策略系統(Strategy System)是統一管理原則組件的架構機制,負責策略的註冊機制、生命週期管理、上下文注入及事件訂閱,並在播放器運行中統一管理原則的建立、啟動、暫停、重設與銷毀。
基於該機制,開發人員可按需啟用/禁用策略、快速實現自訂策略,或完全替換預設策略以滿足業務定製需求。
什麼是策略?
策略(Strategy)是播放器商務邏輯的封裝單元,實現特定業務能力或自訂邏輯(如首幀統計、卡頓檢測、流量保護、記憶播放)。
核心特徵:
單一職責:聚焦獨立功能目標。
事件驅動:通過事件匯流排響應播放器狀態。
獨立隔離:策略間互不干擾。
通過可插拔可組合的設計,非核心能力被抽象為策略單元,保持播放器核心簡潔,同時提供靈活擴充能力。
功能特性
解決問題
策略系統需解決的痛點:
維護困難:商務邏輯分散,難維護複用。
配置缺失:不同情境需不同邏輯組合,缺乏靈活配置機制。
源碼入侵:定製邏輯需修改架構源碼。
執行個體幹擾:多播放器情境下商務邏輯易串台。
核心價值
策略系統抽離並封裝播放器的監控與分析邏輯,支援客戶按需自主配置與組合。
使用方式 | 說明 | 優勢 |
使用預設策略 | 播放器組件使用官方預設策略。 | 簡化接入流程,開箱即用。 |
自訂策略 | 實現特定業務需求的策略。 | 滿足各類定製化業務需求。 |
不使用原則 | 僅使用播放能力。 | 純播放情境,無邏輯依賴。 |
架構優勢:
解耦:策略與播放器核心邏輯分離,職責清晰。
複用:策略可在不同播放器執行個體間複用。
可擴充:無需修改架構即可自訂策略。
隔離:策略間相互獨立,互不影響。
核心能力
能力 | 說明 |
事件驅動 | 策略通過事件匯流排訂閱播放器事件,響應式執行。 |
生命週期管理 | 統一管理原則的啟動、停止、重設、銷毀。 |
上下文注入 | 策略可通過上下文訪問播放器狀態和資料。 |
原廠模式 | 支援全域註冊策略工廠,自動應用於所有播放器執行個體。 |
內建策略
AliPlayerKit 提供了三個開箱即用的內建策略。
首幀耗時策略(FirstFrameStrategy)
統計播放器從準備階段(PREPARING)到首幀渲染完成的耗時,細分三個階段:
階段 | 說明 |
準備階段 | PREPARING → Prepared |
渲染階段 | Prepared → FirstFrameRendered |
總耗時 | PREPARING → FirstFrameRendered |
使用情境:效能監控、使用者體驗最佳化、播放器調優。
卡頓檢測策略(StutterDetectStrategy)
監控播放過程中的卡頓狀態,統計卡頓情況:
指標 | 說明 |
卡頓次數 | 卡頓發生的次數。 |
卡頓時間長度 | 每次卡頓的期間。 |
卡頓總時間長度 | 整個播放會話的卡頓時間長度總和。 |
有效播放時間長度 | 實際觀看時間長度(不含暫停和卡頓)。 |
使用情境:播放品質監控、使用者體驗分析。
流量保護原則(TrafficProtectionStrategy)
監聽網路狀態變化,在特定情境下提醒使用者:
情境 | 行為 |
WiFi轉換為移動網路 | 提示使用者正在使用流量播放。 |
起播時為移動網路。 | 提示使用者當前使用流量。 |
用情境:流量敏感情境、使用者關懷。
基礎使用
策略系統提供三種使用原則,開發人員可根據需求選擇合適的方式:
策略 | 說明 | 適用情境 |
使用預設策略 | 最簡單的使用方式,播放器組件自動註冊預設策略。 | 快速整合、標準播放情境。 |
自訂部分策略 | 只註冊特定策略,其他不使用。 | 局部定製、按需啟用。 |
完全自訂策略 | 自訂所有策略,建立完全個人化的監控體系。 | 深度定製、特定業務需求。 |
策略一:使用內建策略
最簡單的使用方式,播放器組件將自動註冊預設策略:
// 1. 建立播放器控制器(自動註冊預設策略)
AliPlayerController controller = new AliPlayerController(this);
// 2. 準備播放資料
AliPlayerModel model = new AliPlayerModel.Builder()
.videoSource(videoSource)
.build();
// 3. 配置資料模型
controller.configure(model);
// 4. 綁定到視圖
playerView.attach(controller);預設註冊的策略包括:
FirstFrameStrategy(首幀耗時)。
StutterDetectStrategy(卡頓檢測)。
TrafficProtectionStrategy(流量保護)。
策略二:自訂策略
只註冊需要的策略,其他不啟用:
// 1. 建立播放器控制器
AliPlayerController controller = new AliPlayerController(this);
// 2. 擷取策略管理器並清除預設策略
StrategyManager strategyManager = controller.getStrategyManager();
// 3. 只註冊需要的策略
strategyManager.register(new FirstFrameStrategy());
strategyManager.register(new StutterDetectStrategy());
// 4. 準備播放資料
AliPlayerModel model = new AliPlayerModel.Builder()
.videoSource(videoSource)
.build();
controller.configure(model);
playerView.attach(controller);策略三:完全自訂策略
實現自訂策略,建立完全個人化的監控體系:
// 1. 建立播放器控制器
AliPlayerController controller = new AliPlayerController(this);
// 2. 擷取策略管理器
StrategyManager strategyManager = controller.getStrategyManager();
// 3. 註冊自訂策略
strategyManager.register(new MyCustomStrategy());
strategyManager.register(new MyAnalyticsStrategy());
// 4. 準備播放資料
AliPlayerModel model = new AliPlayerModel.Builder()
.videoSource(videoSource)
.build();
controller.configure(model);
playerView.attach(controller);進階使用
如何?自訂策略?
AliPlayerKit提供兩種實現方式,根據需求選擇:
方式 | 特點 | 適用情境 |
繼承 | 代碼量少,架構管理事件訂閱。 | 大多數監控策略。 |
實現 | 靈活性高,完全自主控制。 | 需要特殊控制邏輯。 |
方式一:繼承 BaseStrategy(推薦)
繼承 BaseStrategy 是最簡單的方式,架構已封裝好事件訂閱和生命週期管理。
適用情境:大多數監控和分析策略,如統計、檢測、日誌等。
建立策略類。
繼承
BaseStrategy並實現必要方法:public class MyAnalyticsStrategy extends BaseStrategy { private static final String TAG = "MyAnalyticsStrategy"; @NonNull @Override public String getName() { return TAG; // 返回策略名稱稱,用於日誌和調試 } @Nullable @Override protected List<Class<? extends PlayerEvent>> observedEvents() { // 聲明需要訂閱的事件類型 return Arrays.asList( PlayerEvents.StateChanged.class, PlayerEvents.Prepared.class, PlayerEvents.Info.class ); } @Override public void onEvent(@NonNull PlayerEvent event) { // 過濾非當前播放器事件 if (!isCurrentPlayer(event)) return; // 處理事件 if (event instanceof PlayerEvents.StateChanged) { handleStateChanged((PlayerEvents.StateChanged) event); } else if (event instanceof PlayerEvents.Prepared) { handlePrepared((PlayerEvents.Prepared) event); } else if (event instanceof PlayerEvents.Info) { handleInfo((PlayerEvents.Info) event); } } @Override public void onReset() { super.onReset(); // 重設內部狀態,準備處理新的視頻 } private void handleStateChanged(PlayerEvents.StateChanged event) { // 處理狀態變化 } private void handlePrepared(PlayerEvents.Prepared event) { // 處理準備完成 } private void handleInfo(PlayerEvents.Info event) { // 處理播放資訊更新 } }註冊使用。
StrategyManager strategyManager = controller.getStrategyManager(); strategyManager.register(new MyAnalyticsStrategy());
樣本參考:strategies/FirstFrameStrategy.java。
方式二:實現IStrategy介面
直接實現 IStrategy 介面可以獲得更高的靈活性,但需要自行處理事件訂閱。
適用情境:需要完全控制策略行為,或需要特殊初始化邏輯。
建立策略類。
實現
IStrategy介面,並自行管理事件訂閱:public class MyCustomStrategy implements IStrategy, PlayerEventBus.EventListener<PlayerEvent> { private static final String TAG = "MyCustomStrategy"; private StrategyContext mContext; @NonNull @Override public String getName() { return TAG; } @Override public void onStart(@NonNull StrategyContext context) { mContext = context; // 手動訂閱事件 PlayerEventBus.getInstance().subscribe(PlayerEvents.StateChanged.class, this); } @Override public void onStop() { // 手動取消訂閱 PlayerEventBus.getInstance().unsubscribe(PlayerEvents.StateChanged.class, this); mContext = null; } @Override public void onReset() { // 重設內部狀態 } @Override public void onEvent(@NonNull PlayerEvent event) { // 處理事件 } }註冊使用。
StrategyManager strategyManager = controller.getStrategyManager(); strategyManager.register(new MyAnalyticsStrategy());
如何?帶回調的策略?
策略可以通過回調介面將結果通知給業務層:
public class MyAnalyticsStrategy extends BaseStrategy {
public interface Callback {
void onAnalysisComplete(AnalysisResult result);
}
@Nullable
private final Callback mCallback;
// 支援無參構造
public MyAnalyticsStrategy() {
this(null);
}
// 支援傳入回調
public MyAnalyticsStrategy(@Nullable Callback callback) {
mCallback = callback;
}
@Override
public void onEvent(@NonNull PlayerEvent event) {
// ... 商務邏輯
// 通過回調通知結果
if (mCallback != null) {
mCallback.onAnalysisComplete(result);
}
}
}使用方式:
strategyManager.register(new MyAnalyticsStrategy(result -> {
// 處理分析結果
Log.d("Analytics", "Result: " + result);
}));樣本參考:strategies/FirstFrameStrategy.java。
如何訪問播放器狀態?
策略可以通過 StrategyContext 訪問播放器狀態和資料:
@Override
public void onEvent(@NonNull PlayerEvent event) {
if (!isCurrentPlayer(event)) return;
// 擷取播放器 ID
String playerId = getPlayerId();
// 擷取播放資料模型
AliPlayerModel model = mContext.getModel();
if (model != null) {
String title = model.getVideoTitle();
VideoSource source = model.getVideoSource();
}
// 擷取播放器狀態儲存
IPlayerStateStore stateStore = mContext.getPlayerStateStore();
long position = stateStore.getCurrentPosition();
long duration = stateStore.getDuration();
PlayerState state = stateStore.getPlayState();
}mContext在建構函式中為null,需在onStart()之後才被初始化。
如何註冊全域原則?
通過 StrategyRegistry 可以註冊全域原則,自動應用於所有新建立的播放器執行個體:
// 註冊全域原則
StrategyRegistry.addGlobalStrategy(() -> new MyAnalyticsStrategy());
// 之後建立的所有 AliPlayerController 都會自動註冊此策略
AliPlayerController controller = new AliPlayerController(this); 恢複預設配置:
StrategyRegistry.resetToDefault();最佳實務
生命週期管理
public class MyStrategy extends BaseStrategy {
@Overridepublic void onStart(@NonNull StrategyContext context) {
super.onStart(context); // 必須調用// 初始化資源
}
@Overridepublic void onStop() {
// 清理資源super.onStop(); // 必須調用
}
@Overridepublic void onReset() {
super.onReset();
// 重設內部狀態,準備處理新視頻
}
}事件過濾
在多播放器情境下,務必過濾事件來源:
@Overridepublic void onEvent(@NonNull PlayerEvent event) {
// 過濾非當前播放器事件,避免串台if (!isCurrentPlayer(event)) return;
// 處理當前播放器的事件
}資源清理
策略中如果有 Handler、Runnable 等非同步資源,務必在 onStop() 中清理:
public class MyStrategy extends BaseStrategy {
private final Handler mHandler = new Handler();
private Runnable mRunnable;
@Overridepublic void onStart(@NonNull StrategyContext context) {
super.onStart(context);
mRunnable = () -> doSomething();
mHandler.postDelayed(mRunnable, 1000);
}
@Overridepublic void onStop() {
// 清理非同步任務,避免記憶體流失
mHandler.removeCallbacks(mRunnable);
super.onStop();
}
}注意事項
情境 | 推薦做法 | 原因 |
多播放器情境 | 使用 | 避免事件串台。 |
長時間運行任務 | 在 | 避免記憶體流失。 |
視頻切換 | 在 | 確保狀態正確。 |
訪問 Context | 通過 | 確保上下文可用。 |
樣本參考
專案提供了完整的樣本,位於 playerkit-examples/example-strategy-system。
樣本功能
功能 | 說明 |
記憶播放策略 | 自動記錄播放進度,下次播放時恢複。 |
策略註冊示範 | 示範如何註冊自訂策略。 |
運行樣本
在Demo App中選擇 Strategy System 樣本查看效果。
API 參考
核心介面
介面/類 | 說明 |
| 策略介面,定義生命週期方法。 |
| 策略基類,封裝事件訂閱管理。 |
| 策略管理器,管理原則生命週期。 |
| 策略上下文,提供唯讀播放器資訊。 |
| 策略註冊中心,管理全域原則。 |
BaseStrategy 方法
方法 | 說明 |
| 返回策略名稱稱,用於日誌和調試。 |
| 返回需要訂閱的事件類型列表。 |
| 事件回調,處理訂閱的事件。 |
| 檢查事件是否來自當前播放器。 |
| 擷取當前播放器 ID。 |
StrategyManager 方法
方法 | 說明 |
| 註冊策略。 |
| 登出策略。 |
| 根據名稱擷取策略。 |
| 啟動所有策略。 |
| 停止所有策略。 |
| 重設所有策略。 |
| 銷毀管理器。 |
StrategyContext 方法
方法 | 說明 |
| 擷取播放器 ID。 |
| 擷取播放資料模型。 |
| 擷取播放器狀態儲存(唯讀)。 |
技術原理
整體架構
策略系統採用 事件驅動 + 管理器調度 的架構模式。
設計要點:
策略不持有播放器引用:通過事件驅動,徹底解耦。
策略只關注事件:只需聲明訂閱什麼和如何處理。
管理器統一調度:生命週期、事件分發由管理器控制。
生命週期
策略採用三階段生命週期:
生命週期 | 觸發時機 | 說明 |
|
| 策略啟動,訂閱事件,初始化資源。 |
| 播放內容切換 | 重設內部狀態,準備處理新視頻。 |
|
| 策略停止,取消訂閱,清理資源。 |
事件驅動機制
策略通過事件匯流排訂閱播放器事件,採用發布-訂閱模式。
事件訂閱流程:
策略在
observedEvents()中聲明需要訂閱的事件類型BaseStrategy在onStart()中自動訂閱這些事件BaseStrategy在onStop()中自動取消訂閱策略在
onEvent()中處理接收到的事件
設計理念:事件驅動機制徹底解耦策略與播放器—策略無需持有播放器引用,只需關注事件訂閱與處理邏輯,實現關注點分離。
多播放器隔離
在多播放器情境下,策略系統通過以下機制確保隔離:
獨立 StrategyManager:每個
AliPlayerController擁有獨立的StrategyManager。獨立 StrategyContext:每個策略上下文綁定特定播放器 ID。
事件過濾:策略通過
isCurrentPlayer(event)過濾事件來源。
@Overridepublic void onEvent(@NonNull PlayerEvent event) {
// 每個播放器執行個體的事件都帶有 playerId// isCurrentPlayer() 檢查事件是否來自當前播放器if (!isCurrentPlayer(event)) return;
// 只處理當前播放器的事件
}常見問題
策略什麼時候啟動?
調用 playerView.attach() 時自動啟動所有登入的策略。
如何擷取播放器目前狀態?
通過 StrategyContext 的 getPlayerStateStore() 方法:
IPlayerStateStore stateStore = mContext.getPlayerStateStore();
PlayerState state = stateStore.getPlayState();
long position = stateStore.getCurrentPosition();如何調試策略?
使用 LogHub 查看日誌,TAG 格式:策略名稱稱.BaseStrategy 或 StrategyManager。
高頻崩潰錯例
以下是客戶回函中最常導致崩潰的問題,請務必避免:
錯例 1:在建構函式中訪問 mContext
錯誤碼:
public class MyStrategy extends BaseStrategy {
public MyStrategy() {
super();
// 建構函式中 mContext 為 null
String playerId = getPlayerId(); // 返回Null 字元串
AliPlayerModel model = mContext.getModel(); // NullPointerException!
}
}
正確代碼:
@Overridepublic void onStart(@NonNull StrategyContext context) {
super.onStart(context); // ✅ 必須首先調用// 在 onStart 之後才能訪問 mContext
String playerId = getPlayerId(); // 正常擷取
AliPlayerModel model = mContext.getModel(); // 正常擷取
}
崩潰原因:mContext 在 onStart() 調用後才被賦值,建構函式執行時還未啟動策略。
錯例 2:忘記調用 super.onStart() 導致事件訂閱失效
錯誤碼:
public class MyStrategy extends BaseStrategy {
@Overridepublic void onStart(@NonNull StrategyContext context) {
// 忘記調用 super.onStart(context)// mContext 為 null,事件訂閱不會執行
initMyResources();
}
@Overridepublic void onEvent(@NonNull PlayerEvent event) {
// 永遠不會被調用!因為事件訂閱未執行
}
}
正確代碼:
@Overridepublic void onStart(@NonNull StrategyContext context) {
super.onStart(context); // ✅ 必須首先調用
initMyResources();
}
崩潰原因:super.onStart(context) 會初始化 mContext 並訂閱 observedEvents() 返回的事件。不調用會導致 mContext 為 null、事件訂閱無效。
錯例 3:未過濾事件導致多播放器串台
錯誤碼:
public class MyStrategy extends BaseStrategy {
@Overridepublic void onEvent(@NonNull PlayerEvent event) {
// 未過濾事件來源,多播放器情境會串台if (event instanceof PlayerEvents.StateChanged) {
// 所有播放器的狀態變化都會觸發此邏輯
updateUI(); // 可能操作錯誤的播放器 UI
}
}
}
正確代碼:
@Overridepublic void onEvent(@NonNull PlayerEvent event) {
// 首先過濾非當前播放器事件if (!isCurrentPlayer(event)) return;
if (event instanceof PlayerEvents.StateChanged) {
updateUI(); // 只處理當前播放器的事件
}
}
崩潰原因:在多播放器情境下,所有播放器的事件都會分發到所有策略。不進行過濾會導致策略處理其他播放器的事件。
錯例 4:onStop 未清理非同步任務導致記憶體流失
錯誤碼:
public class MyStrategy extends BaseStrategy {
private Handler handler = new Handler();
@Overridepublic void onStart(@NonNull StrategyContext context) {
super.onStart(context);
handler.postDelayed(() -> updateProgress(), 1000); // 延遲任務
}
@Overridepublic void onStop() {
// 忘記移除延遲任務super.onStop();
}
}
正確代碼:
@Overridepublic void onStop() {
handler.removeCallbacksAndMessages(null); // 清理所有延遲任務super.onStop();
}
崩潰原因:延遲任務持有策略執行個體引用,策略停止後無法被 GC,導致記憶體流失。任務執行時可能訪問已銷毀的資源。
錯例 5:observedEvents 返回 null 導致事件訂閱失敗
錯誤碼:
public class MyStrategy extends BaseStrategy {
@Nullable@Overrideprotected List<Class<? extends PlayerEvent>> observedEvents() {
// 返回 null,雖然不會崩潰,但事件訂閱不會執行return null;
}
@Overridepublic void onEvent(@NonNull PlayerEvent event) {
// 永遠不會被調用
}
}
正確代碼:
@Nullable@Overrideprotected List<Class<? extends PlayerEvent>> observedEvents() {
return Arrays.asList(
PlayerEvents.StateChanged.class, // 聲明要訂閱的事件
PlayerEvents.Prepared.class
);
}
@Overridepublic void onEvent(@NonNull PlayerEvent event) {
// 現在可以正常收到事件了
}
崩潰原因:BaseStrategy 在訂閱事件時會檢查 observedEvents() 傳回值,null 或空列表都不會訂閱任何事件。
錯例 6:onReset 未重設內部狀態導致資料錯誤
錯誤碼:
public class MyStrategy extends BaseStrategy {
private int mPlayCount = 0;
@Overridepublic void onEvent(@NonNull PlayerEvent event) {
if (!isCurrentPlayer(event)) return;
if (event instanceof PlayerEvents.Prepared) {
mPlayCount++; // 視頻切換時未重設,會累積
Log.d(TAG, "Play count: " + mPlayCount);
}
}
@Overridepublic void onReset() {
super.onReset();
// 忘記重設 mPlayCount
}
}
正確代碼:
@Overridepublic void onReset() {
super.onReset();
mPlayCount = 0; // 重設內部狀態
}崩潰原因:onReset() 在視頻切換時調用,用於重設策略內部狀態。不重設會導致狀態累積,影響統計準確性。