一、 内容提供器简介

1、内容提供器主要用于在不同的应用程序/进程之间实现数据共享&交互的功能,他提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性

2、内容提供器可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险

3、ContentProvider = 中间者角色(搬运工),真正存储&操作数据的数据源为原来存储数据的方式(数据库(sqlite)、文件、XML、网络等等) 4、ContentProvider一般为存储和获取数据提供统一的接口,可以在不同的应用程序之间共享数据。

android中的内容提供者 安卓内容提供器是什么_android中的内容提供者

二、原理

内容提供器运用了Android中的Binder机制,该机制是一种Android中实现跨进程通信(IPC)的方式的机制

android中的内容提供者 安卓内容提供器是什么_开发语言_02

三、访问其他程序中的数据

1、内容提供器的用法一般有两种:

  1. 使用现有的内容提供器来读取和操作相应程序中的数据;
  2. 创建自己的内容提供器给我们的程序的数据提供外部访问的接口。

2、若一个应用程序通过内容提供器对其数据提供了外部访问接口,那么任何其他的应用程序就都可以对这部分数据进行访问,如电话簿、短信、媒体库等程序都提供了类似的访问接口。

3.1 常用工具类

3.1.1 ContentResolver类

1、通过借助ContentResolver类以访问内容提供器中共享的数据
2、ContentResolver还用于统一管理不同 ContentProvider间的操作
3、ContentResolver中提供了一系列的方法用于对数据进行CRUD操作

3.1.2 ContentUris类

1、作用:操作URI
2、具体使用

//withAppendedId()作用:向URI追加一个id
	Uri uri = Uri.parse("content://cn.scu.myprovider/user")
	Uri resultUri = ContentUris.withAppendedId(uri, 7);
	// 最终生成后的Uri为:content://cn.scu.myprovider/user/7
	
	// parseId()作用:从URL中获取ID
	Uri uri = Uri.parse("content://cn.scu.myprovider/user/7")
	long personid = ContentUris.parseId(uri);
	//获取的结果为:7

3.1.3 UriMatcher类

作用:在ContentProvider中注册URI;根据URI匹配ContentProvider对应的数据表

3.1.4 ContentObserver类

1、内容观察者作用:观察 Uri 引起 ContentProvider 中的数据变化 & 通知外界(即访问该数据访问者):当ContentProvider 中的数据发生变化(增、删 & 改)时,就会触发该 ContentObserver类通知数据变化
2、适用场景:需要频繁检测的数据库或者某个数据是否发生改变,如果使用线程去操作,很不经济而且很耗时 。

3.2 ContentResolver的基本用法

1、ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri参数代替,这个参数被称为内容URI
2、该URI又称为统一资源标识符(Uniform Resource Identifier),用于唯一标识 ContentProvider & 其中的数据
3、外界进程通过 URI 找到对应的ContentProvider & 其中的数据,再进行数据操作
4、具体使用:URI分为 系统预置 & 自定义,分别对应系统内置的数据(如通讯录、日程表等等)和自定义数据库
5、URI主要由authority和path两部分组成。

  1. authority是用于对不同的应用程序做区分的,一般采用 程序包名.provider 的方式来进行命名
  2. path是用于对同一应用程序中不同的表做区分的,通常添加到authority的后面
  3. 一个URI最标准的格式写法如下:content://com.example.app.provider/table1,其中:
    content: 为协议声;
    com.example.app为程序包名;
    table1为表名。

6、得到内容URI字符串之后,通过调用parse()方法将它解析成Uri对象作为参数传入,解析的代码如下:

Uri uri = Uri.parse("content://com.example.app.provider/table1");

3.3 通过Uri实现CRUD操作

1、下面4个方法由外部进程回调,并运行在ContentProvider进程的Binder线程池中(不是主线程)
2、存在多线程并发访问时,需要实现线程同步

  1. 若ContentProvider的数据存储方式是使用SQLite & 一个,则不需要,因为SQLite内部实现好了线程同步,若是多个SQLite则需要,因为SQL对象之间无法进行线程同步
  2. 若ContentProvider的数据存储方式是内存,则需要自己实现线程同步

