Android存储目录
内部存储
getFilesDir - 应用内部存储 放在data/data/packagename/files/
getCacheDir - 应用内部存储 放在data/data/packagename/cache/
外部存储
getExternalFilesDir - 放在外部存储mnt/sdcard/Android/data/packagename/files/ 外部存储私有目录
应用卸载就会删除
5.0及以上不需要
WRITE_EXTERNAL_STORAGE
READ_EXTERNAL_STORAGE不安全,别的应用可以写入数据到此目录
Media扫描不出来,不会出现在相册
getExternalCacheDir - 存放临时缓存数据 放在外部存储mnt/sdcard/Android/data/packagename/cache/
对应设置选项设置->应用->应用详情里面的"清除数据"与"清除缓存“选项"
getExternalStorageDirectory - 在sd卡目录mnt/sdcard,外部存储共享目录 外部存储中,除了私有目录以外的目录,都是共享目录。程序保存在共享目录中的数据,在应用被删除后,仍然保留。
Scoped storage in Android 10
在Android 10以前,只要程序获得了READ_EXTERNAL_STORAGE
权限,就可以随意读取外部存储的共享目录;只要程序获得了WRITE_EXTERNAL_STORAGE
权限,就可以随意在外部存储的共享目录上新建文件夹或文件。
于是Google终于开始动手了,在Android 10中提出了分区存储,意在限制程序对外部存储中共享目录的为所欲为。分区存储对 内部存储目录 和 外部存储私有目录 都没有影响。
简而言之,在Android 10中,对于私有目录的读写没有变化,仍然可以使用File那一套,且不需要任何权限。而对于共享目录的读写,则不能按照原来操作方法。
Android 11对共享目录的访问
共享目录文件需要通过MediaStore API或者Storage Access Framework方式访问。
MediaStore API在共享目录指定目录下创建文件或者访问文件自己创建文件,不需要申请存储权限
MediaStore API访问其他应用在共享目录创建的媒体文件(图片、音频、视频), 需要申请存储权限,未申请存储权限,通过ContentResolver查询不到文件Uri,即使通过其他方式获取到文件Uri,读取或创建文件会抛出异常
MediaStore API 目录对应系统文件的文件夹
MediaStore.Images --> DCIM/ 和 Pictures
MediaStore.Video --> DCIM/, Movies/, 和 Pictures/
MediaStore.Audio --> Alarms/, Audiobooks/, Music/, Notifications/, Podcasts/ 和 Ringtones/
MediaStore.Downloads --> android 10新加目录,Download/
MediaStore.Files --> android 10之后可用。如果使用分区存储,只包含当前应用的图片,视频,音频,如果不使用分区存储,则包含所有其它媒体类型
MediaStore API不能够访问其他应用创建的非媒体文件(pdf、office、doc、txt等), 只能够通过Storage Access Framework方式访问
Storage Access Framework 存储访问框架方式访问
Android 4.4 就引入了存储访问框架 (SAF)。借助 SAF,用户可轻松在其所有首选文档存储提供程序中浏览并打开文档、图像及其他文件。用户可通过易用的标准界面,以统一方式在所有应用和提供程序中浏览文件,以及访问最近使用的文件。
文档和其它文件写入外部共享存储
不需要申请写权限,会弹出系统文件页面,用户手动创建文件
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {type = "text/plain"addCategory(Intent.CATEGORY_OPENABLE)intent.putExtra(Intent.EXTRA_TITLE, )}startActivityForResult(intent, REQUEST_CREATE_DOCUMENT)
Android 11 Scoped storage变更
对于启用了Scoped storage的应用,如果有需要授权,对话框发生变化,提示应用正在请求访问照片和媒体,以前包含文件
[图片上传失败...(image-7b0557-1592559787255)]在android 11上,
WRITE_EXTERNAL_STORAGE
和WRITE_MEDIA_STORAGE
将不能提供相应的访问权限在 Android 11 上,应用无法再访问外部存储设备中的任何其他应用的私有目录的文件
实例演练
写入外部公有存储,在Android 11上向外部存储创建一个文件,通过传统file path形式创建, 先授权,不能创建成功。授权已经不起作用
val file = File(Environment.getExternalStorageDirectory(), packageName)if (!file.exists()) {Log.d("ScopedStorageActivity", "create external file state:${file.mkdirs()}")}create external file state:false读取外部共享存储,android 11通过传统方式读取外部存储
val file = File(Environment.getExternalStorageDirectory(), "test.txt")Log.d("ScopedStorageActivity", "test read external file content:${file.readText()}")Caused by: java.io.FileNotFoundException: /storage/emulated/0/test.txt: open failed: EACCES (Permission denied)
不管授权与否都会报错,不能获取文件。可以采采用存储访问框架来读取, 这样需要用户打开文件,来选择 https://developer.android.com/guide/topics/providers/document-provider?hl=zh-cn
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {type = "application/*"// 我们需要使用ContentResolver.openFileDescriptor读取数据addCategory(Intent.CATEGORY_OPENABLE)}startActivityForResult(intent, REQUEST_OPEN_DOCUMENT)override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {super.onActivityResult(requestCode, resultCode, data)when (requestCode) {REQUEST_OPEN_DOCUMENT -> {if (resultCode == Activity.RESULT_OK) {data?.data.also { documentUri ->Log.d("ScopedStorageActivity", "fileDescriptor documentUri:$documentUri")}}}}}
在onActivityResult回调中就能获取到文件的uri,然后对其进行相应的操作
通过MediaStore API 向共享存储中写入媒体文件,不需要申请权限
MediaStore API ContentResolver APIval bitmap = BitmapFactory.decodeResource(resources, R.mipmap.timg)val displayName = "${System.currentTimeMillis()}.png"val mimeType = "image/png"val compressFormat = Bitmap.CompressFormat.PNGval contentValues = ContentValues()contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)val path = getAppPicturePath()if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, path)} else {val fileDir = File(path)if (!fileDir.exists()) {fileDir.mkdir()}contentValues.put(MediaStore.MediaColumns.DATA, path)}val uri =contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)uri?.also {val outputStream = contentResolver.openOutputStream(it)outputStream?.also { os ->bitmap.compress(compressFormat, 100, os)os.close()Toast.makeText(this, "添加图片成功", Toast.LENGTH_SHORT).show()}}通过MediaStore API 向共享存储中读取媒体文件,不需要申请权限
private fun readExternalFileByMediaStore() {var pathKey = ""var pathValue = ""pathKey = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {MediaStore.MediaColumns.DATA} else {MediaStore.MediaColumns.RELATIVE_PATH}// RELATIVE_PATH会在路径的最后自动添加/pathValue = getAppPicturePath()val cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,null,if (pathKey.isEmpty()) {null} else {"$pathKey LIKE ?"},if (pathValue.isEmpty()) {null} else {arrayOf("%$pathValue%")},"${MediaStore.MediaColumns.DATE_ADDED} desc")cursor?.also {while (it.moveToNext()) {val id = it.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))val displayName = it.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME))val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)Log.d("ScopedStorageActivity", "read external uri:$uri, name:$displayName")Toast.makeText(this, "$displayName", Toast.LENGTH_LONG).show()}}cursor?.close()}
然后通过uri可以得到Bitmap
val openFileDescriptor = contentResolver.openFileDescriptor(uri, "r")openFileDescriptor?.apply {val bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor)readImg.setImageBitmap(bitmap)}openFileDescriptor?.close()
如果要读取其它应用的媒体文件,扫出应用的相册,就需要申请
READ_EXTERNAL_STORAGE
权限,然后通过MediaStorage API读取
Scoped storage兼容
对于通过filepath读取比较重的应用,requestLegacyExternalStorage = true 继续保留,在android 10,维持原状
android 11之后requestLegacyExternalStorage失效,必须适配
如果当前应用以兼容模式运行,覆盖安装后应用仍然会以兼容模式运行,卸载重新安装应用才会以分区存储模式运行
文件迁移是将应用共享目录文件迁移到应用私有目录或者Android10要求的media集合目录
android 11可以用传统File path直接操作方式读媒体文件,性能会有影响,底层还是要转化成ContentResolver.openFileDescriptor读取, 会比较耗时,官方建议用MediaStore
val file = File(Environment.getExternalStorageDirectory().absolutePath+ File.separator + Environment.DIRECTORY_PICTURES + File.separator + APP_FOLDER_NAME,"rebase.png")Log.d("ScopedStorageActivity", "${file.path}")showImage(Uri.fromFile(file))
官方文档
应用数据和文件
Android 11 中的存储机制更新




