This document describes the design background, strategy types, and usage of the AliPlayerKit player lifecycle strategy.
Background
The player lifecycle strategy is a strategy pattern abstracted from the multi-instance player pool originally implemented for Alibaba Cloud's short drama applications.
This design enables fast video swiping by using a globally shared player instance pool to provide the following:
Instance reuse: Reduces the overhead of creating new instances.
Dynamic configuration: Flexibly controls the number of instances.
Thread optimization: Provides fine-grained control over thread resources.
This approach delivers fast first-frame loading, smooth switching, and a controllable memory footprint, making it adaptable to a wide range of business scenarios.
Key concepts
What is a player lifecycle strategy?
A player lifecycle strategy is an architecture for managing the lifecycle of player instances. It defines core operations such as acquire, recycle, and clear, decoupling player resource management from business logic.
Different business scenarios have different requirements for managing player instances:
Use case | Requirements | Recommended strategy |
Standard video playback | Simple and direct, no reuse needed. | default strategy |
Short video list | Frequent switching, requires fast startup. | reuse pool strategy |
Video preloading | Instance is bound to an ID and supports preloading. | ID-scoped strategy |
Memory-sensitive scenarios | A single global instance for a minimal memory footprint. | singleton strategy |
Advantages of the strategy pattern
The strategy pattern offers the following core advantages for managing the player lifecycle:
Decoupling: Separates business logic from the details of player instance creation and destruction.
Flexibility: Allows you to switch strategies at runtime without modifying business logic.
Extensibility: You can implement custom strategies to meet specific business needs.
Observability: Unified lifecycle events make monitoring and debugging easier.
Features
Problems it solves
Decentralized management: No unified mechanism to control player instances.
Performance overhead: Frequent creation and destruction of player instances waste resources.
Reuse challenges: Player instances cannot be reused across different scenarios.
Uncontrolled memory: Lack of effective control over memory footprint.
Core value
Usage | Description | Advantage |
default strategy | Creates a new instance for each use and destroys it afterward. | Simple and direct, with no residual state. |
reuse pool strategy | Maintains an object pool to reuse idle instances. | Reduces creation overhead and improves startup speed. |
ID-scoped strategy | Maintains a separate instance for each ID. | Supports preloading and ID-bound reuse. |
singleton strategy | A single global instance. | Minimal memory footprint. |
Architectural advantages:
Strategy decoupling: Player resource management is separated from business logic for clear responsibilities.
Runtime switching: Switch strategies without restarting the application.
Thread safety: All strategy implementations support safe access from multiple threads.
Event-driven: Publishes lifecycle events through an event bus for easier monitoring.
Core capabilities
Capability | Description |
Instance acquisition | acquire() Acquires a player instance. The strategy determines whether to create a new one or reuse an existing one. |
Instance recycling | recycle() Recycles a player instance. The strategy determines whether to destroy it or keep it. |
Resource cleanup | clear() Cleans up all resources, ensuring safe release. |
Preloading | preload() Creates instances in advance to reduce time-to-first-frame. |
Pool capacity control | setMaxPoolSize() Dynamically adjusts the pool capacity. |
Built-in components
Strategy types
AliPlayerKit provides four built-in player lifecycle strategies:
Strategy | Description | Use case | Memory footprint |
DefaultLifecycleStrategy | The default strategy. Creates a new instance for each use and destroys it immediately afterward. | Standard playback scenarios, simple and direct. | Low |
ReusePoolLifecycleStrategy | The reuse pool strategy. Maintains an idle pool and uses a LIFO (Last-In, First-Out) policy for reuse. | Short video lists and information feeds. | Medium |
IdScopedPoolLifecycleStrategy | The ID-scoped strategy. Maintains a separate instance for each ID and uses an LRU (Least Recently Used) policy for eviction. | Video preloading, multi-video switching. | Medium |
SingletonLifecycleStrategy | The singleton strategy. A single global instance. | Memory-sensitive scenarios, single video playback. | Lowest |
Strategy details
DefaultLifecycleStrategy (default strategy)
This is the simplest strategy. Each call to acquire() creates a new instance, and each call to recycle() immediately destroys it.
Key points:
Stateless, providing a fresh instance every time.
No reuse, suitable for simple scenarios.
Lowest memory footprint (resources are released immediately after use).
ReusePoolLifecycleStrategy (reuse pool strategy)
Based on the object pool pattern, this strategy maintains an idle pool and an active list. It uses a LIFO (Last-In, First-Out) policy to reuse instances.
Key points:
LIFO policy for the idle pool, where the most recently used instance is reused first.
Supports preloading to create instances in advance.
Configurable pool capacity. Instances are destroyed if the capacity is exceeded.
Ideal for short video list and information feed scenarios.
Each player instance consumes approximately 35–40 MB of memory. The default pool capacity is 3, which you can adjust based on device performance.
IdScopedPoolLifecycleStrategy (ID-scoped strategy)
Maintains a separate player instance for each uniqueId, always returning the same instance for a given ID. It uses an LRU (Least Recently Used) mechanism to control the number of instances.
Key points:
ID-bound, always reusing the same instance for the same ID.
LRU eviction mechanism automatically cleans up the least recently used instances.
Supports preloading to create unbound instances in advance.
Ideal for video preloading and multi-video switching scenarios.
SingletonLifecycleStrategy (singleton strategy)
Maintains a single global player instance. It always returns the same instance, regardless of the uniqueId.
Key points:
Globally unique, with all callers sharing the same instance.
Lowest memory footprint.
Not suitable for playing multiple videos simultaneously.
Lifecycle events
The strategy publishes lifecycle events that you can listen for on the event bus:
Event | Description | When it fires |
PlayerCreated | A player is created. | When a new instance is created. |
PlayerDestroyed | A player is destroyed. | When an instance is destroyed. |
PlayerReused | A player is reused. | When an instance is retrieved from the pool for reuse. |
PlayerHit | A player instance is hit. | When an existing instance is hit for a given ID. |
PlayerEvicted | A player is evicted. | When an instance is evicted by the LRU policy or because the pool is full. |
Basic usage
Use the default strategy
This is the simplest way to use the player, with no extra configuration needed:
// Create a controller (automatically uses DefaultLifecycleStrategy)
AliPlayerController controller = new AliPlayerController(context);
// Bind the player
AliPlayerModel model = new AliPlayerModel.Builder()
.videoSource(videoSource)
.build();
controller.configure(model);
playerView.attach(controller); Use the reuse pool strategy
This strategy is ideal for short video list and information feed scenarios:
// Get the singleton instance of the reuse pool strategy
ReusePoolLifecycleStrategy strategy = ReusePoolLifecycleStrategy.getInstance();
// Set the pool capacity (optional, defaults to 3)
strategy.setMaxPoolSize(3);
// Inject the strategy when creating the controller
AliPlayerController controller = new AliPlayerController(context, strategy);
// Clean up resources when finished
strategy.clear(); Use the ID-scoped strategy
This strategy is ideal for scenarios that require preloading and switching between multiple videos:
// Get the singleton instance of the ID-scoped strategy
IdScopedPoolLifecycleStrategy strategy = IdScopedPoolLifecycleStrategy.getInstance();
// Set the pool capacity
strategy.setMaxPoolSize(3);
// Preload player instances (optional)
strategy.preload(context, 2);
// Inject the strategy when creating the controller
AliPlayerController controller = new AliPlayerController(context, strategy);
// Clean up resources when finished
strategy.clear(); Use the singleton strategy
This strategy is ideal for memory-sensitive scenarios:
// Get the singleton strategy instance
SingletonLifecycleStrategy strategy = SingletonLifecycleStrategy.getInstance();
// Preload the instance (optional, only creates one instance)
strategy.preload(context, 1);
// Inject the strategy when creating the controller
AliPlayerController controller = new AliPlayerController(context, strategy);Advanced usage
Listening for lifecycle events
Listen for the internal lifecycle events of a strategy through the event bus for debugging and monitoring:
PlayerEventBus eventBus = PlayerEventBus.getInstance();
// Listen for player creation
eventBus.subscribe(PlayerLifecycleEvents.PlayerCreated.class, event -> {
Log.d("Player", "Created: " + event.playerId);
});
// Listen for player reuse
eventBus.subscribe(PlayerLifecycleEvents.PlayerReused.class, event -> {
Log.d("Player", "Reused: " + event.playerId);
});
// Listen for player eviction
eventBus.subscribe(PlayerLifecycleEvents.PlayerEvicted.class, event -> {
Log.d("Player", "Evicted: " + event.playerId);
});
// Listen for player destruction
eventBus.subscribe(PlayerLifecycleEvents.PlayerDestroyed.class, event -> {
Log.d("Player", "Destroyed: " + event.playerId);
});
// Unsubscribe when no longer needed
eventBus.unsubscribe(PlayerLifecycleEvents.PlayerCreated.class, listener);Switching strategies dynamically
You can switch strategies at runtime based on business needs:
private IPlayerLifecycleStrategy mCurrentStrategy;
private void switchToReusePool() {
// 1. Clean up old resources
if (mCurrentStrategy != null) {
mCurrentStrategy.clear();
}
// 2. Switch to the reuse pool strategy
mCurrentStrategy = ReusePoolLifecycleStrategy.getInstance();
mCurrentStrategy.setMaxPoolSize(3);
// 3. Preload
mCurrentStrategy.preload(this, 2);
}
private void switchToSingleton() {
// 1. Clean up old resources
if (mCurrentStrategy != null) {
mCurrentStrategy.clear();
}
// 2. Switch to the singleton strategy
mCurrentStrategy = SingletonLifecycleStrategy.getInstance();
}Preloading player instances
Preloading creates player instances in advance to reduce time-to-first-frame:
// Reuse pool strategy: Preloaded instances are placed in the idle pool
ReusePoolLifecycleStrategy strategy = ReusePoolLifecycleStrategy.getInstance();
strategy.preload(context, 2); // Pre-create 2 instances// ID-scoped strategy: Preloads unbound instances
IdScopedPoolLifecycleStrategy strategy = IdScopedPoolLifecycleStrategy.getInstance();
strategy.preload(context, 2); // Pre-create 2 unbound instances// Singleton strategy: Preloads the global instance
SingletonLifecycleStrategy strategy = SingletonLifecycleStrategy.getInstance();
strategy.preload(context, 1); // Creates the global instance (the count parameter is ignored)Implementing a custom strategy
Implement a custom strategy by extending BaseLifecycleStrategy:
public class MyCustomStrategy extends BaseLifecycleStrategy {
private final Map<String, IMediaPlayer> playerMap = new HashMap<>();
@NonNull@Overridepublic IMediaPlayer acquire(@NonNull Context context, @NonNull String uniqueId) {
// Custom acquisition logic
IMediaPlayer player = playerMap.get(uniqueId);
if (player != null) {
// Hit an existing instance
PlayerEventBus.getInstance().post(
new PlayerLifecycleEvents.PlayerHit(player.getPlayerId()));
return player;
}
// Create a new instance
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) {
// Force destruction
playerMap.remove(uniqueId);
destroyPlayer(player);
} else {
// Only stop the player, keep the instance
player.stop();
}
}
@Overridepublic void clear() {
// Clean up all instances
for (IMediaPlayer player : playerMap.values()) {
destroyPlayer(player);
}
playerMap.clear();
}
}Best practices
Strategy selection
Use case | Recommended strategy | Description |
Standard video playback | default strategy | Simple and direct, no extra overhead. |
Short video list (TikTok-style) | reuse pool strategy | Reuses instances for fast switching. |
Video preloading | ID-scoped strategy | Binds instances to IDs and supports preloading. |
Low-end devices | singleton strategy | Minimal memory footprint. |
Educational videos (multi-video switching) | ID-scoped strategy | Preloads the next video. |
Memory optimization
Each player instance consumes approximately 35–40 MB of memory (based on memory profiling results from Alibaba Cloud's multi-instance player pool for short dramas). We recommend adjusting the pool capacity based on device performance:
Device type | Recommended configuration | Pool capacity |
High-end device | ReusePool or IdScopedPool | 3 |
Mid-range device | ReusePool or IdScopedPool | 2 |
Low-end device | Singleton | 1 (default) |
Memory-sensitive scenarios | Singleton | 1 |
// Dynamically adjust based on device memory
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
boolean isLowMemory = am.isLowRamDevice();
if (isLowMemory) {
// Use the singleton strategy on low-memory devices
strategy = SingletonLifecycleStrategy.getInstance();
} else {
// Use the reuse pool strategy on standard devices
strategy = ReusePoolLifecycleStrategy.getInstance();
strategy.setMaxPoolSize(isHighEndDevice ? 3 : 2);
}Precautions
Item | Description |
Clean up promptly | Call clear() or recycle(force=true) to release resources when a page is destroyed. |
Strategy singletons | The ReusePoolLifecycleStrategy, IdScopedPoolLifecycleStrategy, and SingletonLifecycleStrategy classes are implemented as singletons with globally shared state. |
Thread safety | All strategy implementations are thread-safe and can be used in multi-threaded environments. |
Unsubscribe from events | To avoid a memory leak, always unsubscribe from lifecycle events at the appropriate time. |
Initialization order | Strategies are initialized automatically. You do not need to call init() manually. |
Example
A complete example is provided in the project at playerkit-examples/example-lifecycle-strategy.
Example features
Feature | Description |
Strategy switching | Dynamically switch between the four built-in strategies. |
Status display | Displays the real-time status of the strategy (created/reused/hit/evicted). |
Event listening | Listens to and displays lifecycle events. |
Multi-video playback | Demonstrates the effect of switching between multiple videos with different strategies. |
Run the example
In the demo app, select the Lifecycle Strategy example to see it in action.
API
Core interfaces
Interface/class | Description |
| The player lifecycle strategy interface, which defines core operations. |
| The base class for strategies, which encapsulates common logic. |
| The player factory interface, which is responsible for creating and destroying instances. |
IPlayerLifecycleStrategy methods
Method | Description |
| Initializes the strategy and injects the player factory. |
| Acquires a player instance. |
| Recycles a player instance. |
| Cleans up all resources. |
| Preloads player instances. |
BaseLifecycleStrategy methods
Method | Description |
| Sets the maximum pool capacity (only effective for pool-based strategies). |
| Creates a player instance (called by subclasses). |
| Destroys a player instance (called by subclasses). |
How it works
Thread safety
Strategy | Thread safety |
DefaultLifecycleStrategy | Stateless and thread-safe. |
ReusePoolLifecycleStrategy | synchronized blocks. |
IdScopedPoolLifecycleStrategy | synchronized blocks. |
SingletonLifecycleStrategy | AtomicReference + double-checked locking. |
Pooling strategy comparison
Feature | ReusePool | IdScopedPool |
Reuse mechanism | LIFO (Last-In, First-Out) | ID binding + LRU |
Preloading support | Yes (places instances in the idle pool) | Yes (places instances in an unbound queue) |
Eviction policy | Evicts when the pool is full | Automatic LRU eviction |
Use case | Short video list | Multi-video preloading |
Underlying architectural logic:
Why does the reuse pool strategy use LIFO (Last-In, First-Out)?
In scenarios with frequent up-and-down scrolling in a feed, the player instance that was just added to the idle pool is the "hottest" in terms of its underlying decoder context and hardware resources (its cache has not yet been paged out by the system). Reusing it first significantly reduces the time required for low-level context resets, improving startup speed based on the principle of spatial locality.
Why does the ID-scoped strategy use LRU (Least Recently Used)?
Multi-instance pooling follows the principle of temporal locality: as a user scrolls through a feed, the probability of replaying a much older video decreases. The LRU eviction policy aligns with this decay pattern in user browsing history, allowing for effective control over the application's peak memory footprint while maintaining a strict binding to IDs.
FAQ
Choosing the right strategy
Choose based on your business scenario:
Simple playback: Use the default strategy for zero added complexity.
Short video/information feeds: Use the reuse pool strategy for fast switching.
Video preloading: Use the ID-scoped strategy for ID binding.
Memory-sensitive scenarios: Use the singleton strategy for the smallest footprint.
Setting pool capacity
Adjust the capacity based on device performance and business needs:
Each player instance consumes approximately 35–40 MB of memory.
The default pool capacity is 3, which consumes about 100–120 MB.
For low-end devices, set the capacity to 2 or use the singleton strategy.
Using preloading
Use preloading in scenarios where you need to reduce the time-to-first-frame:
Short video list: Preload 1 to 2 instances.
Video detail page: Preload the next recommended video.
Avoid excessive preloading, as it increases memory usage.
Common pitfalls
The following situations are the most common causes of issues reported by users. Make sure to avoid them:
Example 1: Forgetting to call clear()
Incorrect code:
@Overrideprotected void onDestroy() {
super.onDestroy();
// Only detached the View, forgot to clean up instances held by the strategy
mPlayerView.detach();
}
Correct code:
@Overrideprotected void onDestroy() {
super.onDestroy();
// Detach the View
mPlayerView.detach();
// Clean up strategy resources
if (mStrategy != null) {
mStrategy.clear();
}
}
Cause: The strategy (such as ReusePool) holds references to player instances. Failing to clean them up will cause a memory leak.
Example 2: Failing to unsubscribe from an event listener
Incorrect code:
@Overrideprotected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Subscribed to an event but never unsubscribed
PlayerEventBus.getInstance().subscribe(
PlayerLifecycleEvents.PlayerCreated.class,
event -> updateUI()
);
}
Correct code:
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();
// Unsubscribe if (mListener != null) {
PlayerEventBus.getInstance().unsubscribe(
PlayerLifecycleEvents.PlayerCreated.class, mListener);
}
}
Cause: The event bus holds a reference to the Activity. If the listener is not removed, the Activity cannot be released after it is destroyed.
Example 3: Using a player instance after recycling
Incorrect code:
// Recycle the player
strategy.recycle(player, "video_1", false);
// Continue to use the player after recycling
player.start(); // May cause an exceptionCorrect code:
// Recycle the player
strategy.recycle(player, "video_1", false);
player = null; // Clear the reference to prevent misuse. Re-acquire it when needed.
player = strategy.acquire(context, "video_1");Cause: After a player instance is recycled, it may be stopped or destroyed. Using it afterward will cause an exception.
Debugging
Check logs: Filter Logcat by using
tag:AliPlayerKit.Listen for lifecycle events: Observe creation, reuse, and eviction events on the event bus.
Check pool status: Check the current pool size and the number of active instances through logs.
Analyze memory: Use the Android Profiler to check the number of player instances and their memory footprint.