1、分区存储概述
分区存储是Android 10开始引进的Android系统存储管理机制,它允许App读取和写入App自身创建的文件而不需要任何存储权限。其中根据存储位置的不同,可以分为内部内部存储和外部存储。内部存储就不用多说了,而外部存储又分为私有空间和公共空间。私有存储空间位置是/sdcard/Android/data/包名,而公共空间则是相册、下载等。对我们开发者影响最大的就是对于公共存储空间的读写了,总结如下:
- 对于9.0及以下的版本,仍然使用READ和WRITE权限,之前怎么做,现在还是怎么做
- 对于10.0,可以在清单文件中加入以下代码变得跟9.0以前一样
但是不建议,因为这种方式在Android 11已经不行了,反正都要适配<application android:requestLegacyExternalStorage="true" ...> ... </application>
- 对于Android 10和11,读写App自己创建的文件不需要任何存储权限,读取其他应用创建的文件需要READ权限,但是WRITE权限被废弃了,写入其他应用创建的文件需要用户的干预。
Android分区存储机制其实挺好的,让很多软件不能为所欲为,至少提高了“犯罪成本”。就是来得晚了一些,导致我们开发者要各种兼容…
2、读取
以读取相册(DCIM)为例
val uris = ArrayList<Uri>()
contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null,
null, "${MediaStore.MediaColumns.DATE_ADDED} desc")?.use {
// 这里的it是一个Cursor
while (it.moveToNext()) {
val id = it.getLong(it.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
uris.add(uri)
}
}
这个contextResolver就是是Context.getContentResolver(),Actvity是Context子类,所以用kotlin就可以直接简写。
这样就拿到了相册里面的图片的Uri,它是“content://”形式的uri:
content://media/external/images/media/71
这种读取形式在低版本也是可用的,但是需要READ权限。在Android 10和11中,如果有READ权限,则可以读取到所有的图片文件的Uri,否则只能读取到App本身创建的文件Uri。
在拿到Uri后,我们可以通过流的形式读取它:
contentResolver.openInputStream(uri)?.use {
// do sth
}
如果是用于展示图片,可以使用Glide等开源框架,它们本身就支持加载Uri。
3、写入
同样以写入相册(DCIM)为例:
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.girl)
val values = ContentValues()
values.put(MediaStore.MediaColumns.DISPLAY_NAME, "test.png")
values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // RELATIVE_PATH需要API 29
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
} else {
values.put(MediaStore.MediaColumns.DATA, "${Environment.getExternalStorageDirectory().path}/${Environment.DIRECTORY_DCIM}/test.png")
}
contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)?.also { uri ->
contentResolver.openOutputStream(uri)?.use { os ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, os)
}
}
同样地,在低版本写入需要WRITE权限,在10和11上不需要。但是如果这个要修改其他App创建的文件,就需要写成这样
private var uri: Uri? = null // 通过某些操作获取这个uri并赋值
private fun change() {
val temp = uri ?: return
contentResolver.openInputStream(temp)?.use {
val bitmap = BitmapFactory.decodeStream(it)
val values = ContentValues()
values.put(MediaStore.MediaColumns.DISPLAY_NAME, "test.png")
values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // RELATIVE_PATH需要API 29
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
} else {
values.put(MediaStore.MediaColumns.DATA, "${Environment.getExternalStorageDirectory().path}/${Environment.DIRECTORY_DCIM}/test.png")
}
try {
contentResolver.openOutputStream(temp)?.use { os ->
bitmap.compress(Bitmap.CompressFormat.PNG, 80, os)
Toast.makeText(this@MainActivity, "OK", Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && e is RecoverableSecurityException) {
startIntentSenderForResult(e.userAction.actionIntent.intentSender, 10086, null, 0, 0, 0)
} else {
throw e
}
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(requestCode == 10086 && resultCode == RESULT_OK) {
change()
}
}
会弹出这样的弹窗
如果是对多个文件进行写入,在Android 11上可以这样写:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val request = MediaStore.createWriteRequest(contentResolver, listOf(uri1, uri2))
startIntentSenderForResult(request.intentSender, 10086, null, 0, 0, 0)
}
值得一提的是,用户如果卸载了App后再重新安装,即使是卸载前App自身创建的文件也需要相关权限。也就是说卸载重装之后,“同一个App”其实在系统眼里不是同一个App。
3、管理存储的权限
分区存储机制很好地规范了Android App的存储行为,让它们读自己该读的,写自己该写的。但是有的应用天生就需要对SD卡进行全方位的访问,比如各种文件浏览器、垃圾清理软件等等,虽然很多所谓的垃圾清理软件本身就是最该被清理的垃圾…对此,Android 11引入了一个新的权限:
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
有了这个权限,就可以跟以前的版本一样随意玩耍了。那么是不是可以直接申请这个权限就可以了呢?机智如我,是可以的,不过应用市场不让上架…所以大部分App是不允许使用这个权限的。如果要申请此权限,需要打开设置界面,让用户手动设置
val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
startActivityForResult(intent, 10010)
如果在manifest中添加了requestLegacyExternalStorage属性,还可以加上包名
val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
intent.data = Uri.parse("package:$packageName")
startActivityForResult(intent, 10010)
出现的界面长这样