全部產品
Search
文件中心

ApsaraVideo VOD:事件系統

更新時間:Sep 10, 2026

本文介紹AliPlayerKit事件系統的核心概念、事件分類、使用方法及最佳實務。

概念介紹

什麼是事件?

事件 (Event) 是播放器運行過程中產生的狀態變化或行為通知。每個事件都攜帶播放器 ID(playerId),用於標識事件來源,實現多播放器執行個體的事件隔離。

AliPlayerKit 中的事件分為兩大類:

類型

說明

樣本

狀態事件

播放器狀態變化的通知,由播放器內部產生。

StateChanged、Prepared、Error

命令事件

控制播放器行為的指令,由 UI 或外部組件產生。

Play、Pause、Seek、SetSpeed

所有事件都繼承自 PlayerEvent 基類,保證統一的事件結構。

什麼是事件系統?

事件系統 (Event System) 是用於管理事件訂閱和發布的通訊架構,核心組件是 PlayerEventBus。

它提供以下能力:

  • 型別安全訂閱:按事件類型訂閱,編譯期保證型別安全。

  • 安全執行緒:支援多線程環境下的事件發布和訂閱。

  • 弱引用管理:使用弱引用儲存監聽器,自動清理已回收的訂閱者。

  • 事件隔離:通過 playerId 實現多播放器執行個體的事件隔離。

通過事件系統,播放器的 UI 組件(Slot)與控制器(Controller)完全解耦:UI 只負責發送命令和訂閱狀態變化,不關心命令如何執行;控制器只負責處理命令和發布狀態變化,不關心誰在監聽。

功能特性

解決問題

  1. 組件解耦:消除組件間直接依賴,降低耦合度,提升可替換性與可測試性。

  2. 事件溯源:解決多播放器執行個體情境下事件來源混亂問題。

  3. 職責分離:剝離UI組件與商務邏輯混雜,明確職責邊界。

核心價值

事件系統將組件通訊標準化,開發人員可以自主選擇使用方式:

使用方式

說明

優勢

訂閱事件

監聽播放器狀態變化。

解耦 UI 與商務邏輯,響應式更新介面。

發送命令

控制播放器行為。

UI 無需持有控制器引用,命令式互動。

自訂事件

擴充事件類型。

滿足特定業務情境,保持架構一致性。

架構優勢:

  • 解耦:組件間通過事件通訊,無直接依賴。

  • 可測試:組件可獨立測試,通過事件類比互動。

  • 可擴充:自訂事件類型,不修改架構代碼。

  • 安全執行緒:支援多線程環境,無需額外同步。

核心能力

能力

說明

型別安全訂閱

按事件類型訂閱,編譯期類型檢查。

弱引用管理

自動清理已回收的監聽器,防止記憶體流失。

事件繼承支援

訂閱父類事件可接收子類事件。

多播放器隔離

通過 playerId 區分不同播放器的事件。

事件分類

播放事件(PlayerEvents)

播放器內部產生的狀態變化事件,用於通知外部組件播放器狀態。

事件

說明

攜帶資料

Prepared

播放器準備完成。

duration 視頻總時間長度

FirstFrameRendered

首幀渲染完成。

-

StateChanged

播放狀態變化。

oldState, newState

VideoSizeChanged

視頻尺寸變化。

width, height

Info

播放進度更新。

currentPosition, duration, bufferedPosition

Error

播放錯誤。

errorCode, errorMsg

LoadingBegin

開始緩衝。

-

LoadingProgress

緩衝進度。

percent, netSpeed

LoadingEnd

緩衝結束。

-

SetSpeedCompleted

倍速設定完成。

speed

SnapshotCompleted

截圖完成。

result, snapshotPath, width, height

SetLoopCompleted

迴圈設定完成。

loop

SetMuteCompleted

靜音設定完成。

mute

SetScaleTypeCompleted

填充模式設定完成。

scaleType

SetMirrorTypeCompleted

鏡像設定完成。

mirrorType

SetRotationCompleted

旋轉設定完成。

rotation

TrackQualityListUpdated

清晰度列表更新。

trackQualityList

TrackSelected

清晰度選擇完成。

trackIndex

TrackSwitchCompleted

清晰度切換完成

quality 目標清晰度

TrackSwitchFailed

清晰度切換失敗

quality 目標清晰度, errorInfo 錯誤資訊

