Player lifecycle strategy

更新时间:
复制 MD 格式

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:

  1. Instance reuse: Reduces the overhead of creating new instances.

  2. Dynamic configuration: Flexibly controls the number of instances.

  3. 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

  1. Decentralized management: No unified mechanism to control player instances.

  2. Performance overhead: Frequent creation and destruction of player instances waste resources.

  3. Reuse challenges: Player instances cannot be reused across different scenarios.

  4. 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.

Note

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

IPlayerLifecycleStrategy

The player lifecycle strategy interface, which defines core operations.

BaseLifecycleStrategy

The base class for strategies, which encapsulates common logic.

IPlayerFactory

The player factory interface, which is responsible for creating and destroying instances.

IPlayerLifecycleStrategy methods

Method

Description

init(IPlayerFactory)

Initializes the strategy and injects the player factory.

acquire(Context, String)

Acquires a player instance.

recycle(IMediaPlayer, String, boolean)

Recycles a player instance.

clear()

Cleans up all resources.

preload(Context, int)

Preloads player instances.

BaseLifecycleStrategy methods

Method

Description

setMaxPoolSize(int)

Sets the maximum pool capacity (only effective for pool-based strategies).

createPlayer(Context)

Creates a player instance (called by subclasses).

destroyPlayer(IMediaPlayer)

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 exception

Correct 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

  1. Check logs: Filter Logcat by using tag:AliPlayerKit.

  2. Listen for lifecycle events: Observe creation, reuse, and eviction events on the event bus.

  3. Check pool status: Check the current pool size and the number of active instances through logs.

  4. Analyze memory: Use the Android Profiler to check the number of player instances and their memory footprint.