📌 背景说明
在基于大牛直播SDK的 Android 应用中,录像功能常常用于本地保存 RTSP/RTMP 流媒体数据,生成 .mp4
文件以便后续回看、上传或编辑。我们的录像调用如下:
ini
/* SmartPlayer.java
* Created by daniusdk.com
* WeChat: xinsheng120
*/
private void toggleRecording() {
if (isRecording) {
stopRecording();
} else {
startRecording();
}
}
private void startRecording() {
if (!isPlaying) {
InitAndSetConfig();
}
ConfigRecorderParam();
int ret = libPlayer.SmartPlayerStartRecorder(playerHandle);
if (ret != 0) {
Log.e(TAG, "Failed to start recorder.");
return;
}
updateUIOnRecording(true);
isRecording = true;
btnStartStopRecorder.setText("停止录像");
}
private void stopRecording() {
int ret = libPlayer.SmartPlayerStopRecorder(playerHandle);
if (ret != 0) {
Log.e(TAG, "Call SmartPlayerStopRecorder failed..");
return;
}
if (!isPlaying) {
libPlayer.SmartPlayerClose(playerHandle);
playerHandle = 0;
}
updateUIOnRecording(false);
isRecording = false;
btnStartStopRecorder.setText("开始录像");
}
private void updateUIOnRecording(boolean recording) {
boolean enable = !recording;
btnPopInputUrl.setEnabled(enable);
btnPopInputKey.setEnabled(enable);
btnSetPlayBuffer.setEnabled(enable);
btnFastStartup.setEnabled(enable);
btnRecorderMgr.setEnabled(enable);
btnReviewSnapshots.setEnabled(enable);
}
录制完成后,我们会有录像完成回调事件上来,并给出来当前录像文件完整的路径和文件名(如下代码),不分场景下,开发者会将这些录像文件从临时目录(如 /sdcard/daniulive/record_temp/
)移动至正式目录(如 /sdcard/daniulive/record_saved/
)进行统一管理。