播放命令(PlayerCommand)

用於控制播放器行為的命令事件,由 UI 組件或外部代碼發送。

命令

說明

參數

Play

開始播放。

-

Pause

暫停播放。

-

Toggle

切換播放/暫停。

-

Stop

停止播放。

-

Replay

重播。

-

Seek

跳轉到指定位置。

position 目標位置(毫秒)

SetSpeed

設定播放速度。

speed 播放速度(0.3-3.0)

Snapshot

截圖。

-

SetLoop

設定迴圈播放。

loop 是否迴圈

SetMute

設定靜音。

mute 是否靜音

SetScaleType

設定填充模式。

scaleType 填充模式

SetMirrorType

設定鏡像。

mirrorType 鏡像模式

SetRotation

設定旋轉。

rotationMode 旋轉角度

SelectTrack

切換清晰度。

trackQuality 目標清晰度

手勢事件(GestureEvents)

使用者手勢操作產生的事件,描述手勢行為本身,不包含商務邏輯。

事件

說明

攜帶資料

SingleTapEvent

單擊。

x, y 點擊座標

DoubleTapEvent

雙擊。

x, y 點擊座標

LongPressEvent

長按開始。

x, y 長按座標

LongPressEndEvent

長按結束。

-

HorizontalDragStartEvent

水平拖動開始。

startX, startY

HorizontalDragUpdateEvent

水平拖動更新。

deltaPercent 增量百分比

HorizontalDragEndEvent

水平拖動結束。

-

LeftVerticalDragStartEvent

左側垂直拖動開始。

startX, startY

LeftVerticalDragUpdateEvent

左側垂直拖動更新。

deltaPercent, currentPercent

LeftVerticalDragEndEvent

左側垂直拖動結束。

-

RightVerticalDragStartEvent

右側垂直拖動開始。

startX, startY

RightVerticalDragUpdateEvent

右側垂直拖動更新。

deltaPercent, currentPercent

RightVerticalDragEndEvent

右側垂直拖動結束。

-

控制欄事件(ControlBarEvents)

控制欄顯示狀態同步事件。

事件

說明

Show

顯示控制欄。

Hide

隱藏控制欄。

ResetTimer

重設自動隱藏計時器。

ShowSettings

顯示設定介面。

ShowSpeedPanel

顯示倍速選擇面板(橫屏情境)

ShowQualityPanel

顯示清晰度選擇面板(橫屏情境)

全屏事件(FullscreenEvents)

全螢幕模式切換事件。

事件

說明

攜帶資料

Toggle

切換全屏狀態

-

FullScreenChanged

全屏狀態變更完成

isFullscreen 是否全屏

生命週期事件(PlayerLifecycleEvents)

播放器生命週期策略事件,用於觀察播放器執行個體的建立、複用、銷毀等行為。

事件

說明

PlayerCreated

建立新播放器執行個體。

PlayerDestroyed

銷毀播放器執行個體。

PlayerReused

複用空閑播放器執行個體。

PlayerHit

命中活躍播放器執行個體。

PlayerEvicted

播放器執行個體被淘汰。

插槽事件(SlotEvents)

插槽內部產生的事件。

事件

說明

TopBarBackClicked

頂部欄返回按鈕點擊。

基礎使用

事件系統提供兩種使用原則:

策略

說明

適用情境

訂閱事件

監聽播放器狀態變化。

UI 更新、商務邏輯響應。

發送命令

控制播放器行為。

使用者互動、外部控制。

策略一:訂閱事件

訂閱事件需要三個步驟:擷取事件匯流排、建立監聽器、訂閱事件。

public class MyActivity extends AppCompatActivity {

    private PlayerEventBus mEventBus;
    private PlayerEventBus.EventListener<PlayerEvents.Info> mInfoListener;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 1. 擷取事件匯流排單例
        mEventBus = PlayerEventBus.getInstance();

        // 2. 建立監聽器
        mInfoListener = event -> {
            // 處理播放進度更新
            long position = event.currentPosition;
            long duration = event.duration;
            updateProgressUI(position, duration);
        };

        // 3. 訂閱事件
        mEventBus.subscribe(PlayerEvents.Info.class, mInfoListener);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 4. 取消訂閱,避免記憶體流失
        if (mEventBus != null && mInfoListener != null) {
            mEventBus.unsubscribe(PlayerEvents.Info.class, mInfoListener);
        }
    }
}     

