Bootstrap

Android分区存储

1、分区存储概述

分区存储是Android 10开始引进的Android系统存储管理机制,它允许App读取和写入App自身创建的文件而不需要任何存储权限。其中根据存储位置的不同,可以分为内部内部存储和外部存储。内部存储就不用多说了,而外部存储又分为私有空间和公共空间。私有存储空间位置是/sdcard/Android/data/包名,而公共空间则是相册、下载等。对我们开发者影响最大的就是对于公共存储空间的读写了,总结如下:

  1. 对于9.0及以下的版本,仍然使用READ和WRITE权限,之前怎么做,现在还是怎么做
  2. 对于10.0,可以在清单文件中加入以下代码变得跟9.0以前一样
    <application android:requestLegacyExternalStorage="true" ...>
    ...
    </application>
    
    但是不建议,因为这种方式在Android 11已经不行了,反正都要适配
  3. 对于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)

出现的界面长这样
在这里插入图片描述

;