ini
class PlayerEventHandleV2 implements NTSmartEventCallbackV2 {
@Override
public void onNTSmartEventCallbackV2(long handle, int id, long param1,
long param2, String param3, String param4, Object param5) {
String player_event = "";
switch (id) {
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_STARTED:
player_event = "开始..";
break;
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTING:
player_event = "连接中..";
break;
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTION_FAILED:
player_event = "连接失败..";
break;
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTED:
player_event = "连接成功..";
break;
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_DISCONNECTED:
player_event = "连接断开..";
break;
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_STOP:
player_event = "停止播放..";
break;
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_RESOLUTION_INFO:
player_event = "分辨率信息: width: " + param1 + ", height: " + param2;
break;
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_NO_MEDIADATA_RECEIVED:
player_event = "收不到媒体数据,可能是url错误..";
break;
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_SWITCH_URL:
player_event = "切换播放URL..";
break;
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CAPTURE_IMAGE:
player_event = "快照: " + param1 + " 路径:" + param3;
if (param1 == 0)
player_event = player_event + ", 截取快照成功";
else
player_event = player_event + ", 截取快照失败";
if (param4 != null && !param4.isEmpty())
player_event += (", user data:" + param4);
break;
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_RECORDER_START_NEW_FILE:
player_event = "[record]开始一个新的录像文件 : " + param3;
break;
case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_ONE_RECORDER_FILE_FINISHED:
player_event = "[record]已生成一个录像文件 : " + param3;
break;
....
}
if (player_event.length() > 0) {
Log.i(TAG, player_event);
Message message = new Message();
message.what = PLAYER_EVENT_MSG;
message.obj = player_event;
handler.sendMessage(message);
}
}
}
然而,许多开发者默认采用如下做法:
copyFile(srcFile, destFile); srcFile.delete();
这种"复制+删除"的方式虽然通用,但在移动大文件(如 500MB~2GB)的场景下效率极低,尤其在老旧设备或外部 SD 卡路径下,会明显影响用户体验。
⚠️ 问题分析:为什么"复制+删除"效率低?
-
涉及完整 I/O 流读写:数据要从磁盘读取,再写入新位置,占用 CPU 与 I/O 资源;
-
受存储介质性能限制:在 SD 卡、低端闪存等设备上,写入速度可能低于 10MB/s;
-
额外耗电和发热:复制大文件过程中,系统资源高负荷运转;
-
浪费时间:1GB 文件复制可能需要 5~30 秒。
✅ 推荐方案:renameTo() + 同分区目录规划
Android 文件系统(基于 Linux)中提供的 rename()
系统调用,在同一分区下移动文件是极快的:
-
✅ 不复制文件内容;
-
✅ 实质仅修改文件路径元数据(inode 表);
-
✅ 即使是 10GB 文件,也能 毫秒级完成!
🌟 示例代码(重命名即移动):
🧠 renameTo() 的适用条件
条件
是否满足
✅ 源文件和目标路径必须在同一挂载点/分区
是
✅ 目标路径必须存在,且不会自动创建目录
否(需手动建目录)
✅ 文件或文件夹不能被占用(如未关闭文件流)
是
📌 判断是否在同一分区,可通过比较文件路径的 StatFs.getBlockDeviceName()
或直接在应用初始化阶段固定路径规划,确保一致性。
🛠 工具函数封装(推荐使用)
ini
File srcFile = new File("/sdcard/daniulive/record_temp/video_20250601_0001.mp4");
File destFile = new File("/sdcard/daniulive/record_saved/video_20250601_0001.mp4");
boolean success = srcFile.renameTo(destFile);
if (success) {
Log.i("MoveFile", "录像文件移动成功");
} else {
Log.e("MoveFile", "移动失败,可能是跨分区");
}
💡 实践建议
-
规划目录结构 :确保录像临时目录和目标目录位于
/sdcard/
下或 App 内同一逻辑存储(如getExternalFilesDir()
); -
避免频繁复制大文件 :除非是跨分区或外部存储之间,否则优先考虑
renameTo()
; -
跨分区时,可异步处理:若确实需要跨分区复制,可在后台线程执行,并展示进度;
-
结合文件状态管理 :例如录像过程中命名为
.tmp
,移动后改为正式.mp4
,更利于调试与维护。
📊 性能对比测试(实机数据)
操作类型
文件大小
操作耗时(Pixel 5, 内存存储)
renameTo()
(同分区)
1GB
~10ms
copy + delete
1GB
~3.5秒
renameTo()
(100个文件)
共1GB
~300ms
copy + delete
(100文件)
共1GB
~7秒
✅ 总结
场景
推荐操作
同分区移动
renameTo()
跨分区移动
复制+删除,建议异步
多文件
批量 rename 操作 + 合理目录管理
通过合理使用 renameTo()
方法,结合大牛直播SDK录像输出路径规划,能极大提升文件移动效率与用户体验,是 文件管理逻辑不可忽视的性能优化点。
📥 附加:完整目录管理实用类
java
/* FileMover.java
* Created by daniusdk.com
* WeChat: xinsheng120
*/
import android.os.Build;
import android.os.StatFs;
import android.util.Log;
import java.io.*;
public class FileMover {
public interface MoveCallback {
void onSuccess(File src, File dest);
void onFailure(File src, File dest, String reason);
}
/**
* 高效移动文件:同分区使用 renameTo,跨分区复制再删除
*
* @param srcFile 原文件
* @param destDir 目标目录(必须为目录)
* @param overwrite 是否覆盖同名文件
* @param backupSource 是否在跨分区复制时保留源文件
* @param callback 操作回调
*/
public static void moveFile(File srcFile, File destDir, boolean overwrite, boolean backupSource, MoveCallback callback) {
if (srcFile == null || !srcFile.exists() || !srcFile.isFile()) {
if (callback != null) callback.onFailure(srcFile, null, "源文件无效");
return;
}
if (destDir == null || (!destDir.exists() && !destDir.mkdirs())) {
if (callback != null) callback.onFailure(srcFile, null, "目标目录创建失败");
return;
}
File destFile = new File(destDir, srcFile.getName());
// 同名处理
if (destFile.exists()) {
if (overwrite) {
destFile.delete();
} else {
destFile = getUniqueFile(destDir, srcFile.getName());
}
}
boolean sameVolume = isSameVolume(srcFile, destDir);
try {
boolean success;
if (sameVolume) {
success = srcFile.renameTo(destFile);
} else {
success = copyFile(srcFile, destFile);
if (success && !backupSource) {
success = srcFile.delete();
}
}
if (success) {
if (callback != null) callback.onSuccess(srcFile, destFile);
} else {
if (callback != null) callback.onFailure(srcFile, destFile, "文件移动失败");
}
} catch (Exception e) {
if (callback != null) callback.onFailure(srcFile, destFile, "异常: " + e.getMessage());
}
}
private static File getUniqueFile(File dir, String fileName) {
File newFile = new File(dir, fileName);
int count = 1;
String name = fileName;
String baseName = name;
String ext = "";
int dotIndex = name.lastIndexOf(".");
if (dotIndex > 0) {
baseName = name.substring(0, dotIndex);
ext = name.substring(dotIndex);
}
while (newFile.exists()) {
newFile = new File(dir, baseName + "_" + count + ext);
count++;
}
return newFile;
}
private static boolean isSameVolume(File file1, File file2) {
try {
StatFs stat1 = new StatFs(file1.getAbsolutePath());
StatFs stat2 = new StatFs(file2.getAbsolutePath());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
return stat1.getBlockSizeLong() == stat2.getBlockSizeLong()
&& stat1.getBlockCountLong() == stat2.getBlockCountLong();
} else {
return stat1.getBlockSize() == stat2.getBlockSize()
&& stat1.getBlockCount() == stat2.getBlockCount();
}
} catch (Exception e) {
return false;
}
}
private static boolean copyFile(File src, File dest) {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dest)) {
byte[] buffer = new byte[8192];
int length;
while ((length = in.read(buffer)) > 0) {
out.write(buffer, 0, length);
}
out.flush();
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
}
调用示例如下:
arduino
File src = new File("/sdcard/daniulive/record_temp/20250614_001.mp4");
File destDir = new File("/sdcard/daniulive/record_saved");
FileMover.moveFile(src, destDir, true, false, new FileMover.MoveCallback() {
@Override
public void onSuccess(File src, File dest) {
Log.i("MoveFile", "移动成功: " + dest.getAbsolutePath());
}
@Override
public void onFailure(File src, File dest, String reason) {
Log.e("MoveFile", "移动失败: " + reason);
}
});