策略二:發送命令

發送命令通過事件匯流排post()方法實現:

// 1. 擷取事件匯流排單例
PlayerEventBus eventBus = PlayerEventBus.getInstance();

// 2. 擷取目標播放器 ID
// 【情境 A:在插槽內部】 直接繼承調用機制
// String playerId = getPlayerId();
// 【情境 B:在 Activity/Fragment 中】 通過你持有的控制器擷取
String playerId = mAliPlayerKitController.getPlayer().getPlayerId();

// 3. 發布命令(發布後,掛載了該 playerId 的 Controller 即會同步執行)
eventBus.post(new PlayerCommand.Play(playerId));

// 其他常用命令樣本:
eventBus.post(new PlayerCommand.Pause(playerId));               // 暫停播放
eventBus.post(new PlayerCommand.Toggle(playerId));              // 切換播放/暫停狀態
eventBus.post(new PlayerCommand.Seek(playerId, 30000));         // 精準跳轉到 30 秒
eventBus.post(new PlayerCommand.SetSpeed(playerId, 1.5f));      // 設定為 1.5 倍速          

進階使用

如何在插槽中使用事件?

在插槽內部使用事件有兩種方式:訂閱事件和發送命令。

方式一:訂閱事件(推薦使用 observedEvents())

BaseSlot 提供了 observedEvents() 和 onEvent() 方法,架構會自動管理訂閱生命週期:

public class MySlot extends BaseSlot {

    // 1. 聲明要訂閱的事件類型
    @Override
    protected List<Class<? extends PlayerEvent>> observedEvents() {
        return Arrays.asList(
            PlayerEvents.StateChanged.class,
            PlayerEvents.Info.class
        );
    }

    // 2. 處理事件回調
    @Override
    protected void onEvent(@NonNull PlayerEvent event) {
        if (event instanceof PlayerEvents.StateChanged) {
            PlayerEvents.StateChanged stateChanged = (PlayerEvents.StateChanged) event;
            updatePlayPauseIcon(stateChanged.newState);
        } else if (event instanceof PlayerEvents.Info) {
            PlayerEvents.Info info = (PlayerEvents.Info) event;
            updateProgress(info.currentPosition, info.duration);
        }
    }
}          

方式二:發送命令

在插槽中通過postEvent()方法發送命令:

public class MySlot extends BaseSlot {

    private void onPlayPauseClick() {
        // 擷取 playerId(在 onAttach 之後可用)
        String playerId = getPlayerId();
        if (playerId != null) {
            // 發送切換播放/暫停命令
            postEvent(new PlayerCommand.Toggle(playerId));
        }
    }

    private void onSeekTo(long position) {
        String playerId = getPlayerId();
        if (playerId != null) {
            // 發送跳轉命令
            postEvent(new PlayerCommand.Seek(playerId, position));
        }
    }
}      

插槽的完整使用方法請參考插槽系統。

如何在策略中使用事件?

在策略中使用事件與插槽類似,通過 observedEvents() 和 onEvent() 方法進行訂閱和處理。但策略有以下特點:

特點

說明

主要用於監控

策略一般只訂閱事件進行分析,不發送命令。

需要過濾事件

多播放器情境下必須通過 isCurrentPlayer(event) 過濾。

訪問上下文

通過 StrategyContext 訪問播放器狀態和資料。

範例程式碼:

public class MyAnalyticsStrategy extends BaseStrategy {

    @Nullable@Overrideprotected List<Class<? extends PlayerEvent>> observedEvents() {
        return Arrays.asList(
            PlayerEvents.StateChanged.class,
            PlayerEvents.Info.class
        );
    }

    @Overridepublic void onEvent(@NonNull PlayerEvent event) {
        // 必須過濾事件來源,避免多播放器串台if (!isCurrentPlayer(event)) return;

        if (event instanceof PlayerEvents.StateChanged) {
            // 處理狀態變化
            PlayerEvents.StateChanged stateChanged = (PlayerEvents.StateChanged) event;
            logState(stateChanged.newState);
        } else if (event instanceof PlayerEvents.Info) {
            // 處理播放進度
            PlayerEvents.Info info = (PlayerEvents.Info) event;
            trackProgress(info.currentPosition, info.duration);
        }
    }
}

策略的完整使用方法請參考策略系統。

如何?自訂事件?

