このドキュメントでは、AliPlayerKit プレーヤーのライフサイクル戦略について、その設計背景、戦略タイプ、および使用方法を説明します。
背景
プレーヤーライフサイクルストラテジーは、元々 Alibaba Cloud のショートドラマアプリケーション向けに実装されたマルチインスタンスプレーヤープールから抽象化されたストラテジーパターンです。
この設計では、グローバルに共有されるプレーヤーインスタンスプールを使用することで高速な動画スワイプが可能になり、以下の利点があります:
インスタンスの再利用:新規インスタンス作成のオーバーヘッドを削減します。
動的設定:インスタンス数を柔軟に制御します。
スレッドの最適化:スレッドリソースに対するきめ細かな制御を提供します。
このアプローチは、高速な初回フレームの読み込み、スムーズな切り替え、および制御可能なメモリフットプリントを実現し、幅広いビジネスシナリオに適応できます。
基本概念
プレーヤーライフサイクル戦略
プレーヤーライフサイクル戦略は、プレーヤーインスタンスのライフサイクルを管理するためのアーキテクチャです。取得、リサイクル、クリアなどのコア操作を定義し、プレーヤーリソース管理をビジネスロジックから分離します。
ビジネスシナリオごとに、プレーヤーインスタンスの管理要件は異なります。
ユースケース | 要件 | 推奨される戦略 |
標準的な動画再生 | シンプルかつ直接的で、再利用は不要です。 | デフォルト戦略 |
ショート動画リスト | 頻繁な切り替えが発生し、高速な起動が必要です。 | 再利用プール戦略 |
動画のプリロード | インスタンスは ID に紐付けられ、プリロードをサポートします。 | ID スコープ戦略 |
メモリに制約のあるシナリオ | メモリフットプリントを最小限に抑えるため、単一のグローバルインスタンスを使用します。 | シングルトン戦略 |
Strategy パターンの利点
Strategy パターンは、プレーヤーライフサイクルの管理において以下のコアな利点を提供します。
デカップリング: ビジネスロジックを、プレーヤーインスタンスの生成と破棄の詳細から分離します。
柔軟性: ビジネスロジックを変更することなく、ランタイムに戦略を切り替えることができます。
拡張性: 特定のビジネスニーズに合わせてカスタム戦略を実装できます。
可観測性: 統一されたライフサイクルイベントにより、モニタリングとデバッグが容易になります。
特徴
解決すべき課題
分散管理:プレーヤーインスタンスを制御するための統一された仕組みがありません。
パフォーマンスオーバーヘッド:プレーヤーインスタンスの生成と破棄を頻繁に行うと、リソースが無駄になります。
再利用の課題:異なるシナリオ間でプレーヤーインスタンスを再利用できません。
メモリ制御の欠如:メモリフットプリントを効果的に制御できません。
コアバリュー
ユースケース | 説明 | 利点 |
デフォルト戦略 | 使用のたびに新しいインスタンスを作成し、使用後に破棄します。 | シンプルかつ直接的で、状態が残りません。 |
再利用プール戦略 | オブジェクトプールを維持し、アイドルインスタンスを再利用します。 | 生成オーバーヘッドを削減し、起動速度を向上させます。 |
ID スコープ戦略 | ID ごとに個別のインスタンスを維持します。 | プリロードと、ID に紐付けられた再利用をサポートします。 |
シングルトン戦略 | グローバルな単一インスタンスです。 | メモリフットプリントを最小化します。 |
アーキテクチャ上の利点:
戦略のデカップリング:プレーヤーリソース管理をビジネスロジックから分離し、責務を明確化します。
ランタイム切り替え:アプリケーションを再起動せずに戦略を切り替え可能です。
スレッドセーフ:すべての戦略実装は、複数スレッドから安全にアクセスできます。
イベント駆動:イベントバスを介してライフサイクルイベントを発行し、モニタリングを容易にします。
コア機能
機能 | 説明 |
インスタンスの取得 |
|
インスタンスのリサイクル |
|
リソースのクリーンアップ |
|
プリロード |
|
プール容量制御 |
|
組み込みコンポーネント
ストラテジーの種類
AliPlayerKit は、4 つの組み込みプレーヤーライフサイクルストラテジーを提供します。
ストラテジー | 説明 | ユースケース | メモリフットプリント |
DefaultLifecycleStrategy | デフォルトのストラテジーです。使用のたびに新しいインスタンスを作成し、直後に破棄します。 | 標準的な再生シナリオ。シンプルで直接的です。 | 低 |
ReusePoolLifecycleStrategy | 再利用プールストラテジーです。アイドルプールを維持し、再利用には LIFO (後入れ先出し) ポリシーを使用します。 | ショート動画リストと情報フィード。 | 中 |
IdScopedPoolLifecycleStrategy | ID スコープのストラテジーです。ID ごとに個別のインスタンスを維持し、エビクションには LRU (Least Recently Used) ポリシーを使用します。 | 動画のプリロード、複数動画の切り替え。 | 中 |
SingletonLifecycleStrategy | シングルトンストラテジーです。単一のグローバルインスタンスです。 | メモリ使用量が重視されるシナリオ、単一動画の再生。 | 最低 |
ストラテジーの詳細
DefaultLifecycleStrategy (デフォルトのストラテジー)
これは最もシンプルなストラテジーです。acquire() を呼び出すたびに新しいインスタンスが作成され、recycle() を呼び出すたびにすぐに破棄されます。
主な特徴:
ステートレスであり、常に新しいインスタンスを提供します。
再利用はなく、シンプルなシナリオに適しています。
メモリフットプリントが低い (リソースは使用後すぐに解放されるため)。
ReusePoolLifecycleStrategy (再利用プールストラテジー)
オブジェクトプールパターンに基づいており、このストラテジーはアイドルプールとアクティブリストを維持します。インスタンスの再利用には LIFO (後入れ先出し) ポリシーを使用します。
主な特徴:
アイドルプールには LIFO ポリシーが適用され、最後に使用されたインスタンスが最初に再利用されます。
インスタンスを事前に作成するためのプリロードをサポートします。
プールの容量は設定可能です。容量を超えた場合、インスタンスは破棄されます。
ショート動画リストや情報フィードのシナリオに最適です。
各プレーヤーインスタンスは約 35~40 MB のメモリを消費します。デフォルトのプールの容量は 3 で、デバイスのパフォーマンスに応じて調整できます。
IdScopedPoolLifecycleStrategy (ID スコープのストラテジー)
uniqueId ごとに個別のプレーヤーインスタンスを維持し、特定の ID に対しては常に同じインスタンスを返します。インスタンスの数を制御するために LRU (Least Recently Used) メカニズムを使用します。
主な特徴:
ID にバインドされており、同じ ID に対しては常に同じインスタンスを再利用します。
LRU エビクションメカニズムにより、最も長い間使用されていないインスタンスが自動的にクリーンアップされます。
バインドされていないインスタンスを事前に作成するためのプリロードをサポートします。
動画のプリロードや複数動画の切り替えシナリオに最適です。
SingletonLifecycleStrategy (シングルトンストラテジー)
単一のグローバルプレーヤーインスタンスを維持します。uniqueId に関係なく、常に同じインスタンスを返します。
主な特徴:
グローバルに一意であり、すべての呼び出し元が同じインスタンスを共有します。
メモリフットプリントが最も低いです。
複数の動画の同時再生には適していません。
ライフサイクルイベント
ストラテジーはライフサイクルイベントを発行し、イベントバスでリッスンできます:
イベント | 説明 | 発行されるタイミング |
PlayerCreated | プレーヤーが作成されます。 | 新しいインスタンスが作成されたとき。 |
PlayerDestroyed | プレーヤーが破棄されます。 | インスタンスが破棄されたとき。 |
PlayerReused | プレーヤーが再利用されます。 | インスタンスが再利用のためにプールから取得されたとき。 |
PlayerHit | プレーヤーインスタンスがヒットします。 | 特定の ID に対して既存のインスタンスがヒットしたとき。 |
PlayerEvicted | プレーヤーがエビクトされます。 | LRU ポリシーによって、またはプールが満杯になったためにインスタンスがエビクトされたとき。 |
基本的な使用方法
デフォルトストラテジーの使用
追加の設定は不要です。プレーヤーを使用する最もシンプルな方法です:
// コントローラーを作成します (DefaultLifecycleStrategy が自動的に使用されます)
AliPlayerController controller = new AliPlayerController(context);
// プレーヤーをアタッチします
AliPlayerModel model = new AliPlayerModel.Builder()
.videoSource(videoSource)
.build();
controller.configure(model);
playerView.attach(controller); 再利用プールストラテジーの使用
このストラテジーは、ショート動画リストや情報フィードのシナリオに最適です:
// 再利用プールストラテジーのシングルトンインスタンスを取得します
ReusePoolLifecycleStrategy strategy = ReusePoolLifecycleStrategy.getInstance();
// プール容量をセットします (任意。デフォルトは 3)
strategy.setMaxPoolSize(3);
// コントローラー作成時にストラテジーをインジェクトします
AliPlayerController controller = new AliPlayerController(context, strategy);
// 終了時にリソースをクリーンアップします
strategy.clear(); ID スコープストラテジーの使用
このストラテジーは、プリロードと複数動画の切り替えが必要なシナリオに最適です:
// ID スコープストラテジーのシングルトンインスタンスを取得します
IdScopedPoolLifecycleStrategy strategy = IdScopedPoolLifecycleStrategy.getInstance();
// プール容量をセットします
strategy.setMaxPoolSize(3);
// プレーヤーインスタンスをプリロードします (任意)
strategy.preload(context, 2);
// コントローラー作成時にストラテジーをインジェクトします
AliPlayerController controller = new AliPlayerController(context, strategy);
// 終了時にリソースをクリーンアップします
strategy.clear(); シングルトンストラテジーの使用
このストラテジーは、メモリセンシティブなシナリオに最適です:
// シングルトンストラテジーインスタンスを取得します
SingletonLifecycleStrategy strategy = SingletonLifecycleStrategy.getInstance();
// インスタンスをプリロードします (任意。インスタンスは 1 つのみ作成されます)
strategy.preload(context, 1);
// コントローラー作成時にストラテジーをインジェクトします
AliPlayerController controller = new AliPlayerController(context, strategy);高度な使用法
ライフサイクル イベントの監視
デバッグとモニタリングのために、イベントバスを介してストラテジー内部のライフサイクル イベントを監視します。
PlayerEventBus eventBus = PlayerEventBus.getInstance();
// プレーヤー作成を監視
eventBus.subscribe(PlayerLifecycleEvents.PlayerCreated.class, event -> {
Log.d("Player", "Created: " + event.playerId);
});
// プレーヤー再利用を監視
eventBus.subscribe(PlayerLifecycleEvents.PlayerReused.class, event -> {
Log.d("Player", "Reused: " + event.playerId);
});
// プレーヤーのエビクションを監視
eventBus.subscribe(PlayerLifecycleEvents.PlayerEvicted.class, event -> {
Log.d("Player", "Evicted: " + event.playerId);
});
// プレーヤーの破棄を監視
eventBus.subscribe(PlayerLifecycleEvents.PlayerDestroyed.class, event -> {
Log.d("Player", "Destroyed: " + event.playerId);
});
// 不要になったらアンサブスクライブ
eventBus.unsubscribe(PlayerLifecycleEvents.PlayerCreated.class, listener);ストラテジーの動的な切り替え
ビジネス要件に応じて、ランタイムにストラテジーを切り替えることができます。
private IPlayerLifecycleStrategy mCurrentStrategy;
private void switchToReusePool() {
// 1. 古いリソースをクリーンアップ
if (mCurrentStrategy != null) {
mCurrentStrategy.clear();
}
// 2. 再利用プールストラテジーに切り替え
mCurrentStrategy = ReusePoolLifecycleStrategy.getInstance();
mCurrentStrategy.setMaxPoolSize(3);
// 3. プリロード
mCurrentStrategy.preload(this, 2);
}
private void switchToSingleton() {
// 1. 古いリソースをクリーンアップ
if (mCurrentStrategy != null) {
mCurrentStrategy.clear();
}
// 2. シングルトンストラテジーに切り替え
mCurrentStrategy = SingletonLifecycleStrategy.getInstance();
}プレーヤーインスタンスのプリロード
プリロードは、最初のフレームが表示されるまでの時間を短縮するために、プレーヤーインスタンスを事前に作成します。
// 再利用プールストラテジー: プリロードされたインスタンスはアイドルプールに配置されます
ReusePoolLifecycleStrategy strategy = ReusePoolLifecycleStrategy.getInstance();
strategy.preload(context, 2); // インスタンスを2つ事前作成// IDスコープストラテジー: バインドされていないインスタンスをプリロード
IdScopedPoolLifecycleStrategy strategy = IdScopedPoolLifecycleStrategy.getInstance();
strategy.preload(context, 2); // バインドされていないインスタンスを2つ事前作成// シングルトンストラテジー: グローバルインスタンスをプリロード
SingletonLifecycleStrategy strategy = SingletonLifecycleStrategy.getInstance();
strategy.preload(context, 1); // グローバルインスタンスを作成 (countパラメーターは無視)カスタムストラテジーの実装
BaseLifecycleStrategy を継承してカスタムストラテジーを実装します。
public class MyCustomStrategy extends BaseLifecycleStrategy {
private final Map<String, IMediaPlayer> playerMap = new HashMap<>();
@NonNull@Overridepublic IMediaPlayer acquire(@NonNull Context context, @NonNull String uniqueId) {
// カスタムの取得ロジック
IMediaPlayer player = playerMap.get(uniqueId);
if (player != null) {
// 既存のインスタンスにヒット
PlayerEventBus.getInstance().post(
new PlayerLifecycleEvents.PlayerHit(player.getPlayerId()));
return player;
}
// 新しいインスタンスを作成
player = createPlayer(context);
playerMap.put(uniqueId, player);
return player;
}
@Overridepublic void recycle(@Nullable IMediaPlayer player, @NonNull String uniqueId, boolean force) {
if (player == null) return;
if (force) {
// 強制的に破棄
playerMap.remove(uniqueId);
destroyPlayer(player);
} else {
// プレーヤーは停止するが、インスタンスは保持
player.stop();
}
}
@Overridepublic void clear() {
// すべてのインスタンスをクリーンアップ
for (IMediaPlayer player : playerMap.values()) {
destroyPlayer(player);
}
playerMap.clear();
}
}ベストプラクティス
ストラテジーの選択
ユースケース | 推奨ストラテジー | 説明 |
標準的な動画再生 | デフォルトストラテジー | シンプルかつ直接的で、余分なオーバーヘッドがありません。 |
ショート動画リスト (TikTok 形式) | ReusePool ストラテジー | インスタンスを再利用して高速な切り替えを実現します。 |
動画のプリロード | IdScoped ストラテジー | インスタンスを ID にバインドし、プリロードをサポートします。 |
ローエンドデバイス | Singleton ストラテジー | メモリフットプリントを最小限に抑えます。 |
教育動画 (複数動画の切り替え) | IdScoped ストラテジー | 次の動画をプリロードします。 |
メモリの最適化
各プレーヤーインスタンスは、約 35~40 MB のメモリを消費します (Alibaba Cloud のショートドラマ向けマルチインスタンスプレーヤープールのメモリプロファイリング結果に基づく)。デバイスのパフォーマンスに応じてプール容量を調整することを推奨します:
デバイスタイプ | 推奨設定 | プール容量 |
ハイエンドデバイス | ReusePool ストラテジーまたは IdScoped ストラテジー | 3 |
ミッドレンジデバイス | ReusePool ストラテジーまたは IdScoped ストラテジー | 2 |
ローエンドデバイス | Singleton ストラテジー | 1 (デフォルト) |
メモリ使用量が重視されるシナリオ | Singleton ストラテジー | 1 |
// デバイスのメモリに基づいて動的に調整します
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
boolean isLowMemory = am.isLowRamDevice();
if (isLowMemory) {
// メモリの少ないデバイスでは Singleton ストラテジーを使用します
strategy = SingletonLifecycleStrategy.getInstance();
} else {
// 標準的なデバイスでは ReusePool ストラテジーを使用します
strategy = ReusePoolLifecycleStrategy.getInstance();
strategy.setMaxPoolSize(isHighEndDevice ? 3 : 2);
}注意事項
項目 | 説明 |
迅速なクリーンアップ | ページが破棄されるときに |
ストラテジーのシングルトン |
|
スレッドセーフ | すべてのストラテジー実装はスレッドセーフであり、マルチスレッド環境で使用できます。 |
イベントの登録解除 | メモリリークを避けるため、適切なタイミングで常にライフサイクルイベントの登録を解除してください。 |
初期化順序 | ストラテジーは自動的に初期化されます。手動で |
サンプル
完全なサンプルは、プロジェクト内の playerkit-examples/example-lifecycle-strategy に用意されています。
サンプルの機能
機能 | 説明 |
戦略の切り替え | 4 つの組み込みの戦略を動的に切り替えます。 |
ステータス表示 | 戦略のリアルタイムステータス (作成済み / 再利用済み / ヒット / エビクト済み) を表示します。 |
イベントのリッスン | ライフサイクルイベントをリッスンして表示します。 |
複数動画の再生 | 異なる戦略で複数の動画を切り替えた際の効果を実演します。 |
サンプルの実行
デモアプリで [ライフサイクル戦略] のサンプルを選択すると、実際の動作を確認できます。
API
コアインターフェイス
インターフェイス/クラス | 説明 |
| コア操作を定義する、プレーヤーのライフサイクルストラテジーのインターフェイスです。 |
| 共通ロジックをカプセル化する、ストラテジーの基底クラスです。 |
| インスタンスの作成と破棄を担う、プレーヤーファクトリのインターフェイスです。 |
IPlayerLifecycleStrategy のメソッド
メソッド | 説明 |
| ストラテジーを初期化し、プレーヤーファクトリをインジェクトします。 |
| プレーヤーインスタンスを取得します。 |
| プレーヤーインスタンスをリサイクルします。 |
| すべてのリソースを解放します。 |
| プレーヤーインスタンスをプリロードします。 |
BaseLifecycleStrategy のメソッド
メソッド | 説明 |
| 最大プール容量を設定します (プールベースのストラテジーでのみ有効です)。 |
| プレーヤーインスタンスを作成します (サブクラスから呼び出されます)。 |
| プレーヤーインスタンスを破棄します (サブクラスから呼び出されます)。 |
仕組み
スレッドセーフ
ストラテジー | スレッドセーフ |
DefaultLifecycleStrategy | ステートレスでスレッドセーフです。 |
ReusePoolLifecycleStrategy | synchronized ブロックを使用します。 |
IdScopedPoolLifecycleStrategy | synchronized ブロックを使用します。 |
SingletonLifecycleStrategy | AtomicReference + Double-Checked Locking を使用します。 |
プーリングストラテジーの比較
機能 | ReusePool | IdScopedPool |
再利用メカニズム | LIFO (Last-In, First-Out) | ID バインディング + LRU (Least Recently Used) |
プリロードのサポート | あり (インスタンスをアイドルプールに配置) | あり (インスタンスを未バインドキューに配置) |
エビクションポリシー | プールが満杯になったときにエビクトします | 自動 LRU エビクション |
ユースケース | ショート動画リスト | 複数動画のプリロード |
基盤となるアーキテクチャロジック:
再利用プールストラテジーが LIFO (Last-In, First-Out) を使用する理由
フィード内で頻繁に上下にスクロールするシナリオでは、アイドルプールに追加されたばかりのプレーヤーインスタンスは、基盤となるデコーダーコンテキストとハードウェアリソースの観点から最も「ホット」な状態です (そのキャッシュはまだシステムによってページアウトされていません。)。これを最初に再利用することで、低レベルのコンテキストリセットに必要な時間が大幅に短縮され、空間的局所性の原理に基づいて起動速度が向上します。
ID スコープストラテジーが LRU (Least Recently Used) を使用する理由
マルチインスタンスプーリングは時間的局所性の原理に従います。ユーザーがフィードをスクロールするにつれて、はるか昔の動画を再生する確率は低下します。LRU エビクションポリシーは、ユーザーの閲覧履歴におけるこの減衰パターンに沿っており、ID への厳密なバインディングを維持しながら、アプリケーションのピークメモリフットプリントを効果的に制御できます。
よくある質問
適切なストラテジーの選択
ビジネスシナリオに基づいて選択してください。
シンプルな再生:複雑さを追加しないため、デフォルトストラテジーを使用します。
ショートビデオ、情報フィード:高速な切り替えのため、再利用プールストラテジーを使用します。
ビデオのプレローディング:ID バインディングのため、IDスコープストラテジーを使用します。
メモリに制約のあるシナリオ:最小限のフットプリントのため、シングルトンストラテジーを使用します。
プール容量の設定
デバイスのパフォーマンスとビジネスニーズに基づいて容量を調整してください。
各プレーヤーインスタンスは、約 35~40 MB のメモリを消費します。
デフォルトのプール容量は 3 で、約 100~120 MB を消費します。
ローエンドデバイスの場合は、容量を 2 に設定するか、シングルトンストラテジーを使用してください。
プレローディングの使用
最初のフレームまでの時間を短縮する必要があるシナリオでプレローディングを使用してください。
ショートビデオリスト:1~2 個のインスタンスをプレロードしてください。
ビデオ詳細ページ:次の推奨ビデオをプレロードしてください。
過度なプレローディングは避けてください。メモリ使用量が増加します。
よくある落とし穴
以下の状況は、ユーザーから報告される問題の最も一般的な原因です。必ず回避してください。
例 1:clear() の呼び出し忘れ
誤ったコード:
@Overrideprotected void onDestroy() {
super.onDestroy();
// View のみをデタッチし、ストラテジーが保持するインスタンスのクリーンアップを忘れた
mPlayerView.detach();
}
正しいコード:
@Overrideprotected void onDestroy() {
super.onDestroy();
// View をデタッチ
mPlayerView.detach();
// ストラテジー リソースをクリーンアップ
if (mStrategy != null) {
mStrategy.clear();
}
}
原因:ストラテジー (ReusePool など) はプレーヤーインスタンスへの参照を保持しています。クリーンアップしないと、メモリリークが発生します。
例 2:イベントリスナーのアンサブスクライブ忘れ
誤ったコード:
@Overrideprotected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// イベントをサブスクライブしたが、アンサブスクライブしなかった
PlayerEventBus.getInstance().subscribe(
PlayerLifecycleEvents.PlayerCreated.class,
event -> updateUI()
);
}
正しいコード:
private PlayerEventBus.EventListener<PlayerLifecycleEvents.PlayerCreated> mListener;
@Overrideprotected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mListener = event -> updateUI();
PlayerEventBus.getInstance().subscribe(
PlayerLifecycleEvents.PlayerCreated.class, mListener);
}
@Overrideprotected void onDestroy() {
super.onDestroy();
// アンサブスクライブ
if (mListener != null) {
PlayerEventBus.getInstance().unsubscribe(
PlayerLifecycleEvents.PlayerCreated.class, mListener);
}
}
原因:イベントバスは Activity への参照を保持します。リスナーを削除しないと、Activity が破棄された後も解放されません。
例 3:リサイクル後のプレーヤーインスタンスの使用
誤ったコード:
// プレーヤーをリサイクル
strategy.recycle(player, "video_1", false);
// リサイクル後もプレーヤーを使用し続ける
player.start(); // 例外が発生する可能性があります正しいコード:
// プレーヤーをリサイクル
strategy.recycle(player, "video_1", false);
player = null; // 誤用を防ぐために参照をクリアします。必要に応じて再取得します。
player = strategy.acquire(context, "video_1");原因:プレーヤーインスタンスがリサイクルされた後、停止または破棄されている可能性があります。リサイクル後に使用すると例外が発生します。
デバッグ
ログを確認する:
tag:AliPlayerKitを使用して Logcat をフィルタリングします。ライフサイクルイベントをリッスンする:イベントバスで作成、再利用、エビクションイベントを監視します。
プールステータスを確認する:ログを通じて現在のプールサイズとアクティブなインスタンス数を確認します。
メモリを分析する:Android Profiler を使用して、プレーヤーインスタンスの数とメモリフットプリントを確認します。