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.java 的 openVideo() 方法:
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:// 协议播放本地缓存 |
注意事项
-
file:// 协议:播放本地缓存时必须添加
file://前缀,否则无法正确识别为本地文件 -
资源类型:使用
file://协议时应保持REMOTE类型,因为 Cocos 引擎内部会正确处理 -
缓存清理:记得实现缓存大小限制和清理逻辑,避免缓存无限增长
-
异常处理:在 JNI 调用和文件操作时要做好异常捕获
总结
通过将 MediaPlayer.prepare() 改为 prepareAsync(),成功解决了 Android 视频播放的 ANR 问题。配合视频本地缓存优化,大幅提升了用户体验。
希望这篇文章能帮助到遇到类似问题的开发者!
关键词: Cocos Creator, Android, VideoPlayer, ANR, MediaPlayer, prepareAsync, 视频缓存