Android文件读写和分享总结

在日志中看到好多警告和错误,是第三方SDK中的文件权限问题。想升级第三方库,又懒得升。有些已经停止维护了。整理一篇文件读写,等到真不行,只能下载他们的源码自己修改了。

Android文件读写和分享总结


一、目录怎么选

目录 示例路径 权限 谁用 是否推荐
内部私有目录 /data/data/包名/files/ 不需要 App 自己保存配置、缓存、数据库、token 强烈推荐
内部缓存目录 /data/data/包名/cache/ 不需要 临时文件、网络缓存 推荐
外部专属目录 /storage/emulated/0/Android/data/包名/files/ 不需要 App 自己的大文件、日志、导出前临时文件 推荐
公共图片/视频/音频 Pictures/、Movies/、Music/ Android 10+ 写自己文件不需要权限 用户可见媒体 推荐用 MediaStore
公共下载目录 /storage/emulated/0/Download/ Android 10+ 用 MediaStore/SAF PDF、Excel、ZIP、导出文件 推荐用 SAF 或 MediaStore Downloads
任意用户选择文件 用户手动选择 不需要传统存储权限 打开 PDF、Excel、TXT、ZIP 推荐用 SAF
根目录/系统目录 /system/、/data/、其他 App 目录 普通 App 不能访问 系统/Root 不要碰

Android 11 起,分区存储对外部存储访问限制更严格,target API 30+ 必须使用 scoped storage;Android 10 之前还能靠路径和 READ/WRITE_EXTERNAL_STORAGE,但现在不建议这样做。


二、权限规则

1. App 自己目录:不需要权限

csharp 复制代码
context.filesDir
context.cacheDir
context.getExternalFilesDir(null)
context.externalCacheDir

这些 Android 7--16 都稳定可用。


2. 访问相册图片/视频/音频

Android 13+:

ini 复制代码
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

Android 12 及以下:

ini 复制代码
<uses-permission
    android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="32" />

Android 官方说明:访问其他 App 创建的媒体文件,需要声明对应的媒体读取权限;但 Android 10+ 访问自己写入 MediaStore 的媒体文件,不需要存储权限。


3. 写入图片、视频、音频、下载文件

推荐:

复制代码
Android 10+:MediaStore
Android 7-9:WRITE_EXTERNAL_STORAGE 或退回 App 专属目录

4. PDF、Excel、ZIP、TXT 等普通文件

推荐:

复制代码
让用户选择保存位置:ACTION_CREATE_DOCUMENT
让用户选择打开文件:ACTION_OPEN_DOCUMENT

也就是 SAF,优点是 Android 7--16 都能用,而且不需要传统存储权限。


三、Manifest 权限

xml 复制代码
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
​
    <!-- Android 12 及以下读取公共文件 -->
    <uses-permission
        android:name="android.permission.READ_EXTERNAL_STORAGE"
        android:maxSdkVersion="32" />
​
    <!-- Android 9 及以下写公共目录 -->
    <uses-permission
        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="28" />
​
    <!-- Android 13+ 读取媒体 -->
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
​
</manifest>

四、Android 7--16 通用文件存储

1. 保存 App 私有文件

kotlin 复制代码
fun savePrivateFile(context: Context, fileName: String, content: String) {
    val file = File(context.filesDir, fileName)
    file.writeText(content)
}
​
fun readPrivateFile(context: Context, fileName: String): String {
    val file = File(context.filesDir, fileName)
    return if (file.exists()) file.readText() else ""
}

适合保存:

复制代码
配置
草稿
token
用户偏好
App 内部数据

2. 保存外部 App 专属文件

kotlin 复制代码
fun saveExternalAppFile(context: Context, fileName: String, content: String): File {
    val dir = context.getExternalFilesDir("logs")
    if (dir != null && !dir.exists()) {
        dir.mkdirs()
    }
​
    val file = File(dir, fileName)
    file.writeText(content)
    return file
}

路径类似:

bash 复制代码
/storage/emulated/0/Android/data/你的包名/files/logs/app.log

特点:

复制代码
不需要权限
用户一般看不到
App 卸载后会被删除
适合日志、临时导出文件、大缓存

3. 保存图片到相册 MediaStore

kotlin 复制代码
fun saveImageToPictures(
    context: Context,
    fileName: String,
    bytes: ByteArray
): Uri? {
    val resolver = context.contentResolver
​
    val values = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
        put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
​
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/MyApp")
            put(MediaStore.Images.Media.IS_PENDING, 1)
        }
    }
