安卓系统会在每次开机之后扫描所有文件并分类整理存入数据库
,这个数据库保存了手机上存储的所有文件的信息。该数据库文件存放在Android设备的/data/data/com.android.providers.media/databases
或/data/data/com.android.providers.media.module/databases
目录当中,该目录下有两个数据库文件分别是internal.db(内部存储数据库文件)和external.db(外部存储数据库文件), 这两个数据库文件中的数据表和表结构都大体相似,区别在于internal.db是用来存放内部存储中的文件信息的,而external.db是用来存储外部存储中的文件信息的。因此可以通过访问这两个数据库获取例如媒体文件(音频、视频、图片)等的文件信息, 而不必通过遍历媒体文件的方式来获取文件信息。但是在android设备中是禁止应用程序直接对这个数据进行直接操作的,而是将这个数据库的操作通过ContentProvider
(内容提供者) 将数据操作提供出来, 如要对ContentProvider
中的数据进行操作,可以通过ContentResolver
(数据调用者) 对象结合Uri
进行调用 来实现 。 ContentResolver
(数据调用者)可以实现与ContentProvider
进行通信,通过ContentResolver
调用ContentProvider
的添加(insert
)、删除(delete
)、查询(query
)、修改(update
)等操作的同名方法,从而让ContentProvider
对象接收数据请求、执行请求的操作并返回结果,这是一套标准的Android内容提供者数据模型。ContentProvider
将其存储的数据以数据表的形式提供给访问者,在数据表中每一行为一条记录,每一列为具有特定类型和意义的数据。每一条数据记录都包括一个 “_ID
” 数值字段,改字段唯一标识一条数据。每一个Content Provider
都对外提供一个能够唯一标识自己数据集(data set)的公开URI
, 如果一个Content Provider
管理多个数据集,其将会为每个数据集分配一个独立的URI
。所有的Content Provider
的URI
都以"content://
"开头,其中"content:
"是用来标识数据是由Content Provider
管理的 schema
。Android 系统为一些常见的数据类型(如音乐、视频、图像、手机通信录联系人信息等)内置了一系列的 Content Provider
,这些都位于android.provider
包下。持有特定的许可,可以在自己开发的应用程序中访问这些Content Provider
。
ContentProvider
这个机制,提供安全的数据访问操作方式。ContentProvider
还有一个重要的特点就是它是可以使得某些数据可以被跨进程访问,会自动处理安全性和跨进程通信。ContentProvider
暴露自己的数据,可以使得开发者在开发时无需知道数据是如何存储的,是使用数据库还是使用文件?开发者只需要通过这一套标准及统一的接口和数据打交道。获取ContentResolver
实例:
val mResolver = context.getContentResolver()
ContentResolver
实例获得后,就可以进行各种增删改查的方法 ,ContentResolver
类也提供了与ContentProvider
类相对应的四个方法可供调用:
返回值 | 函数声明 | 说明 |
---|---|---|
final Uri | insert(Uri url, ContentValues values) | 该方法用于往ContentProvider添加数据。 |
final int | delete(Uri url, String where, String[] selectionArgs) | 该方法用于从ContentProvider删除数据。 |
final Cursor | query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) | 该方法用于从ContentProvider中获取数据。 |
final int | update(Uri uri, ContentValues values, String where, String[] selectionArgs) | 该方法用于更新ContentProvider中的数据。 |
ContentResolver
是通过Uri
来查询ContentProvider
中提供的数据的。因此想操作ContentProvider
,必须要知道内容提供者的Uri
,在正确得到Uri
之后,就可以通过ContentResolver
对象来操作ContentProvider
中的数据了。
上文中提到了Android提供内容的叫ContentProvider
,那么在Android中怎么区分各个Provider
?有的是提供联系人的,有的是提供图片的,有的是提供视频的等等。所以就需要有一个唯一的标识来标识这个Provider
,Uri
(通用资源标识符 Universal Resource Identifier
)就是起到了这个标识的作用。每一个ContentProvider
都会有一个唯一的Uri
地址,通过这个Uri
标识可以获取到ContentProvider
和其中的数据,然后进行数据操作。
ContentProvider
使用的Uri
语法结构如下:
content://media/external/images/media
content://
是通用前缀,表示该Uri
用于ContentProvider
定位资源。authority
是授权者名称,用来确定具体由哪一个ContentProvider
提供资源。因此一般authority
都由类的小写全称组成,以保证唯一性。data_path
是数据路径,用来确定请求的是哪个数据集。id
是数据编号,用来请求单条数据。如果是多条这个字段忽略。对于系统已经提供了如通讯录、多媒体、短信等的URI
,可以直接用ContentResolver
调用这些URI
,对系统数据库进行增删改查等操作,从而保证整个Android设备中数据的统一。
以content://media/external/images/media
为例,其URI
有三种写法:
Uri uri1 = Uri.parse("content://media/external/images/media");
Uri uri2 = MediaStore.Images.Media.getContentUri("external");
Uri uri3 = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Android 提供了 MediaStore
类来对数据库的的多媒体数据进行封装。
我们知道Android系统中的每一种媒体文件有两种地址描述方式:
ContentProvider
是用来存储和获取公共数据的统一接口,Content Provider
为每一类资源分配了URI地址比如图片的Uri地址就包括MediaStore.Images.Media.INTERNAL_CONTENT_URI
和MediaStore.Images.Media.EXTERNAL_CONTENT_URI
这两个地址,其值分别是content://media/internal/images/media
和content://media/external/images/media
,对应内部库和外部库地址。每一张图片的地址基本上是上面的基础URL地址下加上图片的内部ID
。例如外部存储上的图片ID
为52,其对应的Uri地址就是 content://media/external/images/media/52
. 知道了这个地址,基本上就可以操作这张图片的所有信息了。同样,对于其他多媒体文件例如外部库的音频 Uri
地址为 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
,其它 Uri
地址都是类似的。/mnt/sdcard/images/52.jpg
。其实这个路径存储在文件储存库中的data
字段中,有了这点关联,可以在这两种模式下进行任意切换。通过上文可以知道Android为多媒体提供的ContentProvider
的Uri
都记录在MediaStore
这个类里。MediaStore
获取文件信息,是通过文件的mime-type
来获取的,也就说文件储存库中有mime-type
这个字段。
MediaStore内部类
class MediaStore.Audio
:所有音频内容的类。class MediaStore.Files
:文件储存库中所有文件的索引,包括非媒体文件和媒体文件类。class MediaStore.Images
:所有图片内容的类。interface MediaStore.MediaColumns
:文件储存库中表的公共字段。class MediaStore.Video
:所有视频内容的类。MediaStore内部类对外提供的多媒体Uri常量有如下
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
:存储在手机外部存储器上的音频文件Uri
路径。MediaStore.Audio.Media.INTERNAL_CONTENT_URI
:存储在手机内部存储器上的音频文件Uri
路径。MediaStore.Images.Media.EXTERNAL_CONTENT_URI
:存储在手机外部存储器上的图片文件Uri
路径。MediaStore.Images.Media.INTERNAL_CONTENT_URI
:存储在手机内部存储器上的图片文件Uri
路径。MediaStore.Video.Media.EXTERNAL_CONTENT_URI
:存储在手机外部存储器上的视频文件Uri
路径。MediaStore.Video.Media.INTERNAL_CONTENT_URI
:存储在手机内部存储器上的视频文件Uri
路径。注意:MediaStore.Downloads.EXTERNAL_CONTENT_URI是Android10版本新增API,用于创建、访问非媒体文件。
Android系统给我们定义好了许多的媒体文件对应的URI路径如下表:
媒体类型 | Uri路径 | MediaStore内部类常量 | 默认存储目录 | 允许存储目录 |
---|---|---|---|---|
Image(图片) | content://media/external/images/media | MediaStore.Images.Media.EXTERNAL_CONTENT_URI | Pictures | DCIM、Pictures |
Audio(音频) | content://media/external/audio/media | MediaStore.Audio.Media.EXTERNAL_CONTENT_URI | Music | Alarms、Music、Notifications、Podcasts、Ringtones |
Video(视频) | content://media/external/video/media | MediaStore.Video.Media.EXTERNAL_CONTENT_URI | Movies | DCIM 、Movies |
Download(下载文件) | content://media/external/downloads | MediaStore.Downloads.EXTERNAL_CONTENT_URI | Download | Download |
有了对应的Uri
地址,我们可以通过ContentResolver
调用添加(insert
)、删除(delete
)、查询(query
)、 修改(update
)等操作了。
有了 Uri
地址,我们可以通过ContentResolver
的 query()
方法来获取到 Cursor
,从而获取到数据库资源。
public final Cursor query (Uri uri, String[] projection,String selection,String[] selectionArgs, String sortOrder)
ContentResolver
的query
方法接受几个参数,参数意义如下:
Uri
:这个Uri
代表要查询的内容提供者的Uri
。上文说到多媒体类型的Uri一般都直接从MediaStore
里取得,例如我要取所有图片的信息,就必须利用MediaStore.Images.Media.EXTERNAL_CONTENT_URI
这个Uri
。projection
: 代表告诉Provider
要返回的字段内容(列Column
),用一个String
数组来表示。用null
表示返回Provider
的所有字段内容(列Column
)。selection
:相当于SQL语句中的where
子句,就是代表查询条件。null
表示不进行添加筛选查询。selectArgs
:如果selection
里有?这个符号时,这里可以以实际值代替这个问号。如果Selections
这个没有?的话,那么这个String
数组可以为null
。sortOrder
:说明查询结果按什么来排序。相当于SQL语句中的Order by
,升序 asc
/降序 desc
,null
为默认排序。示例:
//查找所有图片的信息
Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
null,
null,
null,
null);
//查找所有图片的信息的DISPLAY_NAME字段
Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Images.Media.DISPLAY_NAME},
null,
null,
null);
//查找图片名字叫“xx.png”的图片信息,null表示不进行筛选。
Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Images.Media.DISPLAY_NAME},
//第三个参数设置条件,相当于SQL语句中的where。
//null表示不进行筛选。
MediaStore.Images.Media.DISPLAY_NAME + "='xx.png'",
null,
null);
//查找图片名字叫“xx.png”的图片信息,如果在selection参数里面有?,那么你在selectionArgs写的数据就会替换掉?,
Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Images.Media.DISPLAY_NAME},
MediaStore.Images.Media.DISPLAY_NAME + "=?",
new String[]{"xx.png"},
null);
//默认排序是升序。注意:desc前有空格
Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
null,
null,
null,
ContactsContract.Contacts._ID + " DESC");
上面就是各个参数的意义,它返回的查询结果一个Cursor
结果集。那么如何使用Cursor
对象获取数据?
Cursor
就是游标,把查询到的结果集封装在一个Cursor
对象当中。我们知道只要是数据表都是可以通过行和列定位到具体的位置然后数据将其取出,Cursor
也不例外,可以通过cursor.moveToNext()
让行向后移动,然后根据getColumnIndex(String columnName)
获取到列名的索引位置。有了每一行的的列名索引位置就可以就可以取出每一行中对应的数据了。当然这些数据类型也是多种多样的,Cursor
也给我们提供了以下的方法去获取不同类型的值:
byte[] getBlob(int columnIndex)
String getString(int columnIndex)
int getInt(int columnIndex)
long
形式返回请求列的值:long getLong(int columnIndex)
float getFloat(int columnIndex)
double getDouble(int columnIndex)
int getType(int columnIndex)
boolean isNull(int columnIndex)
short getShort(int columnIndex)
示例:使用ContentResolver
通过Uri
获取图片文件相关的信息集Cursor
,然后通过Cursor
取出图片文件相关的信息:
// 先拿到图片提供者的Uri
val imageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
// 需要获取图片数据表中的哪几列信息,注意这里需要和cursor取出的列名相对应否则会出现空指针
val projection = arrayOf(
//获取ID列的数据
MediaStore.Images.Media._ID,
//获取MIME_TYPE列的值
MediaStore.Images.Media.MIME_TYPE,
//获取DISPLAY_NAME列的值
MediaStore.Images.Media.DISPLAY_NAME
)
// 查询条件:因为是查询全部图片,传null
//String selection = MediaStore.Images.Media.DISPLAY_NAME +"= ?";
// 条件参数:因为是查询全部图片,传null
//String[] args = new String[] {“xxx.png”}
// 排序:可以添加排序
// val order = MediaStore.Files.FileColumns._ID + "DESC"
// 开始查询
val cursor = contentResolver.query(imageUri, projection, null, null, null)
//获取我们需要的数据在数据的第几列,这里需要和projection查询的数据对应起来,因为cursor结果集只包含projection的列信息,否则会出现空指针
if (cursor != null) {
// 获取id字段是第几列
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
val mimeTypeIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.MIME_TYPE)
val displayNameIndex = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
//循环取出cursor每一行的数据
while (cursor.moveToNext()) {
//根据列的坐标,取出对应行数的数据
val id = cursor.getLong(idIndex)
//根据列的坐标,取出对应行数的数据
val type = cursor.getString(mimeTypeIndex)
//根据列的坐标,取出对应行数的数据
val disName = cursor.getString(displayNameIndex)
//根据ID和图片提供者的Uri可以合成图片的Uri
val imageUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
//TODO
}
//关闭游标
cursor.close()
}
通过上面的例子,可以获取到手机外部存储中所有的图片文件信息的ID
、MIME_TYPE、DISPLAY_NAME
等信息,如果要获取到文件大小等其他的信息也是可以的,只需要在projection
集合中添加对应的列名然后,在cursor
中找到每一行对应的列的索引值就可以取出对应的值,这些列名可以在MediaStore.MediaColumns
取公共常量字段,也可以根据文件类型的不同在MediaStore
的内部类中取值:
图片文件比较常见的列名有:
MediaStore.Images.Media._ID
:磁盘上文件的路径MediaStore.Images.Media.DATA
:磁盘上文件的路径MediaStore.Images.Media.DATE_ADDED
:文件添加到media provider
的时间(单位秒)MediaStore.Images.Media.DATE_MODIFIED
:文件最后一次修改单元的时间MediaStore.Images.Media.DISPLAY_NAME
:文件的显示名称MediaStore.Images.Media.HEIGHT
:图像/视频的高度,以像素为单位MediaStore.Images.Media.MIME_TYPE
:文件的MIME类型MediaStore.Images.Media.SIZE
:文件的字节大小MediaStore.Images.Media.TITLE
:标题MediaStore.Images.Media.WIDTH
:图像/视频的宽度,以像素为单位。视频文件比较常见的列名有:
MediaStore.Video.Media.TITLE
: 名称MediaStore.Video.Media.DURATION
: 总时长MediaStore.Video.Media.DATA
: 地址MediaStore.Video.Media.SIZE
: 大小MediaStore.Video.Media.WIDTH
:视频的宽度,以像素为单位。MediaStore.Video.Media.HEIGHT
:视频的高度,以像素为单位音频文件比较常见的列名有:
MediaStore.Audio.Media.TITLE
:歌名MediaStore.Audio.Media.ARTIST
:歌手MediaStore.Audio.Media.DURATION
:总时长MediaStore.Audio.Media.DATA
:地址MediaStore.Audio.Media.SIZE
:大小如果需要插入数据只需要调用contentResolver.insert(uri, contentValues)
;构造一个 ContentValues
对象,通过 ContentResolver.insert
插入到对应的目录中,该方法会返回一个 Uri
,通过对该 Uri
进行文件流写入即可:
private fun saveImage(bitmap: Bitmap){
val values = ContentValues()
val insertUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,values)
insertUri?.let {
contentResolver.openOutputStream(it).use {outputStream->
bitmap.compress(Bitmap.CompressFormat.PNG,100,outputStream)
}
}
}
ContentValues
类和Bundle
类很类似,都是使用HashMap的泛型形式来存储的,并且都是HashMap
。通过ContentValues
可以设置列的数据,和上文中提到的一样可以在MediaStore.MediaColumns
中
取公共常量字段也可以在MediaStore.MediaColumns
取公共常量字段:
private fun saveImage(bitmap: Bitmap){
val values = ContentValues().apply {
//设置文件的 MimeType
put(MediaStore.Images.Media.MIME_TYPE,"image/png")
//指定保存的文件名,
put(MediaStore.Images.Media.DISPLAY_NAME,"${System.currentTimeMillis()}.png")
//指定保存的文件目录,如果不设置这个值,则会被默认保存到对应的媒体类型的文件夹下,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Images.Media.RELATIVE_PATH,"${Environment.DIRECTORY_PICTURES}/DemoPicture")
} else {
put(MediaStore.MediaColumns.DATA,"${Environment.getExternalStorageDirectory().path}${File.separator}${Environment.DIRECTORY_DCIM}${File.separator}${System.currentTimeMillis()}.png")
}
}
//插入文件数据库并获取到文件的Uri
val insertUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,values)
insertUri?.let {
//通过outputStream将图片文件内容写入Url
contentResolver.openOutputStream(it).use {outputStream->
bitmap.compress(Bitmap.CompressFormat.PNG,100,outputStream)
}
}
}
对于 Android 中的媒体类型,应该按照不同的MimeType
放到对应的公共目录媒体文件夹下:
媒体类型 | Uri路径 | MediaStore内部类常量 | 文件类型(MimeType) | 默认存储目录 | 允许存储目录 |
---|---|---|---|---|---|
Image(图片) | content://media/external/images/media | MediaStore.Images.Media.EXTERNAL_CONTENT_URI | image/* | Pictures | DCIM、Pictures |
Audio(音频) | content://media/external/audio/media | MediaStore.Audio.Media.EXTERNAL_CONTENT_URI | audio/* | Music | Alarms、Music、Notifications、Podcasts、Ringtones |
Video(视频) | content://media/external/video/media | MediaStore.Video.Media.EXTERNAL_CONTENT_URI | video/* | Movies | DCIM 、Movies |
Files(下载) | content://media/external/downloads | MediaStore.Downloads.EXTERNAL_CONTENT_URI | file/* | Download | Download |
当通过MediaStore API
创建文件时,文件会保存到对应的类型默认目录,例如图片文件(mimeType = image/*
)会被保存到Pictures(Environment#DIRECTORY_PICTURES)
目录下;也可以使用MediaStore.xxx.Media.RELATIVE_PATH
自己指定要存放的目录或者子目录,如:contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/自定义子目录")
,文件就会放在Pictures/自定义子目录/
中;或者使用contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
,将文件放到DCIM/
中。
需要注意的是,不能将文件放置到允许存储目录之外的其他文件夹下,比如将一个 mimeType
为 audio/mpeg
放到 Pictures
目录下,这样的行为是不被允许的,也就是如果设置 MIME_TYPE = audia/*
并将 RELATIVE_PATH
设置为 Environment#DIRECTORY_PICTURES
这样是会报 Throw IllegalArgumentException
。当然也可以将所有的文件都通过 MediaStore
放到 Downloads
文件夹下。
是MediaStore.Images.Media.RELATIVE_PATH是在Android10才被添加进来的因此在低版本中可以通过FilePath去适配。