文章目录
- 一、Android存储结构
- 二、什么是分区存储?
- 三、私有目录和公有目录
- 三、存储权限和分区存储有什么关系?
- 四、我们应该该怎么做适配?
- 4.1、利用File进行操作
- 4.2、使用MediaStore操作数据库
一、Android存储结构
Android存储分为内部存储和外部存储
二、什么是分区存储?
看下面的未使用分区存储时的结构图,App私有目录就是上面说的内部存储,共享存储空间就是上面说的外部存储。
分区存储就是在外部存储中的这些文件夹不能随便放了,必须相应的文件类型存到相应的目录中才可以。比如图片文件只能放到Picture目录或者DCIM目录中,就不能放到Movies或者Music中了,否则就会报错崩溃。
这里提一句,Download目录可以放任何类型的文件,这个目录没有类型限制。
Android10以前,外部存储中的所有文件虽然有分类目录,但是不管文件是什么类型都可以随便存放,比如mp3音频文件可以放到Movies目录中或者Picture目录中。
对于Android10,Google第一次添加了分区存储方案,这是作为的一个过渡版本,并且Google在Android10上添加了一个属性让你来选择是否使用分区存储方案 ,就是在Manifest中配置的:
android:requestLegacyExternalStorage="true"
,默认是false,即开启分区存储。
从Android11开始,Google强制使用分区存储,也就是说
requestLegacyExternalStorage
这个属性不再起作用了。
三、私有目录和公有目录
data目录下的可以理解为就是内部存储中的私有目录。
sdcard目录下的就作为公有目录,没有分区存储时,如果要访问里面的文件就需要权限。
如果使用了分区存储,要注意在sdcard目录中,也有私有目录和公有目录的概念。
在sdcard中的data目录下,可以看到应用的包名,这就是外部存储中的私有目录,访问这里面的文件不需要权限,只有访问Android目录外的文件才需要权限。并且卸载应用还会被删除。
三、存储权限和分区存储有什么关系?
存储权限也跟Android版本有关,我们很容易把它和分区存储的概念搞在一起弄的晕头转向,其实并没有什么关系,所以我们讲分区存储不需要考虑要什么什么权限,那是另外一回事。
四、我们应该该怎么做适配?
对于Android10以前(不包含Android10),没有分区存储的概念,并且我们操作文件都是用File对象。
对于Android10,我们有两种选择,即可以使用分区存储,也可以不使用。
对于Android10以后(不包含Android10),强制分区存储了,我们操作文件就需要用MediaStore来操作数据库才行。
4.1、利用File进行操作
这个就不过多进行介绍,随便百度都有很多。
4.2、使用MediaStore操作数据库
- 我们操作的数据库文件其实就存在内部存储的私有目录中:
/data/data/com.android.providers.media
。
我们可以将数据库导出利用数据库工具查看(需要root),里面有个files表,可以看到很多字段,这些字段就对应我们着我们平常代码中所写的:Media.DATA
、Media.DISPLAY_NAME
、Media.DURATION
等等,我们可以根据保存的文件类型以及自己的需求来选择需要的字段。
- 同时可以看到每种类型的文件目录也都有相应的字段,不管是文件夹目录还是文件在数据库中都会有一条数据相对应。
- 使用分区存储后,我们操作文件都需要注意相应的文件类型。
例如MediaStore中的三种类型媒体:音频,视频,图片。
每种类型都分别有三种Uri:内部存储,外部存储,可移动存储(这个不用太关心)。
MediaStore.ImagesMediaStore.VideoMediaStore.Audio
MediaStore.Images.Media.INTERNAL_CONTENT_URI//content//media/internal/image/mediaMediaStore.Images.Media.EXTERNAL_CONTENT_URI//content//media/external/image/mediaMediaStore.Images.Media.getContentUri(volumeName)//content//media/<volumeName>/image/mediaMediaStore.Video.Media.INTERNAL_CONTENT_URI//content//media/internal/video/mediaMediaStore.Video.Media.EXTERNAL_CONTENT_URI//content//media/external/video/mediaMediaStore.Video.Media.getContentUri(volumeName)//content//media/<volumeName>/video/mediaMediaStore.Audio.Media.INTERNAL_CONTENT_URI//content//media/internal/audio/mediaMediaStore.Audio.Media.EXTERNAL_CONTENT_URI//content//media/external/audio/mediaMediaStore.Audio.Media.getContentUri(volumeName)//content//media/<volumeName>/audio/media
当然MediaStore也还有其他类型,目前一共有五个。
- 我们操作数据库文件每次用到的就是一个Uri,比如说我要插入一张图片,执行完下面这个方法,就可以直接在相册中查看到你添加的图片了。
private fun insertImage() {val displayName = "test.jpg"val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URIval values = ContentValues()//根据文件类型和自己的需求选择字段,比如图片这里就必须要指定MIME_TYPEvalues.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, displayName)values.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/jpg")values.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)val imageUri = contentResolver.insert(uri, values)//到这里创建的算是一个文件夹,如果通过下面的代码写入数据后就会变成图片文件if (imageUri != null) {try {val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)val outputStream = contentResolver.openOutputStream(imageUri)if (outputStream != null) {bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)outputStream.close()}ToastUtil.showToast(this, "添加图片成功")} catch (e: Exception) {e.printStackTrace()}} else {ToastUtil.showToast(this, "操作失败")}}
- 简单的查询操作,比如查询上面添加的那张图片。
private fun query(): Uri? {val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI//查询条件,根据DISPLAY_NAMEval selection = MediaStore.Images.Media.DISPLAY_NAME + "=?"val args: Array<String> = arrayOf("test.jpg")val projection: Array<String> = arrayOf(MediaStore.Images.Media._ID)//数据库查询val cursor = contentResolver.query(uri, projection, selection, args, null)return if (cursor != null && cursor.moveToFirst()) {val queryUri = ContentUris.withAppendedId(uri, cursor.getLong(0))cursor.close()ToastUtil.showToast(this, "查询成功:$queryUri")queryUri} else {ToastUtil.showToast(this, "查询失败")null}}
- 简单的删除操作,注意删除操作前需要先通过查询得到相应的uri。
private fun delete() {//先查询后删除val uri = query()if (uri != null) {contentResolver.delete(uri, null, null)}}
- 简单的修改操作,跟删除一样,也需要先通过查询得到相应的uri。
private fun update() {//先查询后修改val uri = query()if (uri != null) {//修改的字段val contentResolver = ContentValues()contentResolver.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, "修改后的图片.jpg")//操作数据库getContentResolver().update(uri, contentResolver, null, null)}}
- 总结一下操作数据库,其实就3步:拿到uri,构建字段条件,执行。拿插入图片来举例。