3.3.1 query

Cursor cursor = getContentResolver().query(uri,projection,selection,selectionArgs,sortOrder);

通过上述方法获取一个Cursor对象,用于打印查询数据,其中括号里的参数情况如下:

android中的内容提供者 安卓内容提供器是什么_ide_03

if(cursor != null){
			while(cursor.moveToNext()){
				String column1 = cursor.getString(cursor.getColumnIndex("column1"));
				int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
			}
		}
基本上同SQLite一样,通过get方法获取数据然后操作

3.3.2 insert

ContentValues values = new ContentValues();
	values.put("column1","text");
	values.put("column2",1);
	getContentResolver().insert(uri,values);

3.3.3 delete

getContentResolver().delete(uri,"column2 = ?",new String[]{"1"});

3.3.4 update

ContentValues values = new ContentValues();
		values.put("column1","");
		getContentResolver().update(uri,values,"column1 = ? and column2 = ?",new String[]{"text","1"});

3.4 举例:读取系统联系人

用一个ListView+运行时权限来完成对联系人的读取

public class MainActivity extends AppCompatActivity {
    ArrayAdapter<String> adapter;
    List<String> contactsList = new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
      	...
        ListView contactsView = (ListView)findViewById(R.id.contacts_view);
        adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,contactsList);
        contactsView.setAdapter(adapter);
    }
    private void readContacts(){
       ....
    }
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
       ....
    }
}
首先实例化ListView并弄好适配器,其中 android.R.layout.simple_list_item_1是自带的一种元素
@Override
    protected void onCreate(Bundle savedInstanceState) {
      	...
        if(ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)!= PackageManager.PERMISSION_GRANTED){
            ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.READ_CONTACTS},1);
        }else{
            readContacts();
        }
    }
    private void readContacts(){
       ....
    }
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
      	switch (requestCode){
            case 1:
                if(grantResults.length>0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
                    readContacts();
                }else{
                    Toast.makeText(this,"你取消了申请",Toast.LENGTH_SHORT).show();
                }
                break;
            default:
        }
    }
然后就是运行时权限判断
private void readContacts(){
		       Cursor cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
		               null,null,null,null);
		       if(cursor != null){
		           while (cursor.moveToNext()){
		               String name = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
		               String telephone = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
		               contactsList.add(name+"\n"+telephone);
		           }
		           arrayAdapter.notifyDataSetChanged();
		       }
		   }
重点看readContacts()方法里面的代码
	1、首先通过query()方法得到一个Cursor对象,其中的ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME是系统已经封装好的一个常量
	2、然后就是通过循环,将数据读取出来,并存入到List集合里面,即ListView的数据源里
	3、调用notifyDataSetChanged()方法通知刷新一下ListView
<uses-permission android:name="android.permission.READ_CONTACTS"/>
最后是声明权限

四、 创建自己的内容提供器

1、通过新建一个类去继承ContentProvider的方式来创建一个自己的内容提供器,代码如下:

public class MyProvider extends ContentProvider {
		//初始化内容提供器的时候调用。通常会在这里完成对数据库的创建和升级等操作
	    @Override
	    public boolean onCreate() {
	        return false;
	    }
		//从内容提供器中查询数据:使用uri参数确定查询那张表,projection用于确定查询那些列, selection和selectionArgs用于查询那些行, sortOrder用于对结果进行排序
	    @Override
	    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
	        return null;
	    }
		//根据传入的URI来返回对应的MIME类型
	    @Override
	    public String getType(Uri uri) {
	        return null;
	    }
		//添加数据:uri参数确定要添加到的表,待添加的数据保存在values参数中
	    @Override
	    public Uri insert(Uri uri, ContentValues values) {
	        return null;
	    }
		//删除数据:uri参数确定要删除哪个表的数据,selection和selectionArgs用于约束删除哪些行
	    @Override
	    public int delete(Uri uri, String selection, String[] selectionArgs) {
	        return 0;
	    }
		//更新数据:uri参数确定要更新哪个表的数据,values保存新数据,selection和selectionArgs用于约束更新哪些行
	    @Override
	    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
	        return 0;
	    }
	}

2、创建内容提供器时,内容URI写法如下:content://com.example.app.provider/table1;

  • 现在若内容后面加数字,表示期望访问该表中拥有相应id的数据,如 content://com.example.app.provider/table1/1 表示访问id为1的数据;(这个id是自己定义的数据库里面的一个自增型的id序号)
  • 内容URI的格式主要是以上两种:①以路径结尾表示期望访问该表中所有的数据;②以id结尾表示期望访问该表中拥有相应id的数据
  • 使用通配符来分别匹配这两种格式的内容URI,规则如下:
  • 1) * :表示能够匹配任意长度的任意字符,如 content://com.example.app.provider/ * ,表示一个能够匹配任意表的内容URI格式;
  • 2) # :表示能够匹配任意长度的数字,如 content://com.example.app.provider/table1/ # ,表示一个能够匹配table1表中任意一行数据的内容URI格式;

3、借助UriMatcher这个类就可以实现匹配内容URI的功能。该类中提供了一个addURI()方法,该方法接收authority、path和一个自定义代码,这样调用match()方法时就可以将一个Uri对象传入,返回值是某个能够匹配这个Uri对象所对应的自定义代码,利用这个代码,我们就可以判断出调用方期望访问的是那张表中的数据了,代码如下:

public static final int TABLE1_DIR = 0;
    public static final int TABLE1_ITEM = 1;
    public static final int TABLE2_DIR = 2;
    public static final int TABLE2_ITEM = 3;
    private static UriMatcher uriMatcher;
    static {
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI("com.example.app.provider","table1",TABLE1_DIR);//表示访问table1表中所有数据
        uriMatcher.addURI("com.example.app.provider","table1/#",TABLE1_ITEM);//表示访问table1表中单条数据
        uriMatcher.addURI("com.example.app.provider","table2",TABLE2_DIR);//表示访问table12表中所有数据
        uriMatcher.addURI("com.example.app.provider","table2/#",TABLE2_ITEM);//表示访问table2表中单条数据
    }
    @Override
    public Cursor query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal cancellationSignal) {
        switch (uriMatcher.match(uri)) {
            case TABLE1_DIR:
                //查询table1表中的所有数据
                break;
            case TABLE1_ITEM:
                //查询table1表中的单条数据
                break;
            case TABLE2_DIR:
                //查询table2表中的所有数据
                break;
            case TABLE2_ITEM:
                //查询table2表中的单条数据
                break;
            default:
                break;
        }
		return (要求返回的数据);
    }

4、getType()方法:它是所有的内容提供器都必须提供的一个方法,用于获取Uri对象所对应的MIME类型。
5、MIME数据类型的作用在于ContentProvider根据 URI 返回MIME类型
6、一个内容URI所对应的MIME字符串主要由3部分组成,Android对这三部分做出了如下格式规定:

  • 1)必须以vnd开头;
  • 2)如果内容URI以路径结尾,则后面接android.cursor.dir/ ,返回多条记录;如果内容URI以id结尾,则后面接android.cursor.item/,返回单条记录;
  • 3)最后接上vnd.<authority>.<path>
  • 4)例如
    content://com.example.app.provider/table1 对应的MIME类型是vnd.android.cursor.dir/vnd.com.example.app.provider.table1;content://com.example.app.provider/table1/1对应的MIME类型是vnd.android.cursor.item/vnd.com.example.app.provider.table1; 现在我们来完善getType()方法中的逻辑:
@Override
    public String getType(Uri uri) {
        switch (uriMatcher.match(uri)) {
            case TABLE1_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.app.provider.table1";
            case TABLE1_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.app.provider.table1";
            case TABLE2_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.app.provider.table2";
            case TABLE2_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.app.provider.table2";
            default:
                break;
        }
        return null;
    }

5、内容提供器的安全机制:只要不向UriMatcher中添加隐私数据的URI,那么该部分的数据就根本无法被外部程序访问到,安全问题也就不复存在了。