一、版本变动参考文档
2、行为变更:以 Android 12 为目标平台的应用 | Android 开发者 | Android Developers
二、文件基本概念
1、内存RAM:随机存取存储器(Random Access Memory)
2、内部存储:/data/user/0/
- 开发更熟悉的
/data/data/<package> - 实际上是
/data/user/<current_user_id>/<package>的一个链接,current_user_id默认为0,第一个用户
files目录:/data/data//files【val path = filesDir.absolutePath】
cache目录:/data/data//cache【val path = cacheDir.absolutePath】
shared目录:/data/data//shared_prefs【一般sharePrefs会保存在这里】 databases目录:data/data//databses【一般sqlite会保存在这里】
3、外部存储:
1、 简单介绍
1、支持外置SD卡,所以有内外存概念;
- 后续安卓内部存储空间通过fuse技术挂载到
/storage/emulated/0上,这个挂载点就是外部存储 - 图片里可以看到SDCard是个软链接,可以不用太在意【安卓古早机子和新机结构都这样】
storage/emulated/0/基本文件夹
sdcard/基本文件夹
2、 外部也分为私有/公有存储目录
- 外部私有目录的作用:大文件推荐存外部私有存储
/storage/emulated/0/Android/data/<package>
files目录: /storage/emulated/0/Android/data//files
【val path = getExternalFilesDir("")?.absolutePath ?: ""】
cache目录: /storage/emulated/0/Android/data//cache
【val path = externalCacheDir?.absolutePath ?: ""】
-
getExternalFilesDir()是需要一个字符串参数的,空串就是files;* 传入"test",则获取到:/storage/emulated/0/Android/data/<package>/files/test; 更推荐用这个,getExternalStoragePublicDirectory在android 10 (api 29)已过时 -
Environment有官方推荐命名,例如:DIRECT_DCIM
3、除了私有目录以外的目录,都是公有目录。程序保存在公有目录中的数据,在应用被删除后,仍然保留。
常见的有:Downloads、Documents、Pictures 、DCIM、Movies、Music、Ringtones等
val ringtoneDir = File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "ringtones").apply { mkdirs() }
val ringtoneFile = File(ringtoneDir, "selected_ringtone.mp3")
//对应
getExternalFilesDir->/storage/emulated/0/Android/data/com.kakusummer.androidutilsturbo/files/Download/ringtones/selected_rin
2、方法介绍
1、Environment.getExternalStoragePublicDirectory,存放外部公有
- 存在这个位置可以在手机"最近"文件中显示,DIRECTORY_DOCUMENTS目录显示不出来
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
- getExternalFilesDir(String type):这是 Context 类的方法,用来获取应用专属的外部存储目录
三、文件版本变迁
一、Android Q(10) - API 29
1、基本知识
外部私有目录,你可以在不需要特殊权限的情况下进行读写操作
外部公共目录:
- Android 10 及以上版本:访问外部存储文件需要使用
Scoped Storage,需要特别注意 Android 10 引入的存储权限限制。你可以使用MediaStoreAPI 来访问公共目录,并且无需READ_EXTERNAL_STORAGE或WRITE_EXTERNAL_STORAGE权限,除非你需要访问所有文件。- Android 9(API 级别 28)及以下版本:需要声明
READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限。
1、 提出了分区存储-Scoped Storage模式;
2、 私有目录的读写没有变化,和之前一样,仍可以使用File那一套,且不需任何权限【通过intent获取的content:// 可mediaplayer播放,荣耀等部分机子必须得申请权限才能使用,狗屎】
- 这个链接在Activity能播放,广播接收器和Service里播不了
3、 对于公有目录必须使用
- 使用
MediaStore提供的API:访问图片、视频、音频这三类(基本思路是通过contentResolver与对应MediaStore标志结合)。 - 其他使用
SAF(存储访问框架)。
4、暴力申请 MANAGE_EXTERNAL_STORAGE权限(API 30):
- 不用刻意使用SAF,可以使用File那一套;
- Google play可能会比较严格,国内倒还好。
- 这种机子就算写了清单文件
READ_EXTERNAL_STORAGE或WRITE_EXTERNAL_STORAGE权限,设置页也读不出来。
5、申请其他三个细分的权限
- 清单文件
- 这俩最高只支持到Target32,项目中如果设置大于32,APP设置页就不会显示这个开关,如果设置小于等于32,即使是高版本的机子也能显示
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
- 具体显示的样式看机子,低版本可能只有一个开关,高版本会细化为两个开关
- 大于Targt32的解决方案
-
申请文件管理
-
优点:简单粗暴
-
缺点:部分应用可能上不了架【Google Play推荐文件管理项目使用该权限】
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
if (!Environment.isExternalStorageManager()) {
//跳转新页面申请权限
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
intent.data = Uri.parse("package:$packageName")
startActivityForResult(intent, 101)
}
- 声明对应的权限,代码中动态申请对应的
<!--Android 13版本适配,细化存储权限-->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
2、 Scoped Storage模式介绍
1、提供外部存储空间视图模式:Legacy View、Filtered View
- Legacy View-兼容模式:兼容以前访问逻辑,申请权限后App可访问外部存储,拥有完整的访问权限
- Android Q上,TargetSDK大于或等于29的APP默认Filtered View,反之则默认被赋予Legacy View;
- AndroidManifest.xml中设置新属性requestLegacyExternalStorage来修改外部存储空间视图模式,true为兼容模式;
- 非常重要!!!项目里最好都写起来
android:requestLegacyExternalStorage="true"
<meta-data
android:name="ScopedStorage"
android:value="true" />
从MediaStore接口中查询到的DATA字段将在Android Q开始废弃,不应该利用它来访问文件或者判断文件是否存在;
使用MediaStore.MediaColumns.RELATIVE_PATH代替data
3、SAF和MediaStore
二、Android 11 - Target 30
Ⅰ - 文件相关
应用targetSdkVersion >= 30,都会强制打开分区存储,requestLegacyExternalStorage将会无效,同时APP将不能直接访问外部存储。
1、暴力解决方案 声明管理所有文件:MANAGE_EXTERNAL_STORAGE;
三、Android 12 - Target 31
1、清单文件
1、android:exported
-
若设置为true,不同项目可通过隐式唤起
-
设置为false时,一个项目内不同module也可隐式唤起
2、必须确定:PendingIntent.FLAG_IMMUTABLE,PendingIntent.FLAG_IMMUTABLE与通知/Service
- 官网可以看到他们的add api情况
- FLAG_IMMUTABLE API level 23-6.0
- FLAG_MUTABLE [API level 31]-12
- 我是直接minApi 24的,直接使用下列方式适配
- 本体声明:PendingIntent.FLAG_IMMUTABL
- 更新使用:PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
- 使用介绍
- 低版本 Android(API 30 及以下) :不会报错或有问题,
PendingIntent.FLAG_IMMUTABLE会被忽略,仍然可以正常工作。
- 高版本 Android(API 31 及以上) :需要明确指定
FLAG_IMMUTABLE或FLAG_MUTABLE,否则可能导致PendingIntent无法正常使用。
四、拓展阅读
1、SAF的使用:使用“存储访问框架”打开文件 | Android 开发者
1、参考文章:轻松适配Android 10 Scoped Storage 分区存储 | HurryYu
2、权限请求
- 清单文件合并说明:tools:node="remove"
- 手撸框架参考
1、新建项目自带权限(2023-7-28)
- 媒体音量控制;
- 读取应用列表;
- 读取剪切板;
- 写入剪切板;
2、常见问题
- 防止被Activity拦截
3、 File类API
- listFiles() 返回当前路径下的一系列文件名
- getName() 返回此抽象路径名表示的文件或目录的名称。
- 通过File对象查看文件路径上是否存在文件,createNewFile来创建新文件
- mkdir()和mkdirs()区别 加s的好用,可建立多级多级目录
- Java File文件操作样例
- 实践: 通过指定存储位置新建File,得到的File文件容量会是0;