當業務需要自訂事件時,可以繼承 PlayerEvent 建立新的事件類型。

  1. 建立事件類別。

    繼承 PlayerEvent 並添加業務所需的資料欄位:

    /**
     * 自訂播放事件
     */public class MyCustomEvent extends PlayerEvent {
    
        /**
         * 自訂資料欄位
         */public final String customData;
    
        /**
         * 建構函式
         *
         * @param playerId 播放器 ID
         * @param customData 自訂資料
         */public MyCustomEvent(@NonNull String playerId, @NonNull String customData) {
            super(playerId);
            this.customData = customData;
        }
    }
  2. 發布事件。

    // 建立並發布事件
    MyCustomEvent event = new MyCustomEvent(playerId, "custom_data");
    PlayerEventBus.getInstance().post(event);
  3. 訂閱自訂事件。

    // 訂閱自訂事件
    PlayerEventBus.getInstance().subscribe(MyCustomEvent.class, event -> {
        // 處理自訂事件
        String data = event.customData;
    });

如何?多播放器事件隔離?

每個事件都攜帶 playerId,通過判斷 playerId 實現多播放器事件隔離:

// 訂閱事件時過濾 playerId
mEventBus.subscribe(PlayerEvents.Info.class, event -> {
    // 只處理當前播放器的事件if (mPlayerId.equals(event.playerId)) {
        updateProgress(event.currentPosition, event.duration);
    }
});

如何批量訂閱命令?

PlayerCommand 提供了 ALL_COMMANDS 數組,可用於批量訂閱:

// 批量訂閱所有播放命令for (Class<? extends PlayerCommand> commandClass : PlayerCommand.ALL_COMMANDS) {
    mEventBus.subscribe(commandClass, mCommandListener);
}

// 批量取消訂閱for (Class<? extends PlayerCommand> commandClass : PlayerCommand.ALL_COMMANDS) {
    mEventBus.unsubscribe(commandClass, mCommandListener);
}

最佳實務

訂閱生命週期管理

情境

推薦做法

原因

Activity/Fragment

在 onDestroy() 中取消訂閱。

避免Activity泄漏。

Slot 內部

使用 observedEvents() 自動管理。

架構自動處理生命週期。

單例組件

使用弱引用或手動管理。

單例生命週期長,需主動清理。

安全執行緒

事件分發在調用線程同步執行,注意以下情境:

// 在主線程訂閱,事件回調也在主線程
runOnUiThread(() -> {
    mEventBus.subscribe(PlayerEvents.Info.class, event -> {
        // 此回調在發布事件的線程執行// 如果在子線程發布事件,需要切換到主線程更新 UI
        runOnUiThread(() -> updateUI(event));
    });
});

命令發送時機

命令

建議發送時機

說明

Play/Pause/Toggle

任意時刻。

安全命令,無副作用。

Seek

播放器 Prepared 之後。

確保視頻已載入。

SetSpeed

播放中或暫停時。

確保播放器已初始化。

Snapshot

首幀渲染後。

確保有畫面可截。

注意事項

  1. 建構函式風險:禁止在建構函式中訂閱/發送事件(此時對象未初始化,存在null 指標風險;playerId尚未注入)。

  2. 監聽器匹配:取消訂閱時需使用原監聽器執行個體(依賴對象引用匹配機制)。

  3. 耗時操作禁止:事件回調中禁止耗時操作(事件同步分發,阻塞線程)。

高頻事件效能紅線:

  • 情境:PlayerEvents.Info(播放進度)、LoadingProgress及GestureEvents.Update等高頻事件(最高數十次/秒)。

  • 要求:避免記憶體配置(如new String())、重度布局重新整理;建議複用對象,採用節流策略與同值過濾減少無效重繪。

樣本參考

專案提供了完整的樣本,位於 playerkit-examples/example-event-system。

樣本功能

功能

說明

訂閱播放進度

即時接收 PlayerEvents.Info 事件。

發送播放命令

通過 PlayerCommand.Toggle 控制播放/暫停。

事件記錄展示

顯示最近 20 條事件資訊。

運行樣本

在 Demo App 中選擇 Event System 樣本查看效果。

API 參考

核心介面

介面/類

說明

PlayerEvent

事件基類,所有事件繼承此類。

PlayerCommand

命令基類,所有命令繼承此類。

PlayerEventBus

