适用于:文件创建 / 删除 / 拷贝 / 读写 / 缓存目录管理 / 资源文件复制 / 图片缓存 / 分享等
支持 Android 10.0 +(Scoped Storage)
一、前言
在 Android 项目中,文件操作几乎是绕不开的基础能力,例如:
- 文件下载保存
- 日志写入
- 缓存管理
- 图片 / 视频文件处理
- 文件复制、删除、重命名
- 适配 Android 10+ 分区存储
- 如果每个模块都写一套文件工具,不仅混乱,而且极易出错。
本文整理了一套 稳定、实用、工程级 FileUtils 工具类,已经在真实项目中长期使用。
二、功能概览
该 FileUtils 工具类主要包含以下功能:
📁 目录管理
- 获取 App 文件目录 / 缓存目录
- 自动创建多级目录
- 判断读写权限
📄 文件操作
- 创建 / 删除 / 移动 / 复制 文件
- 批量删除文件
- 读取 / 写入文件内容
📦 资源处理
- 从 res/raw、drawable 拷贝资源文件
- 将 Bitmap 保存为文件
- 从 Uri 复制文件到本地
🧹 清理功能
- 清空指定文件夹
- 删除缓存目录
📷 多媒体支持
- 文件大小计算
- 文件扩展名解析
- 支持视频 / 图片文件处理
三、核心 FileUtils 工具类源码
✅ 可直接复制使用
❗ 已适配 Android 10+ 分区存储
java
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.text.TextUtils;
import androidx.annotation.AnyRes;
import com.xxx.xxx.base.MyApplication;
import com.xxx.xxx.bean.LocalFileNodeBean;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.text.DecimalFormat;
import java.util.List;
public class FileUtils {
private static final String TAG = "FileUtils";
//Logcat文件夹
public static final String LOGCAT = "logcat";
/**
* 获取应用的文件存储路径,兼容 Android 6.0 以上版本
* Android 版本 推荐路径说明
* Android 10+ (Q / API 29) getExternalFilesDir(...) 是 app 私有目录,不需要权限,更安全。
* Android 9 及以下(API < 29) Environment.getExternalStorageDirectory() 是外部存储根目录,需要权限。
*/
public static String getFilePathDir(Context context) {
if(context == null) {
context = MyApplication.getContext();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS).getAbsolutePath(); // 适用于 Android 10及以上
}
return Environment.getExternalStorageDirectory().getPath(); // 适用于 Android 4.4 及以下
}
/**
* 获取应用的缓存路径,兼容 Android 6.0 以上版本
* <p>
* - Android 10+ 使用 getExternalCacheDir(),无需权限,自动清理。
* - Android 9 及以下可回退到外部存储根目录,但推荐仍使用 getExternalCacheDir。
*/
public static String getFileCachePathDir(Context context) {
if(context == null) {
context = MyApplication.getContext();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
File cacheDir = context.getExternalCacheDir(); // app外部缓存路径(如:/storage/emulated/0/Android/data/包名/cache)
if (cacheDir != null) {
return cacheDir.getAbsolutePath();
}
}
return Environment.getExternalStorageDirectory().getPath();
}
/**
* Logcat存储路径
* (Math.random() * 10) 随机数避免重复文件
*/
public static String getLogcatPathDir(Context context) {
File dir = new File(getFilePathDir(context), FileUtils.LOGCAT);
if (!dir.exists()) {
dir.mkdirs(); // 创建目录(已存在会忽略)
}
return dir.getAbsolutePath();
}
/**
* 应用下载文件夹(自动创建)
*/
public static String getCustomApkUploadFilePathDir(Context context) {
File dir = new File(getFileCachePathDir(context), FileUtils.APK_UPLOAD);
if (!dir.exists()) {
dir.mkdirs(); // 创建目录(已存在会忽略)
}
return dir.getAbsolutePath();
}
/**
* 创建文件夹和文件
*/
public static boolean createFileAndPath(String filePath) {
File file = new File(filePath);
// 获取文件的父目录路径
File parentDir = file.getParentFile();
// 确保父目录存在
if (!parentDir.exists()) {
// 逐级创建目录
if (!parentDir.mkdirs()) {
LogUtils.e(TAG, "Failed to create directory: " + parentDir.getAbsolutePath());
return false;
}
}
try {
// 如果文件不存在,创建文件
if (!file.exists()) {
if (file.createNewFile()) {
LogUtils.d(TAG, "File created: " + filePath);
return true;
} else {
LogUtils.e(TAG, "Failed to create file: " + filePath);
return false;
}
}
} catch (IOException e) {
LogUtils.e(TAG, "Error creating file: " + e.getMessage());
}
return true; // 如果文件已经存在,则直接返回 true
}
public static boolean createFileAndPath(File file) {
// 获取文件的父目录路径
File parentDir = file.getParentFile();
// 确保父目录存在
if (!parentDir.exists()) {
// 逐级创建目录
if (!parentDir.mkdirs()) {
LogUtils.e(TAG, "Failed to create directory: " + parentDir.getAbsolutePath());
return false;
}
}
try {
// 如果文件不存在,创建文件
if (!file.exists()) {
if (file.createNewFile()) {
LogUtils.d(TAG, "File created: " + file.getAbsolutePath());
return true;
} else {
LogUtils.e(TAG, "Failed to create file: " + file.getAbsolutePath());
return false;
}
}
} catch (IOException e) {
LogUtils.e(TAG, "Error creating file: " + e.getMessage());
}
return true; // 如果文件已经存在,则直接返回 true
}
/**
* 创建一个文件夹,逐级创建文件路径
*/
public static boolean createFilePath(String path) {
File file = new File(path);
if (!file.exists()) {
// 逐级创建文件夹路径
if (file.mkdirs()) {
LogUtils.d(TAG, "Created file path: " + path);
return true;
} else {
LogUtils.e(TAG, "Failed to create file path: " + path);
return false;
}
}
return true;
}
public static boolean createFilePath(File file) {
if (!file.exists()) {
// 逐级创建文件夹路径
if (file.mkdirs()) {
LogUtils.d(TAG, "Created file path: " + file.getAbsolutePath());
return true;
} else {
LogUtils.e(TAG, "Failed to create file path: " + file.getAbsolutePath());
return false;
}
}
return true;
}
/**
* 创建一个文件
*/
public static boolean createFile(String path) {
File file = new File(path);
if (!file.exists()) {
try {
return file.createNewFile();
} catch (IOException e) {
LogUtils.e(TAG, "Error creating file: " + e.getMessage());
}
}
return false;
}
/**
* 删除单个文件或文件夹(递归)
*
* @param path 文件路径
* @return 是否删除成功
*/
public static boolean deleteFile(String path) {
if (path == null) return false;
File file = new File(path);
if (!file.exists()) return false;
// 如果是文件夹,递归删除内部文件
if (file.isDirectory()) {
File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
deleteFile(child.getAbsolutePath());
}
}
}
return file.delete(); // 删除自身
}
/**
* 复制文件
*/
public static boolean copyFile(String sourcePath, String destPath) {
File sourceFile = new File(sourcePath);
File destFile = new File(destPath);
try (FileInputStream fis = new FileInputStream(sourceFile);
FileOutputStream fos = new FileOutputStream(destFile);
FileChannel sourceChannel = fis.getChannel();
FileChannel destChannel = fos.getChannel()) {
long size = sourceChannel.size();
sourceChannel.transferTo(0, size, destChannel);
return true;
} catch (IOException e) {
LogUtils.e(TAG, "Error copying file: " + e.getMessage());
}
return false;
}
/**
* 批量删除多个文件或文件夹
*
* @param paths 文件路径列表
* @return 成功删除的数量
*/
public static int deleteFiles(List<String> paths) {
if (paths == null || paths.isEmpty()) return 0;
int successCount = 0;
for (String path : paths) {
if (deleteFile(path)) {
successCount++;
}
}
return successCount;
}
/**
* 清空指定文件夹内的所有文件和子文件夹,但保留文件夹本身
*
* @param folderPath 文件夹路径
* @return 成功删除的数量
*/
public static int clearFolder(String folderPath) {
if (folderPath == null || folderPath.isEmpty()) return 0;
File folder = new File(folderPath);
if (!folder.exists() || !folder.isDirectory()) return 0;
int successCount = 0;
File[] files = folder.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
successCount += clearFolder(file.getAbsolutePath()); // 递归清空子文件夹
if (file.delete()) successCount++; // 删除空子文件夹
} else {
if (file.delete()) successCount++;
}
}
}
return successCount;
}
/**
* 批量删除多个文件或文件夹
*
* @param paths 文件路径列表
* @return 成功删除的数量
*/
public static int deleteSpecialFiles(List<LocalFileNodeBean> paths) {
if (paths == null || paths.isEmpty()) return 0;
int successCount = 0;
for (LocalFileNodeBean path : paths) {
if (deleteFile(path.getFilePath())) {
successCount++;
}
}
return successCount;
}
/**
* 移动文件
*/
public static boolean moveFile(String sourcePath, String destPath) {
File sourceFile = new File(sourcePath);
File destFile = new File(destPath);
return sourceFile.renameTo(destFile);
}
/**
* 读取文件内容(返回字节流)
*/
public static byte[] readFile(String path) {
File file = new File(path);
if (file.exists()) {
try (InputStream is = new FileInputStream(file)) {
byte[] bytes = new byte[(int) file.length()];
is.read(bytes);
return bytes;
} catch (IOException e) {
LogUtils.e(TAG, "Error reading file: " + e.getMessage());
}
}
return null;
}
/**
* 写入文件内容(字节流)
*/
public static boolean writeFile(String path, byte[] data) {
File file = new File(path);
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
LogUtils.e(TAG, "Error creating file: " + e.getMessage());
return false;
}
}
try (OutputStream os = new FileOutputStream(file)) {
os.write(data);
os.flush();
return true;
} catch (IOException e) {
LogUtils.e(TAG, "Error writing file: " + e.getMessage());
}
return false;
}
/**
* 获取文件大小
*/
public static String getFileSize(String path) {
File file = new File(path);
if (file.exists()) {
long size = file.length();
return formatSize(size);
}
return "File not found";
}
/**
* 格式化文件大小,适应不同单位(B,KB,MB,GB)
*/
private static String formatSize(long size) {
DecimalFormat df = new DecimalFormat("#.00");
String fileSizeString;
if (size < 1024) {
fileSizeString = df.format(size) + " B";
} else if (size < 1048576) {
fileSizeString = df.format(size / 1024.0) + " KB";
} else if (size < 1073741824) {
fileSizeString = df.format(size / 1048576.0) + " MB";
} else {
fileSizeString = df.format(size / 1073741824.0) + " GB";
}
return fileSizeString;
}
/**
* 判断是否具有写入权限
*/
public static boolean canWrite(String path) {
File file = new File(path);
return file.exists() && file.canWrite();
}
/**
* 判断是否具有读取权限
*/
public static boolean canRead(String path) {
File file = new File(path);
return file.exists() && file.canRead();
}
/**
* 获取文件名(包含扩展名),例如:/storage/emulated/0/Movies/test.mp4 → test.mp4
*/
public static String getFileNameWithExtension(String path) {
if (path == null || path.isEmpty()) return "";
return new File(path).getName();
}
/**
* 获取文件名(不包含扩展名),例如:/storage/emulated/0/Movies/test.mp4 → test
*/
public static String getFileNameWithoutExtension(String path) {
if (path == null || path.isEmpty()) return "";
String fileName = new File(path).getName();
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex > 0 && dotIndex < fileName.length()) {
return fileName.substring(0, dotIndex);
}
return fileName;
}
/**
* 获取文件扩展名,例如:/storage/emulated/0/Movies/test.mp4 → mp4
*/
public static String getFileExtension(String path) {
if (path == null || path.isEmpty()) return "";
String fileName = new File(path).getName();
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex >= 0 && dotIndex < fileName.length() - 1) {
return fileName.substring(dotIndex + 1);
}
return "";
}
/**
* 获取文件扩展名(包含点),例如:/storage/emulated/0/Movies/test.mp4 → .mp4
*/
public static String getFileExtensionWithDot(String path) {
if (path == null || path.isEmpty()) return "";
String fileName = new File(path).getName();
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex >= 0 && dotIndex < fileName.length() - 1) {
return fileName.substring(dotIndex); // 保留点号
}
return "";
}
/**
* 将用户选择的 Uri 文件复制到 app 的缓存目录中,
* 返回 FFmpeg 可识别的本地文件路径。
*
* @param uri 用户选择的图片 Uri(content://)
* @return 本地缓存文件的绝对路径,失败时返回 null
*/
public static File copyUriToFile(Context context, Uri uri, String filePath, String customFile) {
try {
InputStream inputStream = context.getContentResolver().openInputStream(uri);
if (inputStream == null) return null;
File outFile = new File(filePath, customFile);
if (outFile.exists()) {
outFile.delete(); // 可选:删除旧文件
}
OutputStream outputStream = new FileOutputStream(outFile);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
outputStream.close();
inputStream.close();
return outFile;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将 drawable/raw 中的图片资源保存为本地缓存文件
*
* @param context 上下文
* @param resId drawable 中的资源 ID(如 R.drawable.xxx)
* @param fileParent 生成的文件根目录
* @param fileName 生成的文件名(如 header.jpg)
* @return 返回 File 文件,可用于上传或传给 FFmpeg
*/
public static File copyResToFile(Context context, @AnyRes int resId, String fileParent, String fileName) {
try {
@SuppressLint("ResourceType") InputStream inputStream = context.getResources().openRawResource(resId);
File outFile = new File(fileParent, fileName);
OutputStream outputStream = new FileOutputStream(outFile);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
outputStream.close();
inputStream.close();
return outFile;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 确保将资源文件只复制一次到本地指定目录下,如果文件已存在则直接返回
*
* @param context 上下文
* @param resId 资源 ID(如 R.raw.xxx 或 R.drawable.xxx)
* @param fileParent 目标文件夹绝对路径
* @param fileName 目标文件名(如 "video.mp4")
* @return 返回 File 对象,失败时返回 null
*/
public static File ensureResFileCopiedOnce(Context context, @AnyRes int resId, String fileParent, String fileName) {
try {
@SuppressLint("ResourceType") InputStream inputStream = context.getResources().openRawResource(resId);
File outFile = new File(fileParent, fileName);
// 如果文件已存在,直接返回
if (outFile.exists() && outFile.length() > 0) {
return outFile;
}
OutputStream outputStream = new FileOutputStream(outFile);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
outputStream.close();
inputStream.close();
return outFile;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将 Bitmap 保存到 app 的外部缓存目录中,返回文件对象
*
* @param context 上下文
* @param bitmap 要保存的图片
* @param fileName 保存的文件名(如:screenshot.png)
*/
public static File saveBitmapToCache(Context context, Bitmap bitmap, String filePath, String fileName) {
try {
// 创建文件
File file = new File(filePath, fileName);
if (file.exists()) {
file.delete(); // 可选:删除旧文件
}
// 使用输出流将 Bitmap 压缩为 PNG 并写入文件
FileOutputStream fos = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.flush();
fos.close();
return file; // 返回文件路径
} catch (IOException e) {
e.printStackTrace();
return null; // 出错返回 null
}
}
/**
* 异步复制相册图片到指定路径(目标路径为字符串),带成功/失败回调
*
* @param sourcePath 相册源文件路径
* @param targetPath 目标文件完整路径(如 /data/data/.../temp_watermark.png)
* @param listener 复制结果回调:onSuccess(String path) / onFailure(String msg)
*/
public static void copyAlbumImageAsync(String sourcePath, String targetPath, EventListener<String> listener) {
if (TextUtils.isEmpty(sourcePath)) {
if (listener != null) listener.onAction(EnumUtils.EventListenerAction.FAILURE, "源文件路径为空");
return;
}
File srcFile = new File(sourcePath);
if (!srcFile.exists()) {
if (listener != null) listener.onAction(EnumUtils.EventListenerAction.FAILURE, "源文件不存在: " + sourcePath);
return;
}
if (TextUtils.isEmpty(targetPath)) {
if (listener != null) listener.onAction(EnumUtils.EventListenerAction.FAILURE, "目标路径为空");
return;
}
File targetFile = new File(targetPath);
File parentDir = targetFile.getParentFile();
if (parentDir != null && !parentDir.exists()) {
parentDir.mkdirs();
}
new Thread(() -> {
try (InputStream in = new FileInputStream(srcFile);
OutputStream out = new FileOutputStream(targetFile)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
out.flush();
LogUtils.d(TAG, "图片复制成功: " + targetFile.getAbsolutePath());
if (listener != null) {
listener.onAction(EnumUtils.EventListenerAction.SUCCESS, targetFile.getAbsolutePath());
}
} catch (IOException e) {
LogUtils.e(TAG, "复制失败: " + e.getMessage());
if (listener != null) {
listener.onAction(EnumUtils.EventListenerAction.FAILURE, "复制失败: " + e.getMessage());
}
}
}).start();
}
/**
* 获取其他文件夹下某个文件的完整路径,并自动创建目录
*
* <p>
* 使用场景:
* 1. 存放地球动画文件
* 2. AI编辑生成的封面图
* 3. 其他需要存放的资源文件
* </p>
*
* @param fileName 文件名,例如 "my_video.mp4" 或 "cover.png"
* @return 完整文件路径,例如:
* /storage/emulated/0/Android/data/com.yourapp/files/other_files/my_video.mp4
*/
public static String getOtherFileFullPath(String filePath, String fileName) {
// 4. 使用 File 拼接文件路径,避免手动拼接字符串出错
File targetFile = new File(filePath, fileName);
// 5. 返回完整路径字符串
return targetFile.getAbsolutePath();
}
}
LocalFileNodeBean 类
java
/**
* Author: Su
* Date: 2025/12/20
* Description: Description
*/
public class LocalFileNodeBean implements Comparable<LocalFileNodeBean> {
private String filePath;
private String fileName;
private long fileSize;
private long lastModified;
private EnumUtils.MediaFileInfoType fileType;
public boolean isEditState; //编辑状态
public boolean isSelect; //选中状态
public String progress; //下载进度
public LocalFileNodeBean(File file, EnumUtils.MediaFileInfoType type) {
this.filePath = file.getAbsolutePath();
this.fileName = file.getName();
this.fileSize = file.length();
this.lastModified = file.lastModified();
this.fileType = type;
}
public LocalFileNodeBean(String filePath, String fileName, long fileSize, long lastModified, EnumUtils.MediaFileInfoType type) {
this.filePath = filePath;
this.fileName = fileName;
this.fileSize = fileSize;
this.lastModified = lastModified;
this.fileType = type;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public long getFileSize() {
return fileSize;
}
public void setFileSize(long fileSize) {
this.fileSize = fileSize;
}
public EnumUtils.MediaFileInfoType getFileType() {
return fileType;
}
public void setFileType(EnumUtils.MediaFileInfoType fileType) {
this.fileType = fileType;
}
public long getLastModified() {
return lastModified;
}
public void setLastModified(long lastModified) {
this.lastModified = lastModified;
}
public boolean isEditState() {
return isEditState;
}
public void setEditState(boolean editState) {
isEditState = editState;
}
public boolean isSelect() {
return isSelect;
}
public void setSelect(boolean select) {
isSelect = select;
}
public String getProgress() {
return progress;
}
public void setProgress(String progress) {
this.progress = progress;
}
// --- 支持按时间倒序、名称升序排序 ---
@Override
public int compareTo(LocalFileNodeBean other) {
int result = Long.compare(other.lastModified, this.lastModified); // 倒序:新时间在前
if (result == 0 && !this.fileName.equals(other.fileName)) {
return this.fileName.compareToIgnoreCase(other.getFileName()); // 名称升序
}
return result;
}
}
四、总结
✔ 覆盖文件操作 90% 使用场景
✔ 兼容 Android 10+ Scoped Storage
✔ 项目级通用工具类
✔ 适合中大型项目直接复用