​
    val uri = resolver.insert(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        values
    ) ?: return null
​
    resolver.openOutputStream(uri)?.use { output ->
        output.write(bytes)
    }
​
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        values.clear()
        values.put(MediaStore.Images.Media.IS_PENDING, 0)
        resolver.update(uri, values, null, null)
    }
​
    return uri
}

适合:

复制代码
保存图片
保存截图
保存二维码
保存用户能在相册看到的图片

4. 保存文件到 Download 目录

kotlin 复制代码
fun saveFileToDownload(
    context: Context,
    fileName: String,
    mimeType: String,
    bytes: ByteArray
): Uri? {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        val resolver = context.contentResolver
​
        val values = ContentValues().apply {
            put(MediaStore.Downloads.DISPLAY_NAME, fileName)
            put(MediaStore.Downloads.MIME_TYPE, mimeType)
            put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/MyApp")
            put(MediaStore.Downloads.IS_PENDING, 1)
        }
​
        val uri = resolver.insert(
            MediaStore.Downloads.EXTERNAL_CONTENT_URI,
            values
        ) ?: return null
​
        resolver.openOutputStream(uri)?.use {
            it.write(bytes)
        }
​
        values.clear()
        values.put(MediaStore.Downloads.IS_PENDING, 0)
        resolver.update(uri, values, null, null)
​
        uri
    } else {
        null
    }
}

Android 10+ 推荐这样写。


5. Android 7--9 保存到 Download

kotlin 复制代码
@Suppress("DEPRECATION")
fun saveFileToDownloadLegacy(
    fileName: String,
    bytes: ByteArray
): File {
    val dir = Environment.getExternalStoragePublicDirectory(
        Environment.DIRECTORY_DOWNLOADS
    )
​
    if (!dir.exists()) {
        dir.mkdirs()
    }
​
    val file = File(dir, fileName)
    file.writeBytes(bytes)
    return file
}

注意:Android 7--9 需要申请:

ini 复制代码
<uses-permission
    android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="28" />

6. SAF:让用户选择保存位置

kotlin 复制代码
fun createDocumentLauncher(activity: Activity) {
    val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "text/plain"
        putExtra(Intent.EXTRA_TITLE, "demo.txt")
    }
​
    activity.startActivityForResult(intent, 1001)
}

写入用户选择的文件:

kotlin 复制代码
fun writeToUri(context: Context, uri: Uri, content: String) {
    context.contentResolver.openOutputStream(uri)?.use { output ->
        output.write(content.toByteArray())
    }
}

适合:

复制代码
PDF
Excel
Word
ZIP
TXT
用户自己决定保存到哪里

7. SAF:让用户选择打开文件

kotlin 复制代码
fun openDocument(activity: Activity) {
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "*/*"
    }
​
    activity.startActivityForResult(intent, 1002)
}

读取:

kotlin 复制代码
fun readFromUri(context: Context, uri: Uri): ByteArray {
    return context.contentResolver.openInputStream(uri)?.use {
        it.readBytes()
    } ?: ByteArray(0)
}

五、权限申请代码

kotlin 复制代码
fun requestStoragePermission(activity: Activity) {
    if (Build.VERSION.SDK_INT >= 33) {
        activity.requestPermissions(
            arrayOf(
                android.Manifest.permission.READ_MEDIA_IMAGES,
                android.Manifest.permission.READ_MEDIA_VIDEO
            ),
            2001
        )
    } else if (Build.VERSION.SDK_INT >= 23) {
        activity.requestPermissions(
            arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE),
            2001
        )
    }
}

六、实际开发推荐方案

场景 推荐方案
保存登录信息、配置 filesDir
保存缓存 cacheDir
保存日志 getExternalFilesDir("logs")
保存图片到相册 MediaStore.Images
保存视频到相册 MediaStore.Video
保存音频 MediaStore.Audio
导出 PDF/Excel ACTION_CREATE_DOCUMENT
用户选择文件上传 ACTION_OPEN_DOCUMENT
下载文件给用户看 Android 10+ 用 MediaStore.Downloads,或者 SAF
扫描整个手机文件 不建议,除非文件管理器类 App

FileProvider是 Android 文件共享机制。

FileProvider 不负责存文件,它负责把文件安全地分享给其他 App。**


一、为什么需要 FileProvider