七、传统Intent,SAF和MediaStore
1、样例一:铃声选择方案
使用传统的
Intent与RingtoneManager结合进行铃声选择,并没有显式地使用 SAF
1、 在 Android 10 及更高版本,选择铃声文件的操作可能会受到存储权限的限制。尤其是,当用户选择铃声时,文件访问权限可能被动态地授予或被拒绝,特别是对于存储在外部存储(如 SD 卡)上的文件【目前倒还好,比较正常】 2、 缺陷和改进建议
- 使用 SAF 访问铃声文件-特征:通过 Intent.ACTION_CREATE_DOCUMENT 或 Intent.ACTION_OPEN_DOCUMENT 来访问铃声文件。这种方式在 Android 10(API 29)及更高版本中更为推荐,因为它遵循 Scoped Storage 的规则,允许你访问应用所需的文件。
- 如果你需要访问外部存储中的文件,可以考虑使用
ContentResolver与 SAF API 来访问铃声文件 - 使用
MediaStore来访问媒体文件,或者使用ACTION_CREATE_DOCUMENT打开文件选择器。
val pickerIntent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM)
putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true)
putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, currentUri)
putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
val defaultUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)
putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, defaultUri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
}
context.startActivityForResult(pickerIntent, requestCode)
2、样例二:图片选择方案
- 建议获取到后还是移动到自己私有目录的Image文件夹目录下
1、轻量级:ACTION_GET_CONTENT,Android 1.0 就有,基于内容提供器
- 返回的 Uri 通常只能 临时使用(系统可能在后台清理缓存,长期持有不稳定)
- 显示效果融合度也不错,可以点击右侧更多选项进行跳到文件夹页,自己内部的文件夹选项只能展示系统级默认图片文件夹;
private val pickImageLiteLauncher =
registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
uri?.let {
targetUri = it
context?.let {safeContext->
Glide.with(safeContext)
.load(it)
.into(mBindView.ivImage)
}
}
}
pickImageLiteLauncher.launch("image/*")
2、可长期持有:ACTION_OPEN_DOCUMENT,Storage Access Framework (SAF) ,是 Android 4.4 (API 19) 引入的
- 可长期持有,只要你调用
takePersistableUriPermission就能长期访问,即使重启也能用,调用一次后把这个Uri加到 持久授权列表(persisted permissions)里,存储在系统级别。
- 之后即使你 杀死 App / 重启手机 / 重新打开 App,只要这个文件还存在,你就能继续访问
fun openGallery(activity: Activity) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "image/*"
}
activity.startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE)
}
八、其他样例
1、Manager和常规读写
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// 先判断有没有权限
if (!Environment.isExternalStorageManager()) {
AlertDialog.Builder(context)
.setTitle("权限说明")
.setMessage("存储权限:用于保存下载申报表功能")
.setNegativeButton("取消") { dialog, which -> }
.setPositiveButton("授予") { dialog, which ->
val intent =
Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
intent.data = Uri.parse("package:" + AppUtils.getAppPackageName())
startActivityForResult(intent, 1024)
}
.create()
.show()
} else {
judgeCopy()
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!PermissionUtils.isGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.READ_EXTERNAL_STORAGE)) {
PermissionTipDialog.show(childFragmentManager) {
if (it) {
judgeCopy()
}
}
} else {
judgeCopy()
}
} else {
judgeCopy()
}
}