事件匯流排,管理訂閱和發布。

EventListener<T>

事件監聽器介面。

PlayerEventBus 方法

方法

說明

getInstance()

擷取事件匯流排單例。

subscribe(eventType, listener)

訂閱指定類型的事件。

unsubscribe(eventType, listener)

取消訂閱指定類型的事件。

unsubscribe(listener)

取消指定監聽器的所有訂閱。

post(event)

發布事件。

getSubscriberCount(eventType)

擷取指定事件類型的訂閱者數量。

hasSubscribers(eventType)

檢查是否有訂閱者。

unsubscribeAll()

清除所有訂閱(慎用)。

技術原理

弱引用機制

事件匯流排使用 WeakReference 儲存監聽器,有以下優勢:

  • 自動清理:監聽器被 GC 回收後,自動從訂閱列表中移除。

  • 防止泄漏:即使忘記取消訂閱,也不會導致嚴重泄漏。

  • 安全訪問:每次分發前檢查弱引用是否有效。

// 弱引用儲存監聽器
List<WeakReference<EventListener<? extends PlayerEvent>>> listeners;

// 分發前檢查有效性for (WeakReference<EventListener> ref : listeners) {
    EventListener listener = ref.get();
    if (listener != null) {
        listener.onEvent(event);  // 有效則調用
    } else {
        listeners.remove(ref);  // 無效則清理
    }
}

單向資料流與狀態同步閉環

事件系統主要承擔“瞬態通知”的職責。它與 PlayerStateStore 狀態儲存相結合,構成了嚴密的單向資料流(UDF)架構。

資料流向:

方向

機制

說明

命令上行

UI → Controller

Slot 發送命令事件控制播放器行為。

狀態下行

Controller → UI

控制器發布狀態變化事件通知 Slot。

狀態拉取

UI → StateStore

Slot 主動拉取目前狀態(補償機制)。

為何需要狀態拉取?

事件具有瞬態性,晚綁定的插槽可能在attach後錯過歷史事件。通過getPlayerStateStore()可即時同步擷取播放器目前狀態,無需依賴事件訂閱時機。

@Overridepublic void onAttach(@NonNull SlotHost host) {
    super.onAttach(host);

    // 插槽晚綁定,錯過了 Prepared、StateChanged 等事件?// 主動拉取目前狀態進行 UI 初始化
    IPlayerStateStore stateStore = host.getPlayerStateStore();
    PlayerState currentState = stateStore.getPlayState();  // 當前播放狀態long position = stateStore.getCurrentPosition();       // 當前播放位置long duration = stateStore.getDuration();              // 視頻總時間長度// 根據目前狀態初始化 UI
    updatePlayPauseIcon(currentState);
    updateProgress(position, duration);
}

這種架構徹底解耦了 UI 與控制器:UI 無需持有控制器引用,只需訂閱事件、發送命令;控制器無需關心誰在監聽,只需發布事件、處理命令。

事件繼承支援

訂閱父類事件可接收所有子類事件:

// 訂閱 PlayerEvent 可接收所有事件
mEventBus.subscribe(PlayerEvent.class, event -> {
    // 接收所有 PlayerEvent 子類事件
});

// 訂閱 PlayerCommand 可接收所有命令
mEventBus.subscribe(PlayerCommand.class, event -> {
    // 接收所有 PlayerCommand 子類命令
});

常見問題

為什麼收不到事件?

請按以下步驟進行排查:

  1. 訂閱狀態:確認已調用subscribe()。

  2. 事件匹配:訂閱類型需與發布類型一致。

  3. 標識校正:事件playerId必須匹配。

  4. 回收檢測:監聽器是否被GC提前回收。

如何調試事件?

使用 LogHub 查看日誌,TAG 為 PlayerEventBus:

// 查看訂閱日誌// I/PlayerEventBus: Subscribed to Info// 查看發布日誌(需開啟詳細日誌)// I/PlayerEventBus: Posted event: Info to 2 listeners

高頻崩潰錯例

以下是開發人員最常遇到的問題,請務必避免:

錯例 1:忘記取消訂閱導致記憶體流失

錯誤碼:

public class MyActivity extends AppCompatActivity {

    @Overrideprotected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 訂閱後未取消訂閱
        PlayerEventBus.getInstance().subscribe(PlayerEvents.Info.class, event -> {
            updateUI(event.currentPosition);  // Activity 泄漏!
        });
    }

    // 沒有 onDestroy 取消訂閱
}

