Android ContentProvider 基本原理和使用详解
ContentProvider(内容提供者)是 Android 的四大组件之一,管理 Android 以结构化方式存放的数据,以相对安全的方式封装数据(表)并且提供简易的处理机制和统一的访问接口供其他程序调用。
Android 的数据存储方式总共有五种,分别是:Shared Preferences、网络存储、文件存储、外储存储、SQLite。但一般这些存储都只是在单独的一个应用程序之中达到一个数据的共享,有时候我们需要操作其他应用程序的一些数据,就会用到 ContentProvider。而且 Android 为常见的一些数据提供了默认的 ContentProvider(包括音频、视频、图片和通讯录等)。

URI(Uniform Resource Identifier)
其它应用可以通过 ContentResolver 来访问 ContentProvider 提供的数据,而 ContentResolver 通过 uri 来定位自己要访问的数据,所以我们要先了解 uri。URI(Universal Resource Identifier)统一资源定位符,如果您使用过安卓的隐式启动就会发现,在隐式启动的过程中我们也是通过 uri 来定位我们需要打开的 Activity 并且可以在 uri 中传递参数。
URI 为系统中的每一个资源赋予一个名字,比方说通话记录。每一个 ContentProvider 都拥有一个公共的 URI,用于表示 ContentProvider 所提供的数据。URI 的格式如下:
- // 规则
- [scheme:][//host:port][path][?query]
- // 示例
- content://com.wang.provider.myprovider/tablename/id:
- 标准前缀(scheme)——content://,用来说明一个Content Provider控制这些数据;
URI 的标识 (host:port)—— com.wang.provider.myprovider,用于唯一标识这个 ContentProvider,外部调用者可以根据这个标识来找到它。对于第三方应用程序,为了保证 URI 标识的唯一性,它必须是一个完整的、小写的类名。这个标识在元素的authorities属性中说明,一般是定义该 ContentProvider 的包.类的名称;
路径(path)——tablename,通俗的讲就是你要操作的数据库中表的名字,或者你也可以自己定义,记得在使用的时候保持一致就可以了;
记录ID(query)——id,如果URI中包含表示需要获取的记录的 ID,则返回该id对应的数据,如果没有ID,就表示返回全部;
对于第三部分路径(path)做进一步的解释,用来表示要操作的数据,构建时应根据实际项目需求而定。如:
操作tablename表中id为11的记录,构建路径:/tablename/11;
操作tablename表中id为11的记录的name字段:tablename/11/name;
操作tablename表中的所有记录:/tablename;
操作来自文件、xml或网络等其他存储方式的数据,如要操作xml文件中tablename节点下name字段:/ tablename/name;
若需要将一个字符串转换成Uri,可以使用Uri类中的parse()方法,如:
- Uri uri = Uri.parse("content://com.wang.provider.myprovider/tablename");
再来看一个例子:
- http://www.baidu.com:8080/wenku/jiatiao.html?id=123456&name=jack
uri 的各个部分在安卓中都是可以通过代码获取的,下面我们就以上面这个 uri 为例来说下获取各个部分的方法:
getScheme():获取 Uri 中的 scheme 字符串部分,在这里是 http
getHost():获取 Authority 中的 Host 字符串,即 www.baidu.com
getPost():获取 Authority 中的 Port 字符串,即 8080
getPath():获取 Uri 中 path 部分,即 wenku/jiatiao.html
getQuery():获取 Uri 中的 query 部分,即 id=15&name=du
MIME
MIME 是指定某个扩展名的文件用一种应用程序来打开,就像你用浏览器查看 PDF 格式的文件,浏览器会选择合适的应用来打开一样。Android 中的工作方式跟 HTTP 类似,ContentProvider 会根据 URI 来返回 MIME 类型,ContentProvider 会返回一个包含两部分的字符串。MIME 类型一般包含两部分,如:
- text/html
- text/css
- text/xml
- application/pdf
分为类型和子类型,Android 遵循类似的约定来定义MIME类型,每个内容类型的 Android MIME 类型有两种形式:多条记录(集合)和单条记录。
- 集合记录(dir):
- vnd.android.cursor.dir/自定义
- 单条记录(item):
- vnd.android.cursor.item/自定义
vnd 表示这些类型和子类型具有非标准的、供应商特定的形式。Android中类型已经固定好了,不能更改,只能区别是集合还是单条具体记录,子类型可以按照格式自己填写。
在使用 Intent 时,会用到 MIME,根据 Mimetype 打开符合条件的活动。
UriMatcher
Uri 代表要操作的数据,在开发过程中对数据进行获取时需要解析 Uri,Android 提供了两个用于操作 Uri 的工具类,分别为 UriMatcher 和 ContentUris 。掌握它们的基本概念和使用方法,对一个 Android 开发者来说是一项必要的技能。
UriMatcher 类用于匹配 Uri,它的使用步骤如下:
- 将需要匹配的Uri路径进行注册,代码如下:
- //常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码
- UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
- //如果match()方法匹配“content://com.wang.provider.myprovider/tablename”路径,返回匹配码为1
- sMatcher.addURI("content://com.wang.provider.myprovider", " tablename ", 1);
- //如果match()方法匹配content://com.wang.provider.myprovider/tablename/11路径,返回匹配码为2
- sMatcher.addURI("com.wang.provider.myprovider", "tablename/#", 2);
此处采用 addURI 注册了两个需要用到的 URI;注意,添加第二个 URI 时,路径后面的 id 采用了通配符形式 “#”,表示只要前面三个部分都匹配上了就 OK。
注册完需要匹配的 Uri 后,可以使用 sMatcher.match(Uri) 方法对输入的 Uri 进行匹配,如果匹配就返回对应的匹配码,匹配码为调用 addURI() 方法时传入的第三个参数。
- switch (sMatcher.match(Uri.parse("content://com.zhang.provider.yourprovider/tablename/100"))) {
- case 1:
- //match 1, todo something
- break;
- case 2
- //match 2, todo something
- break;
- default:
- //match nothing, todo something
- break;
- }
ContentUris
ContentUris 类用于操作 Uri 路径后面的 ID 部分,它有两个比较实用的方法:withAppendedId(Uri uri, long id) 和 parseId(Uri uri)。
withAppendedId(Uri uri, long id) 用于为路径加上 ID 部分:
- Uri uri = Uri.parse("content://cn.scu.myprovider/user")
- //生成后的Uri为:content://cn.scu.myprovider/user/7
- Uri resultUri = ContentUris.withAppendedId(uri, 7);
parseId(Uri uri) 则从路径中获取 ID 部分:
- Uri uri = Uri.parse("content://cn.scu.myprovider/user/7")
- //获取的结果为:7
- long personid = ContentUris.parseId(uri);
ContentProvider 主要方法
ContentProvider 是一个抽象类,如果我们需要开发自己的内容提供者我们就需要继承这个类并复写其方法,需要实现的主要方法如下:
public boolean onCreate():
在创建 ContentProvider 时使用public Cursor query():
用于查询指定 uri 的数据返回一个 Cursorpublic Uri insert():
用于向指定uri的 ContentProvider 中添加数据public int delete():
用于删除指定 uri 的数据public int update():
用户更新指定 uri 的数据public String getType():
用于返回指定的 Uri 中的数据 MIME 类型
数据访问的方法 insert,delete 和 update 可能被多个线程同时调用,此时必须是线程安全的。
如果操作的数据属于集合类型,那么 MIME 类型字符串应该以 vnd.android.cursor.dir/ 开头,
- 要得到所有 tablename 记录: Uri 为 content://com.wang.provider.myprovider/tablename,那么返回的MIME类型字符串应该为:vnd.android.cursor.dir/table。
如果要操作的数据属于非集合类型数据,那么 MIME 类型字符串应该以 vnd.android.cursor.item/ 开头,
- 要得到 id 为 10 的 tablename 记录,Uri 为 content://com.wang.provider.myprovider/tablename/10,那么返回的 MIME 类型字符串为:vnd.android.cursor.item/tablename 。
方法使用示例
使用 ContentResolver 对 ContentProvider 中的数据进行操作的代码如下:
- ContentResolver resolver = getContentResolver();
- Uri uri = Uri.parse("content://com.wang.provider.myprovider/tablename");
- // 添加一条记录
- ContentValues values = new ContentValues();
- values.put("name", "wang1");
- values.put("age", 28);
- resolver.insert(uri, values);
- // 获取tablename表中所有记录
- Cursor cursor = resolver.query(uri, null, null, null, "tablename data");
- while(cursor.moveToNext()){
- Log.i("ContentTest", "tablename_id="+ cursor.getInt(0)+ ", name="+ cursor.getString(1));
- }
- // 把id为1的记录的name字段值更改新为zhang1
- ContentValues updateValues = new ContentValues();
- updateValues.put("name", "zhang1");
- Uri updateIdUri = ContentUris.withAppendedId(uri, 2);
- resolver.update(updateIdUri, updateValues, null, null);
- // 删除id为2的记录,即字段age
- Uri deleteIdUri = ContentUris.withAppendedId(uri, 2);
- resolver.delete(deleteIdUri, null, null);
监听数据变化
如果ContentProvider的访问者需要知道数据发生的变化,可以在ContentProvider发生数据变化时调用getContentResolver().notifyChange(uri, null)来通知注册在此URI上的访问者。只给出类中监听部分的代码:
- public class MyProvider extends ContentProvider {
- public Uri insert(Uri uri, ContentValues values) {
- db.insert("tablename", "tablenameid", values);
- getContext().getContentResolver().notifyChange(uri, null);
- }
- }
而访问者必须使用 ContentObserver 对数据(数据采用 uri 描述)进行监听,当监听到数据变化通知时,系统就会调用 ContentObserver 的 onChange() 方法:
- getContentResolver().registerContentObserver(Uri.parse("content://com.ljq.providers.personprovider/person"),
- true, new PersonObserver(new Handler()));
- public class PersonObserver extends ContentObserver{
- public PersonObserver(Handler handler) {
- super(handler);
- }
- public void onChange(boolean selfChange) {
- //to do something
- }
- }
实例说明
数据源是 SQLite, 用 ContentResolver 操作 ContentProvider。
Constant.java(储存一些常量)
- public class Constant {
- public static final String TABLE_NAME = "user";
- public static final String COLUMN_ID = "_id";
- public static final String COLUMN_NAME = "name";
- public static final String AUTOHORITY = "cn.scu.myprovider";
- public static final int ITEM = 1;
- public static final int ITEM_ID = 2;
- public static final String CONTENT_TYPE = "vnd.android.cursor.dir/user";
- public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/user";
- public static final Uri CONTENT_URI = Uri.parse("content://" + AUTOHORITY + "/user");
- }
DBHelper.java (操作数据库)
- public class DBHelper extends SQLiteOpenHelper {
- private static final String DATABASE_NAME = "finch.db";
- private static final int DATABASE_VERSION = 1;
- public DBHelper(Context context) {
- super(context, DATABASE_NAME, null, DATABASE_VERSION);
- }
- @Override
- public void onCreate(SQLiteDatabase db) throws SQLException {
- //创建表格
- db.execSQL("CREATE TABLE IF NOT EXISTS "+ Constant.TABLE_NAME + "("+ Constant.COLUMN_ID +" INTEGER PRIMARY KEY AUTOINCREMENT," + Constant.COLUMN_NAME +" VARCHAR NOT NULL);");
- }
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) throws SQLException {
- // 这里知识简单删除并创建表格
- // 如果需要保留原来的数据,需要先备份再删除
- db.execSQL("DROP TABLE IF EXISTS "+ Constant.TABLE_NAME+";");
- onCreate(db);
- }
- }
MyProvider.java (自定义的 ContentProvider )
- public class MyProvider extends ContentProvider {
- DBHelper mDbHelper = null;
- SQLiteDatabase db = null;
- private static final UriMatcher mMatcher;
- static{
- mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// 注册 uri- mMatcher.addURI(Constant.AUTOHORITY,Constant.TABLE_NAME, Constant.ITEM);
- mMatcher.addURI(Constant.AUTOHORITY, Constant.TABLE_NAME+"/#", Constant.ITEM_ID);
- }
- @Override
- public String getType(Uri uri) {
// 根据匹配规则返回对应的类型- switch (mMatcher.match(uri)) {
- case Constant.ITEM:
- return Constant.CONTENT_TYPE;
- case Constant.ITEM_ID:
- return Constant.CONTENT_ITEM_TYPE;
- default:
- throw new IllegalArgumentException("Unknown URI"+uri);
- }
- }
- @Override
- public Uri insert(Uri uri, ContentValues values) {
- // TODO Auto-generated method stub
- long rowId;
- if(mMatcher.match(uri)!=Constant.ITEM){
- throw new IllegalArgumentException("Unknown URI"+uri);
- }
- rowId = db.insert(Constant.TABLE_NAME,null,values);
- if(rowId>0){
- Uri noteUri=ContentUris.withAppendedId(Constant.CONTENT_URI, rowId);
- getContext().getContentResolver().notifyChange(noteUri, null);
- return noteUri;
- }
- throw new SQLException("Failed to insert row into " + uri);
- }
- @Override
- public boolean onCreate() {
- // TODO Auto-generated method stub
- mDbHelper = new DBHelper(getContext());
- db = mDbHelper.getReadableDatabase();
- return true;
- }
- @Override
- public Cursor query(Uri uri, String[] projection, String selection,
- String[] selectionArgs, String sortOrder) {
- // TODO Auto-generated method stub
- Cursor c = null;
- switch (mMatcher.match(uri)) {
- case Constant.ITEM:
- c = db.query(Constant.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
- break;
- case Constant.ITEM_ID:
- c = db.query(Constant.TABLE_NAME, projection,Constant.COLUMN_ID + "="+uri.getLastPathSegment(), selectionArgs, null, null, sortOrder);
- break;
- default:
- throw new IllegalArgumentException("Unknown URI"+uri);
- }
- c.setNotificationUri(getContext().getContentResolver(), uri);
- return c;
- }
- @Override
- public int update(Uri uri, ContentValues values, String selection,
- String[] selectionArgs) {
- // TODO Auto-generated method stub
- return 0;
- }
- @Override
- public int delete(Uri uri, String selection, String[] selectionArgs) {
- // TODO Auto-generated method stub
- return 0;
- }
- }
MainActivity.java(ContentResolver操作)
- public class MainActivity extends Activity {
- private ContentResolver mContentResolver = null;
- private Cursor cursor = null;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- // TODO Auto-generated method stub
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- TextView tv = (TextView) findViewById(R.id.tv);
- mContentResolver = getContentResolver();
- tv.setText("添加初始数据 ");
- for (int i = 0; i < 10; i++) {
- ContentValues values = new ContentValues();
- values.put(Constant.COLUMN_NAME, "fanrunqi"+i);
- mContentResolver.insert(Constant.CONTENT_URI, values);
- }
- tv.setText("查询数据 ");
- cursor = mContentResolver.query(Constant.CONTENT_URI, new String[]{Constant.COLUMN_ID,Constant.COLUMN_NAME}, null, null, null);
- if (cursor.moveToFirst()) {
- String s = cursor.getString(cursor.getColumnIndex(Constant.COLUMN_NAME));
- tv.setText("第一个数据: "+s);
- }
- }
- }
最后在manifest申明 :
- <provider android:name="MyProvider" android:authorities="cn.scu.myprovider" />
总结:
如何通过 ContentProvider 查询数据? 通过 ContentResolver 进行uri匹配
如何实现自己的ContentProvider? 继承 ContentProvider,实现对应的方法。在 manifest 中声明
额外补充:隐式 Intent 中 <data> 标签
该部分内容与 ContentProvider 没关系,只是这里讲到了 URI,就顺便此处在插入另外一个知识点:Intent 中 <data> 标签。看不懂的可以直接略过,看下一步分的内容,此处内容与 activity 相关。
Data 的匹配规则:如果过滤规则 intent-filter 中定义了 data,那么 Intent 中必须也要携带可匹配的 data
data 的语法如下所示:
- <data android:scheme=“string”
- android:host=“string”
- android:port=“string”
- android:path=“string”
- android:pathPattern=“string”
- android:pathPrefix=“string”
- android:mimeType=“string”>
data 由两部分组成:mimeType 和 URI。mimeType 可以为空,URI 一定不会为空,因为有默认值。mimeType 指媒体类型,比如 image/jpeg,video/* 等,可表示图片,视频等不同的媒体格式
示例1
data 的匹配:
- <intent-filter>
- <action android:name="com.action.demo1"></action>
- <category android:name="android.intent.category.DEFAULT" />
- <data
- android:scheme="x7"
- android:host="www.360.com"
- />
- </intent-filter>
清单文件 intent-filter 定义的 data 中,只有 URI, 没有 mimeType 类型,匹配如下
- intent.setData(Uri.parse("x7://www.360.com"))
示例2
- <intent-filter>
- <action android:name="com.action.demo1"></action>
- <category android:name="android.intent.category.DEFAULT" />
- <data android:mimeType="image/*" />
- </intent-filter>
清单文件 intent-filter 定义的 data 中,没有定义 URI,只有 mimeType 类型,但是 URI 却有默认值,URI 中的 scheme 默认为 content 或者 file,host 一定不能为空,随便给个字符串abc 都可以,匹配如下
- intent.setDataAndType(Uri.parse("content://abc"),"image/png");
注意:
content://abc 换成 file://abc 在 7.0 以上的版本(即把 targetSdkVersion 指定成 24 及之上并且在 API>=24 的设备上运行)会出现 FileUriExposedException 异常,google 提供了FileProvider 解决这个异常,使用它可以生成 content://Uri 来替代 file://Uri.
示例3
- <intent-filter>
- <action android:name="com.action.demo1"></action>
- <category android:name="android.intent.category.DEFAULT" />
- <data
- android:mimeType="image/*"
- android:scheme="x7"
- android:host="www.360.com"
- />
- <data
- android:mimeType="video/*"
- android:scheme="x7"
- android:host="www.360.com"
- />
- </intent-filter>
清单文件 intent-filter 定义的 data 中,URI 和 mimeType 都有, 匹配如下
- intent.setDataAndType(Uri.parse("x7://www.360.com"),"image/png");
- // 或者
- intent.setDataAndType(Uri.parse("x7://www.360.com"),"video/mpeg");
Inent 中携带的 data 标签对应的数据,在某一组 intent-filter 中可以找到,即匹配成功。data 标签数据在 intent-filter 中也可以有多组
隐示启动,防止匹配失败的可以提前检测:
判断的方法如下:
- PackageManager mPackageManager = getPackageManager();
- //返回匹配成功中最佳匹配的一个act信息,intent 需要按照前面的把 action, data 等都设置好
- ResolveInfo info = mPackageManager.resolveActivity(intent,PackageManager.MATCH_DEFAULT_ONLY);
- //返回所有匹配成功的act信息,是一个集合
- List<ResolveInfo> infoList = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
只要上述 2 个方法的返回值不为 null,那么 startActivity 一定可以成功
参考文章:
ContentProvider 数据访问详解
ContentProvider全解析和使用
Android ContentProvider 基本原理和使用详解的更多相关文章
- Drawable实战解析:Android XML shape 标签使用详解(apk瘦身,减少内存好帮手)
Android XML shape 标签使用详解 一个android开发者肯定懂得使用 xml 定义一个 Drawable,比如定义一个 rect 或者 circle 作为一个 View 的背景. ...
- Android图片缓存之Bitmap详解
前言: 最近准备研究一下图片缓存框架,基于这个想法觉得还是先了解有关图片缓存的基础知识,今天重点学习一下Bitmap.BitmapFactory这两个类. 图片缓存相关博客地址: Android图片缓 ...
- Android Design Support Library使用详解
Android Design Support Library使用详解 Google在2015的IO大会上,给我们带来了更加详细的Material Design设计规范,同时,也给我们带来了全新的And ...
- Android 之窗口小部件详解--App Widget
Android 之窗口小部件详解--App Widget 版本号 说明 作者 日期 1.0 添加App Widge介绍和示例 Sky Wang 2013/06/27 1 App ...
- Android不规则点击区域详解
Android不规则点击区域详解 摘要 今天要和大家分享的是Android不规则点击区域,准确说是在视觉上不规则的图像点击响应区域分发. 其实这个问题比较简单,对于很多人来说根本不值得做为一篇博文写出 ...
- [Android新手区] SQLite 操作详解--SQL语法
该文章完全摘自转自:北大青鸟[Android新手区] SQLite 操作详解--SQL语法 :http://home.bdqn.cn/thread-49363-1-1.html SQLite库可以解 ...
- Android中Service的使用详解和注意点(LocalService)
Android中Service的使用详解和注意点(LocalService) 原文地址 开始,先稍稍讲一点android中Service的概念和用途吧~ Service分为本地服务(LocalServ ...
- Android中SurfaceView的使用详解
Android中SurfaceView的使用详解 http://blog.csdn.net/listening_music/article/details/6860786 Android NDK开发 ...
- 《Android群英传》读书笔记 (5) 第十一章 搭建云端服务器 + 第十二章 Android 5.X新特性详解 + 第十三章 Android实例提高
第十一章 搭建云端服务器 该章主要介绍了移动后端服务的概念以及Bmob的使用,比较简单,所以略过不总结. 第十三章 Android实例提高 该章主要介绍了拼图游戏和2048的小项目实例,主要是代码,所 ...
随机推荐
- Linux:安装禅道
一.准备工作 禅道安装包ZenTaoPMS.8.1.3.zbox_64.gz,上传至服务器:rz命令 解压到指定目录 tar -zxvf ZenTaoPMS.8.1.3.zbox_64.gz -C ...
- hystrix文档翻译之metrics
metrics和监控 动机 HystrixCommands和HystrixObservableCommands执行过程中会产生相关运行情况的metrics.这些metrics对于监控系统表现有很大的 ...
- 003.当在windows终端输入ipconfig时,显示不是内部或外部命令,也不是可运行的程序或批处理文件
当在windows终端输入ipconfig时,显示不是内部或外部命令,也不是可运行的程序或批处理文件,这是环境变量的问题: 右键我的电脑→→→属性→→→高级系统设置→→→(高级)环境变量 在弹出的窗口 ...
- php第三天-数组的定义,数组的遍历,常规数组的操作
0x01 数组分类 在php中有两种数组:索引数组和关联数组 索引数组的索引值是整数,以0开始.当通过位置来标识东西时用索引数组. 关联数组是以字符串作为索引值,关联数组更像操作表.索引值为列名,用于 ...
- Jaskson精讲第6篇-自定义JsonSerialize与Deserialize实现数据类型转换
Jackson是Spring Boot(SpringBoot)默认的JSON数据处理框架,但是其并不依赖于任何的Spring 库.有的小伙伴以为Jackson只能在Spring框架内使用,其实不是的, ...
- SPJ方法
https://www.cnblogs.com/ztz11/p/10657351.html luogu https://blog.csdn.net/qwerty1125/article/details ...
- 推荐一个超牛的SpringCloud微服务项目,开发脚手架
前言 Cloud-Platform是国内首个基于Spring Cloud微服务化开发平台,具有统一授权.认证后台管理系统,其中包含具备用户管理.资源权限管理.网关API 管理等多个模块,支持多业务系统 ...
- 对Elasticsearch生命周期的思考
什么是es索引的生命周期?有啥用?可以怎么用?用了有什么好处呢? 在现实的生产环境中有没有觉得自己刚开始设计的索引的分片数刚刚好,但是随着时间的增长,数据量增大,增长速度增大的情况下,你的es索引的设 ...
- 微信小程序 A~Z城市选择器js文件
微信小程序城市选择 [a~z] 的所有城市选择 city.js a~z排序的城市数据 addressChoose.js 其他js文件可引用 city.js /** * Created by yvded ...
- mysql-4-functions
#进阶4:常见函数(单行函数) /* 将一组逻辑语句封装在方法体中,对外暴露方法名 语法: SELECT 函数名() [FROM 表名] 分类: 1.单行函数:concat,length,ifnull ...