你们应该都有过这样的体会,手机用着用着里面就充斥着各类不懂的文件夹和文件。甚至是连已经删除的软件的文件夹还存在。
为何会发生的这样的问题呢?
由于Google的缺席,致使Android生态野蛮生长,致使不少开发规范没有彻底被落实。
为了解决这样的问题,Google决定重拳出击,提出了分区存储(Scoped Storage)机制,也叫沙盒存储机制。git
那么什么是沙盒存储机制呢。 沙盒机制是一种安全机制,用于防止应用读取其余应用的数据。github
每一个应用程序都有本身的存储空间。
应用程序不能翻过本身的目录,去访问公共目录。
应用程序请求的数据都要经过权限检测,不符合要求不会被放行。
以 Android 10(API 级别 29)及更高版本为目标平台的应用在默认状况下被赋予了对外部存储设备的分区访问权限(即分区存储), 对外部存储文件访问方式从新设计,便于用户更好的管理外部存储文件。若是不符合条件的会以兼容模式运行,兼容模式跟之前同样,根据路径能够直接存储文件。安全
应用只能看到本应用专有的目录(经过 Context.getExternalFilesDir() 访问)以及特定类型的媒体。除非您的应用须要访问存放在应用的专有目录以及 MediaStore 以外的文件,不然最好使用分区存储。框架
在发布Android10的时候官方明确表态: 2020年,主要平台版本将要求全部应用都使用分区存储,不管应用的目标 SDK 级别是多少。所以,您应该提早确保您的应用可以使用分区存储。为此,请确保针对搭载 Android 10(API 级别 29)及更高版本的设备启用了该行为。
翻译成通俗语言,不论是使用requestLegacyExternalStorage=true
的方式以兼容模式运行仍是下降targetSDK
都没法在接下来2020年的Android(API 29)10更新中被豁免。ide
因此为了应用的稳定性,应该尽在进行适配。post
默认状况下,对于targetSdkVersion大于等于29的应用,其访问权限范围限定为分区存储。此应用无需请求与存储相关的用户权限,便可以查看外部存储中如下类型的文件:测试
getExternalFilesDir()
访问)。MediaStore
访问)。分区存储将影响在Android10
系统首次安装启动、且targetSdkVersion >=29
的应用。须要访问和共享外部存储文件的应用会受到影响,须要进行兼容性适配。动画
影响范围: 在Android 10上运行的应用:
1.targetSdkVersion <= 28,不受影响
2.若是targetSdkVersion >= 29,默认状况应用外部存储可见性将被过滤,应用须要对分区存储进行适配。ui
还有值得注意的是如下两种状况比较特殊,不会受到分区存储的影响:spa
若是应用最早安装在Android 10如下的系统,
1) 而后系统经过Fota升级到Android 10
2) 应用经过更新升级到targetSdkVersion >= 29
下面是关于分区存储权限和其余相关项目的表格。
类型 | 位置 | 访问应用本身生成的文件 | 访问其余应用生成的的文件 | 访问方法 | 卸载应用是否删除文件 |
---|---|---|---|---|---|
外部存储 | Photo/ Video/ Audio/ | 无需权限 | 须要权限READ_EXTERNAL_STORAGE | MediaStore Api | 否 |
外部存储 | Downloads | 无需权限 | 无需权限 | 经过存储访问框架SAF,加载系统文件选择器 | 否 |
外部存储 | 应用特定的目录 | 无需权限 | 没法直接访问 | getExternalFilesDir()获取到属于应用本身的文件路径 | 是 |
应用读取或写入应有专有的目录中的文件时,不须要获取存储权限。
在应用中想要获取当前应用的专有存储目录路径是能够用Context.getExternalFilesDir()
的方式获取。
val dirpath = context.getExternalFilesDir("")
val fileString = dirpath + File.separator
val file = File(fileString)
... // 剩下的步骤是用Java IO或者其余IO库来写入数据
复制代码
在共享媒体集合存储中保存媒体文件时,须要根据文件的类型选择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()
}
复制代码
Github: github.com/HyejeanMOON…
其余教程:
Android Jetpack Room的详细教程: juejin.im/post/5ebac9…
Android的属性动画(Property Animation)详细教程: juejin.im/post/5eb7a5…
Android ConstraintLayout的易懂教程: juejin.im/post/5ea50a…
在RecyclerView中能够应对多个ViewType的库--Groupie: juejin.im/post/5e9059…
Google的MergeAdapter的使用: juejin.im/post/5e903f…
Paging在Android中的应用: juejin.im/post/5e75db…
Android UI测试之Espresso: juejin.im/post/5e6caa…
Android WorkManager的使用: juejin.im/post/5e635e…