使用AI修复Android视频播放ANR及缓存问题记录

Cocos Creator Android 视频播放 ANR 问题排查与优化指南

本文记录了在 Cocos Creator 游戏项目中排查 Android 视频播放 ANR(Application Not Responding)问题的完整流程,以及后续的视频缓存优化方案。希望对遇到类似问题的开发者有所帮助。

问题背景

在 Cocos Creator 开发的游戏中,Android 端播放远程视频时频繁出现 ANR 弹窗,用户体验极差。ANR 日志显示:


Input dispatching timed out (Waiting because no window has focus...)

CocosVideoHelper$VideoHandler - 阻塞 4-12 秒

问题分析

1. 定位问题源头

通过分析 ANR trace 日志,发现问题出在 CocosVideoHelper$VideoHandler 处理消息时阻塞主线程。

进一步查看 Cocos 引擎源码 CocosVideoView.java,找到了 根本原因


// CocosVideoView.java - openVideo() 方法

private void openVideo() {

    // ...

    mMediaPlayer = new MediaPlayer();

    loadDataSource();

   

    // 问题在这里!使用同步的 prepare() 方法

    mMediaPlayer.prepare();  // ← 同步阻塞,等待网络加载完成

    this.showFirstFrame();

}

MediaPlayer.prepare()同步方法,会在当前线程阻塞等待视频加载完成。对于远程视频,这意味着网络请求会阻塞主线程数秒甚至十几秒,直接触发 ANR。

2. 问题影响范围

检查发现 stop() 方法中也存在同样的问题:


public void stop() {

    // ...

    mMediaPlayer.reset();

    loadDataSource();

    mMediaPlayer.prepare();  // ← 同样是同步阻塞

    this.showFirstFrame();

}

解决方案

第一步:将同步加载改为异步加载

修改 CocosVideoView.javaopenVideo() 方法:


private void openVideo() {

    // ... 省略其他代码

   

    try {

        mMediaPlayer = new MediaPlayer();

        mMediaPlayer.setOnPreparedListener(mPreparedListener);

        mMediaPlayer.setOnCompletionListener(mCompletionListener);

        mMediaPlayer.setOnErrorListener(mErrorListener);

        mMediaPlayer.setDisplay(mSurfaceHolder);

        mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);

        mMediaPlayer.setScreenOnWhilePlaying(true);

        loadDataSource();

        mCurrentState = State.INITIALIZED;

        // ====== 修复 ANR:使用异步 prepareAsync() 替代同步 prepare() ======

        mCurrentState = State.PREPARING;

        mMediaPlayer.prepareAsync();  // ← 异步方法,立即返回

        // showFirstFrame() 移到 OnPreparedListener 中调用

       

    } catch (IOException ex) {

        // 错误处理...

    }

}

修改 OnPreparedListener,添加 showFirstFrame() 调用:


MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() {

    public void onPrepared(MediaPlayer mp) {

        mVideoWidth = mp.getVideoWidth();

        mVideoHeight = mp.getVideoHeight();

        if (mVideoWidth != 0 && mVideoHeight != 0) {

            fixSize();

        }

        if(!mMetaUpdated) {

            CocosVideoView.this.sendEvent(EVENT_META_LOADED);

            CocosVideoView.this.sendEvent(EVENT_READY_TO_PLAY);

            mMetaUpdated = true;

        }

        mCurrentState = State.PREPARED;

       

        // ====== 异步加载完成后显示第一帧 ======

        CocosVideoView.this.showFirstFrame();

        // 恢复之前的播放状态

        if (mStateBeforeRelease == State.STARTED) {

            CocosVideoView.this.start();

        }

        // ...

    }

};

同样修改 stop() 方法:


public void stop() {

    if (/* 状态检查 */) {

        mCurrentState = State.STOPPED;

        mMediaPlayer.stop();

        this.sendEvent(EVENT_STOPPED);

        try {

            mMediaPlayer.reset();

            loadDataSource();

            // ====== 修复 ANR:使用异步 prepareAsync() ======

            mCurrentState = State.PREPARING;

            mMediaPlayer.prepareAsync();

            // showFirstFrame() 会在 OnPreparedListener 中调用

        } catch (Exception ex) {}

    }

}

第二步:TypeScript 层配合优化

AdapterAndroid.ts 中优化视频播放逻辑:


createVideo(data: VideoData) {

    // 先停止之前的视频

    try {

        if (videoPlayer.isPlaying) {

            videoPlayer.stop();

        }

    } catch (e) {

        console.warn("停止视频时出错:", e);

    }

   

    // 取消所有事件监听

    this.video.node.off(VideoPlayer.EventType.READY_TO_PLAY);

    // ... 其他事件

   

    // 设置基本属性

    videoPlayer.node.active = true;

    videoPlayer.resourceType = VideoPlayer.ResourceType.REMOTE;

    videoPlayer.loop = data.loop;

    videoPlayer.playOnAwake = false;  // 关键:禁用自动播放

   

    // 监听 READY_TO_PLAY 事件

    this.video.node.on(VideoPlayer.EventType.READY_TO_PLAY, () => {

        console.log("视频加载完成");

        if (data.autoplay) {

            videoPlayer.play();  // 加载完成后再播放

        }

    }, this);

   

    // 延迟设置 URL,让 UI 先渲染

    setTimeout(() => {

        videoPlayer.remoteURL = data.url;

    }, 300);

}