正確代碼:

public class MyActivity extends AppCompatActivity {

    private PlayerEventBus.EventListener<PlayerEvents.Info> mListener;

    @Overrideprotected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mListener = event -> updateUI(event.currentPosition);
        PlayerEventBus.getInstance().subscribe(PlayerEvents.Info.class, mListener);
    }

    @Overrideprotected void onDestroy() {
        super.onDestroy();
        // ✅ 必須取消訂閱
        PlayerEventBus.getInstance().unsubscribe(PlayerEvents.Info.class, mListener);
    }
}

崩潰原因:Lambda 運算式持有 Activity 引用,未取消訂閱導致 Activity 無法釋放。

錯例 2:在建構函式中訂閱事件

錯誤碼:

public class MyComponent {

    public MyComponent() {
        // 建構函式中訂閱,對象未完全初始化
        PlayerEventBus.getInstance().subscribe(PlayerEvents.Info.class, event -> {
            // 此時 this 可能未完全初始化
        });
    }
}

正確做法:

public class MyComponent {

    private PlayerEventBus.EventListener<PlayerEvents.Info> mListener;

    public void init() {
        // 在初始化方法中訂閱
        mListener = event -> handleEvent(event);
        PlayerEventBus.getInstance().subscribe(PlayerEvents.Info.class, mListener);
    }

    public void destroy() {
        // 取消訂閱
        PlayerEventBus.getInstance().unsubscribe(PlayerEvents.Info.class, mListener);
    }
}

崩潰原因:建構函式執行時對象未完全初始化,可能導致null 指標或狀態不一致。

錯例 3:在插槽建構函式中發送事件

錯誤碼:

public class MySlot extends BaseSlot {

    public MySlot(Context context) {
        super(context);
        // 建構函式中 playerId 未設定
        postEvent(new PlayerCommand.Toggle(getPlayerId()));  // getPlayerId() 返回 null!
    }
}

正確做法:

public class MySlot extends BaseSlot {

    @Overridepublic void onAttach(@NonNull SlotHost host) {
        super.onAttach(host);
        // 在 onAttach 之後發送事件
        String playerId = getPlayerId();
        if (playerId != null) {
            postEvent(new PlayerCommand.Toggle(playerId));
        }
    }
}

崩潰原因:getPlayerId() 在 onAttach() 之後才返回有效值,建構函式中為 null。

錯例 4:事件回調中執行耗時操作

錯誤碼:

mEventBus.subscribe(PlayerEvents.Info.class, event -> {
    // 在回調中執行網路請求,阻塞事件分發
    String data = fetchFromNetwork();  // 阻塞!
    updateUI(data);
});

正確做法:

mEventBus.subscribe(PlayerEvents.Info.class, event -> {
    // 耗時操作非同步執行
    executorService.execute(() -> {
        String data = fetchFromNetwork();
        runOnUiThread(() -> updateUI(data));
    });
});

崩潰原因:事件分發是同步的,耗時操作會阻塞所有後續事件的分發,導致 UI 卡頓。

錯例 5:Slot 中未使用 observedEvents() 訂閱

錯誤碼:

public class MySlot extends BaseSlot {

    @Overridepublic void onAttach(@NonNull SlotHost host) {
        super.onAttach(host);
        // 手動訂閱,需要手動取消
        PlayerEventBus.getInstance().subscribe(PlayerEvents.Info.class, mListener);
    }

    @Overridepublic void onDetach() {
        // 忘記取消訂閱,導致泄漏super.onDetach();
    }
}

正確做法:

public class MySlot extends BaseSlot {

    // 使用 observedEvents() 自動管理@Overrideprotected List<Class<? extends PlayerEvent>> observedEvents() {
        return Arrays.asList(PlayerEvents.Info.class);
    }

    @Overrideprotected void onEvent(@NonNull PlayerEvent event) {
        if (event instanceof PlayerEvents.Info) {
            // 處理事件
        }
    }
}

崩潰原因:手動訂閱需要手動取消,忘記取消會導致監聽器泄漏。

如果確實需要手動訂閱,必須在 onDetach() 中取消:

@Overridepublic void onDetach() {
    PlayerEventBus.getInstance().unsubscribe(PlayerEvents.Info.class, mListener);
    super.onDetach();
}