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。

相关推荐
通玄11 小时前
Jetpack Compose 入门系列(六):Navigation 3 页面导航
android
rocpp14 小时前
Android 多语言切换实战:从 Context 到 Android 13 应用语言适配
android·kotlin
释然小师弟15 小时前
Android开发十年:反思与回顾
android·后端·嵌入式
黄林晴17 小时前
用了这么久 Koin Scope,原来一直都用错了?
android·kotlin
爱勇宝1 天前
我做了一个只用来搜歌词的小 App
android·前端·后端
众少成多积小致巨1 天前
JNI (Java Native Interface) 技术手册中文参考指南
android·java·c++
Coffeeee2 天前
如何使用Glide和Coil加载WebP动图
android·kotlin·glide
Kapaseker2 天前
5 分钟搞懂 Kotlin DSL
android·kotlin
恋猫de小郭2 天前
AI Agent 开发究竟是啥?如何用 AI 开发 Agent ?深入浅出给你一套概念
android·前端·ai编程