当前位置:主页 > 移动开发 > Android代码 >

详解Android10的分区存储机制(Scoped Storage)适配教程

时间:2021-01-31 08:11:58 | 栏目:Android代码 | 点击:

1. 简介

大家应该都有过这样的体会,手机用着用着里面就充斥着各种不懂的文件夹和文件。甚至是连已经删除的软件的文件夹还存在。

为什么会发生的这样的问题呢?

因为Google的缺席,导致Android生态野蛮生长,导致很多开发规范没有完全被落实。
为了解决这样的问题,Google决定重拳出击,提出了分区存储(Scoped Storage)机制,也叫沙盒存储机制。
那么什么是沙盒存储机制呢。
沙盒机制是一种安全机制,用于防止应用读取其他应用的数据。

  1. 每个应用程序都有自己的存储空间。
  2. 应用程序不能翻过自己的目录,去访问公共目录。
  3. 应用程序请求的数据都要通过权限检测,不符合要求不会被放行。

2. 关于Android10的分区机制

以 Android 10(API 级别 29)及更高版本为目标平台的应用在默认情况下被赋予了对外部存储设备的分区访问权限(即分区存储), 对外部存储文件访问方式重新设计,便于用户更好的管理外部存储文件。如果不符合条件的会以兼容模式运行,兼容模式跟以前一样,根据路径可以直接存储文件。

应用只能看到本应用专有的目录(通过 Context.getExternalFilesDir() 访问)以及特定类型的媒体。除非您的应用需要访问存放在应用的专有目录以及 MediaStore 之外的文件,否则最好使用分区存储。
在发布Android10的时候官方明确表态:

2020年,主要平台版本将要求所有应用都使用分区存储,无论应用的目标 SDK 级别是多少。因此,您应该提前确保您的应用能够使用分区存储。为此,请确保针对搭载 Android 10(API 级别 29)及更高版本的设备启用了该行为。
翻译成通俗语言,不管是使用requestLegacyExternalStorage=true的方式以兼容模式运行还是降低targetSDK都无法在接下来2020年的Android(API 29)10更新中被豁免。

所以为了应用的稳定性,应该尽在进行适配。

3. 具体分区存储权限的介绍

默认情况下,对于targetSdkVersion大于等于29的应用,其访问权限范围限定为分区存储。此应用无需请求与存储相关的用户权限,即可以查看外部存储中以下类型的文件:

  1. 应用外部特定目录中的文件(使用getExternalFilesDir()访问)。
  2. 应用自己创建的照片、视频和音频(通过MediaStore访问)。

分区存储将影响在Android10系统首次安装启动、且targetSdkVersion >=29的应用。需要访问和共享外部存储文件的应用会受到影响,需要进行兼容性适配。

影响范围:
在Android 10上运行的应用:
1.targetSdkVersion <= 28,不受影响
2.如果targetSdkVersion >= 29,默认情况应用外部存储可见性将被过滤,应用需要对分区存储进行适配。

还有值得注意的是以下两种情况比较特殊,不会受到分区存储的影响:

如果应用最先安装在Android 10以下的系统,
1) 然后系统通过Fota升级到Android 10
2) 应用通过更新升级到targetSdkVersion >= 29

下面是关于分区存储权限和其他相关项目的表格。

类型 位置 访问应用自己生成的文件 访问其他应用生成的的文件 访问方法 卸载应用是否删除文件
外部存储 Photo/ Video/ Audio/ 无需权限 需要权限READ_EXTERNAL_STORAGE MediaStore Api
外部存储 Downloads 无需权限 无需权限 通过存储访问框架SAF,加载系统文件选择器
外部存储 应用特定的目录 无需权限 无法直接访问 getExternalFilesDir()获取到属于应用自己的文件路径

4. 专有目录存储

应用读取或写入应有专有的目录中的文件时,不需要获取存储权限。
在应用中想要获取当前应用的专有存储目录路径是可以用Context.getExternalFilesDir()的方式获取。

val dirpath = context.getExternalFilesDir("")
val fileString = dirpath + File.separator
val file = File(fileString)
...  // 剩下的步骤是用Java IO或者其他IO库来写入数据

5. 共享媒体集合存储

在共享媒体集合存储中保存媒体文件时,需要根据文件的类型选择MediaStore。

把相关数据放入到ContentValues中,最后把ContentValues插入到ContentResolver中,并获得返回的Uri。

通过Uri过得OutputStream,然后用Okio的IO库,进行文件的存储。

关于Okio的只是以后有机会的话,我们再好好讲一讲。

不要忘了这里需要获取权限。

// 把图片下载到共有媒体集合中,并在相册中显示
// 创建ContentValues, 并加入信息
val values = ContentValues()
values.put(MediaStore.Images.Media.DESCRIPTION, downloadedFile.name)
values.put(MediaStore.Images.Media.DISPLAY_NAME, downloadedFile.name)
values.put(MediaStore.Images.Media.MIME_TYPE, mimeType)
values.put(MediaStore.Images.Media.TITLE, downloadedFile.name)
values.put(
  MediaStore.Images.Media.RELATIVE_PATH,
  "${Environment.DIRECTORY_PICTURES}/${downloadedFile.name}"
)
// 插入到ContentResolver,并返回Uri
val insertUri = context.contentResolver.insert(
  MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
  values
)

if (insertUri != null) {
  // 获取OutputStream
  val outputStream = context.contentResolver.openOutputStream(insertUri)
if (outputStream != null) {
  sink = outputStream.sink().buffer()
} else {
  return@runCatching FileDownloadResult.OthersError
  }
} else {
  return@runCatching FileDownloadResult.OthersError
}

 val responseBody = response.body ?: return@runCatching FileDownloadResult.OthersError

try {
  val contentLength = responseBody.contentLength()
  if (contentLength > FileUtil.getAvailableSize(dirPath)) {
    continuation.resume(FileDownloadResult.StorageError)
  }
  var totalRead: Long = 0
  var lastRead: Long

  do {
    lastRead = responseBody.source().read(sink.buffer(), BUFFER_SIZE)
    if (lastRead == -1L) {
      break
    }
    totalRead += lastRead
    sink.emitCompleteSegments()
  } while (true)
  sink.writeAll(responseBody.source())
  sink.close()
  responseBody.close()
}

6. 其他

Github: https://github.com/HyejeanMOON/ScopedStorageDemo

您可能感兴趣的文章:

相关文章