修复前后对比


修复前 (ANR):

┌─────────────────────────────────────────────────────────┐

│ 主线程: setVideoURL → prepare() → [阻塞等待网络] → 完成  │

│                        ↑                                  │

│                   12秒阻塞,触发 ANR                      │

└─────────────────────────────────────────────────────────┘

修复后 (无阻塞):

┌─────────────────────────────────────────────────────────┐

│ 主线程: setVideoURL → prepareAsync() → 立即返回          │

│                                                          │

│ 后台线程:              [异步加载网络视频]                 │

│                                    ↓                     │

│ 主线程:                        onPrepared() → 播放       │

└─────────────────────────────────────────────────────────┘

进阶优化:视频本地缓存

ANR 问题解决后,视频加载速度仍然较慢。接下来实现本地缓存优化。

创建视频缓存工具类

VideoCacheHelper.java


package com.cocos.lib;

public class VideoCacheHelper {

    private static String sCacheDir = null;

   

    public static void init(Activity activity) {

        sCacheDir = activity.getCacheDir().getAbsolutePath() + "/video_cache/";

        new File(sCacheDir).mkdirs();

    }

   

    // 检查视频是否已缓存

    public static String getCachedVideoPath(String url) {

        String fileName = urlToFileName(url);

        File cachedFile = new File(sCacheDir + fileName);

        if (cachedFile.exists() && cachedFile.length() > 0) {

            return cachedFile.getAbsolutePath();

        }

        return null;

    }

   

    // 异步下载视频到缓存

    public static void downloadVideo(final String url) {

        new AsyncTask<Void, Integer, String>() {

            @Override

            protected String doInBackground(Void... params) {

                // 下载逻辑...

                return localPath;

            }

           

            @Override

            protected void onPostExecute(String result) {

                // 通知 JS 层下载完成

                notifyDownloadComplete(url, result);

            }

        }.execute();

    }

   

    // URL 转文件名(MD5 哈希)

    private static String urlToFileName(String url) {

        MessageDigest md = MessageDigest.getInstance("MD5");

        byte[] digest = md.digest(url.getBytes("UTF-8"));

        // 返回 MD5 + 扩展名

        return md5String + ".mp4";

    }

}

在 CocosActivity 中初始化


// CocosActivity.java

protected void initView() {

    // ... 其他初始化

   

    // 初始化视频缓存工具

    VideoCacheHelper.init(this);

}

TypeScript 层缓存逻辑


// AdapterAndroid.ts

createVideo(data: VideoData) {

    // ... 前置处理

   

    setTimeout(() => {

        // 检查视频是否已缓存

        const cachedPath = this.checkVideoCache(data.url);

       

        if (cachedPath) {

            // 有缓存,使用 file:// 协议播放本地文件

            const localUrl = "file://" + cachedPath;

            console.log("使用本地缓存播放:", localUrl);

            videoPlayer.resourceType = VideoPlayer.ResourceType.REMOTE;

            videoPlayer.remoteURL = localUrl;

        } else {

            // 无缓存,使用远程 URL 播放

            console.log("无缓存,使用远程 URL:", data.url);

            videoPlayer.remoteURL = data.url;

           

            // 后台下载到缓存(下次播放时使用)

            this.downloadVideoToCache(data.url);

        }

    }, 300);

}

private checkVideoCache(url: string): string | null {

    try {

        return native.reflection.callStaticMethod(

            "com/cocos/lib/VideoCacheHelper",

            "getCachedVideoPath",

            "(Ljava/lang/String;)Ljava/lang/String;",

            url

        );

    } catch (e) {

        return null;

    }

}

缓存流程


首次播放:

  createVideo(url)

    → checkVideoCache(url) → null

    → 使用远程 URL 播放

    → 后台调用 downloadVideoToCache(url)

    → 视频下载到本地缓存目录

后续播放:

  createVideo(url)

    → checkVideoCache(url) → "/data/.../video_cache/xxx.mp4"

    → 使用 file:// 协议播放本地文件

    → 秒开!

修改文件清单

| 文件 | 修改内容 |

|------|---------|

| lib/CocosVideoView.java | prepare()prepareAsync(),在 OnPreparedListener 中调用 showFirstFrame() |

| lib/CocosActivity.java | 添加 VideoCacheHelper.init(this) |

| lib/VideoCacheHelper.java | 新增文件,视频缓存工具类 |

| AdapterAndroid.ts | 添加缓存检查逻辑,使用 file:// 协议播放本地缓存 |

注意事项

  1. file:// 协议:播放本地缓存时必须添加 file:// 前缀,否则无法正确识别为本地文件

  2. 资源类型:使用 file:// 协议时应保持 REMOTE 类型,因为 Cocos 引擎内部会正确处理

  3. 缓存清理:记得实现缓存大小限制和清理逻辑,避免缓存无限增长

  4. 异常处理:在 JNI 调用和文件操作时要做好异常捕获

总结

通过将 MediaPlayer.prepare() 改为 prepareAsync(),成功解决了 Android 视频播放的 ANR 问题。配合视频本地缓存优化,大幅提升了用户体验。

希望这篇文章能帮助到遇到类似问题的开发者!


关键词: Cocos Creator, Android, VideoPlayer, ANR, MediaPlayer, prepareAsync, 视频缓存

3赞

cocos自身有下载管线可以缓存在gamecaches 没必要自己写,而且是跨平台,提供lru清除策略。