Android 7.0(API 24)开始禁止暴露 file://

以前:

ini 复制代码
File file = new File("/sdcard/test.jpg");
​
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(
        Uri.fromFile(file),
        "image/*"
);
startActivity(intent);

得到的 URI:

perl 复制代码
file:///sdcard/test.jpg

Android 7 开始直接崩溃:

复制代码
FileUriExposedException

因为:

复制代码
你的 App
↓
直接暴露真实路径
↓
其它 App

存在安全问题。

于是 Google 推出了:

less 复制代码
content://

例如:

less 复制代码
content://com.demo.fileprovider/images/test.jpg

这时候真实路径被隐藏。


二、什么时候必须用

场景1:安装 APK

最常见。

错误写法:

ini 复制代码
Uri uri = Uri.fromFile(apkFile);

正确写法:

ini 复制代码
Uri uri = FileProvider.getUriForFile(
        context,
        "com.demo.fileprovider",
        apkFile
);

然后:

ini 复制代码
intent.addFlags(
        Intent.FLAG_GRANT_READ_URI_PERMISSION
);

场景2:调用相机拍照

相机需要写入照片。

错误:

scss 复制代码
Uri.fromFile(photoFile)

正确:

scss 复制代码
FileProvider.getUriForFile(...)
intent.putExtra(
        MediaStore.EXTRA_OUTPUT,
        photoUri
);

场景3:分享图片

微信

QQ

钉钉

邮件

浏览器

等。

例如:

arduino 复制代码
Intent.ACTION_SEND
content://

场景4:打开 PDF

bash 复制代码
ACTION_VIEW
application/pdf

场景5:打开 Excel

bash 复制代码
application/vnd.ms-excel

场景6:打开 Word

bash 复制代码
application/msword

场景7:查看日志

例如:

c 复制代码
app.log

分享给技术支持。


三、什么时候不需要

读取自己的文件

arduino 复制代码
File file =
        new File(getFilesDir(),"a.txt");

直接读:

scss 复制代码
file.readText()

即可。


写自己的文件

复制代码
filesDir
cacheDir

不需要。


MediaStore

例如:

复制代码
MediaStore.Images

已经返回:

less 复制代码
content://

不需要 FileProvider。


SAF

复制代码
ACTION_OPEN_DOCUMENT

返回:

less 复制代码
content://

不需要 FileProvider。


四、配置方法

AndroidManifest

ini 复制代码
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
​
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths"/>
​
</provider>

res/xml/file_paths.xml

允许共享哪些目录

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<paths>
​
    <files-path
        name="files"
        path="."/>
​
    <cache-path
        name="cache"
        path="."/>
​
    <external-files-path
        name="external"
        path="."/>
​
</paths>

五、完整示例

假设:

bash 复制代码
/storage/emulated/0/Android/data/com.demo/files/test.pdf

分享 PDF:

ini 复制代码
File file = new File(
        getExternalFilesDir(null),
        "test.pdf"
);
​
Uri uri = FileProvider.getUriForFile(
        this,
        getPackageName()+".fileprovider",
        file
);
​
Intent intent =
        new Intent(Intent.ACTION_VIEW);
​
intent.setDataAndType(
        uri,
        "application/pdf"
);
​
intent.addFlags(
        Intent.FLAG_GRANT_READ_URI_PERMISSION
);
​
startActivity(intent);

六、和 MediaStore、SAF 的关系

方案 用途 是否需要 FileProvider
filesDir App私有文件 ❌
cacheDir 缓存 ❌
getExternalFilesDir App专属目录 ❌
MediaStore 图片视频音频 ❌
SAF 用户选文件 ❌
分享文件给别的App 文件共享 ✅
安装APK 文件共享 ✅
调用相机拍照 文件共享 ✅
打开PDF 文件共享 ✅
分享日志 文件共享 ✅

整理完后,发现思路很清晰了,一般文件保存到App私有文件夹中,大文件保存到APP专属目录中。如果是安装apk或分享文件和图片,那么需要用到FileProvider。

相关推荐
三少爷的鞋10 小时前
AI 时代,我们都将成为通才型开发者:只懂 Android,已经不够了
android
Dovis(誓平步青云)14 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
晚风叙码16 小时前
MySQL 数据类型详解:从数值到字符串,一篇讲透
android·mysql·adb
传奇开心果编程16 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程19 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程20 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程21 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程21 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓21 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程21 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer