ContentProvider解析

ContentProvider

一、简介

1、定义

它是Android标准的数据访问接口,用于应用间的数据共享,数据源可以是sql、xml、文件、Preferences、网络请求。

架构图

2、优点

安全:把数据共享给其他应用且不用担心敏感数据的泄漏
简单高效:底层使用匿名共享内存来完成数据的共享

3、底层实现

binder+匿名共享内存
Android是基于linux内核的,在Linux系统中每个文件除了数据之外还有文件权限的属性。Linux将系统中的每一个文件都与一个用户以及用户组关联起来,基于不同的用户而赋予不同的文件权限。只有当一个用户对某个文件拥有相应的权限时,才能执行相应的操作。但是有时候不同应用之间需要进行数据的共享,所以Android提供了ContentProvider,那它是如何实现不同应用间的数据共享的呢。我们知道Android的IPC大部分是使用binder,ContentProvider也不例外,不过ContentProvider传输的一般是大量的数据,如果完全使用binder来完成那么效率非常堪忧,所以Android采用了匿名内存共享数据来把需要在进程间传输的数据都写到共享内存中去,然后只通过Binder进程间通信机制来传输一个共享内存的打开文件描述符给对方就好了,对方拿到打开文件描述符就可以对共享内存的数据进行读写。

二、简单使用

1、相关概念

  • URI(Uniform Resource Identifier统一资源表示符)
    它通过唯一的标识标志某个资源的位置,结构类似HTTP形式的URL。外接进程如果想要获取数据需要先通过URI找到数据然后在进行想要的操作。
    它通常由四个部分来组成:

    content://com.robin.provider/user/123
    

    首先是Schema:它固定为content://
    Authority:唯一地标识了一个特定的Content Provider(com.robin.provider)
    Path:资源路径即数据表名(user)
    ID:资源ID(123)
    此外注意URI模式存在匹配通配符:
    *:匹配任意长度的任何有效字符的字符串
    #:匹配任意长度的数字字符的字符串

  • MIME类型
    作用是指定某个扩展名的数据用某种特定的应用打开。每个MIME类型由两部分组成,前面是数据的大类别,后面定义具体的种类。在ContentProvider中,URI所对应的资源的MIME类型的大类别根据同时访问的资源的数量分为两种,对于访问单个资源的URI,它的大类别就为vnd.android.cursor.item,而对于同时访问多个资源的URI,它的大类别就为vnd.android.cursor.dir。后面具体的种类则由ContentProvider的提供者设置,它的格式一般为vnd.[company name].[resource type]的形式。

// 形式1:单条记录  
vnd.android.cursor.item/自定义
// 形式2:多条记录(集合)
vnd.android.cursor.dir/自定义 

/**完整的MIME类型**/
// 单个记录的MIME类型
  vnd.android.cursor.item/vnd.robin.table
// 多个记录的MIME类型
  vnd.android.cursor.dir/vnd.robin.table

MIME类型常量主要是在实现ContentProvider的getType函数时用到。

2、Content Provider的实现

实现自己的Content Provider时,必须继承自ContentProvider,并且实现以下六个函数:
-- onCreate(),用来执行一些初始化的工作。
-- query(Uri, String[], String, String[], String),用来返回数据给调用者。
-- insert(Uri, ContentValues),用来插入新的数据。
-- update(Uri, ContentValues, String, String[]),用来更新已有的数据。
-- delete(Uri, String, String[]),用来删除数据。
-- getType(Uri),用来返回数据的MIME类型。

3、ContentResolver

ContentProvider类并不会直接与外部进程交互,而是通过
ContentResolver类。ContentResolver可以通过URI操作ContentProvider类中的数据,外界进程也是通过ContentResolver来与ContentProvider进行交互。

ContentResolver类提供了与ContentProvider类相同名字和作用的4个方法

// 外部进程向 ContentProvider 中添加数据
public Uri insert(Uri uri, ContentValues values)  

// 外部进程 删除 ContentProvider 中的数据
public int delete(Uri uri, String selection, String[] selectionArgs)

// 外部进程更新 ContentProvider 中的数据
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)  

// 外部应用 获取 ContentProvider 中的数据
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)

同时Android还提供了三个辅助类:

  • ContentUris
    它是用来操作URI的,主要方法是withAppendedId和parseId
// withAppendedId()作用:向URI追加一个id
Uri uri = Uri.parse(“content://com.robin.provider/user”) 
Uri resultUri = ContentUris.withAppendedId(uri, 7);  
// 最终生成后的Uri为:content://com.robin.provider/user/7

// parseId()作用:从URL中获取ID
Uri uri = Uri.parse(“content://com.robin.provider/user/7”) 
long personid = ContentUris.parseId(uri); 
//获取的结果为:7
  • UriMatcher
    它的作用是在ContentProvider中注册URI、根据URI 匹配ContentProvider中对应的数据表。
// 步骤1:初始化UriMatcher对象
    UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); 
    //常量UriMatcher.NO_MATCH  = 不匹配任何路径的返回码
    // 即初始化时不匹配任何东西

// 步骤2:在ContentProvider 中注册URI(addURI())
    int URI_ITEM_1 = 1;
    int URI_ITEM_2 = 2;
    matcher.addURI(“com.robin.provider”, “user1”, URI_ITEM_1); 
    matcher.addURI(“com.robin.provider”, “user2”, URI_ITEM_2); 
    // 若URI资源路径 = content://com.robin.provider/user1 ,则返回注册码URI_ITEM_1
    // 若URI资源路径 = content://com.robin.provider/user2 ,则返回注册码URI_ITEM_2

// 步骤3:根据URI 匹配 URI_CODE,从而匹配ContentProvider中相应的资源(match())
  • ContentObserver
    内容观察者它的作用是观察Uri引起ContentProvider中的数据变化和通知外界(即访问该数据访问者)
// 步骤1:注册内容观察者ContentObserver
    getContentResolver().registerContentObserver(uri);
    // 通过ContentResolver类进行注册,并指定需要观察的URI

// 步骤2:当该URI的ContentProvider数据发生变化时,通知外界(即访问该ContentProvider数据的访问者)
    public class MyContentProvider extends ContentProvider { 
      public Uri insert(Uri uri, ContentValues values) { 
         db.insert(“table”, “tableid”, values); 
         getContext().getContentResolver().notifyChange(uri, null); 
      // 通知访问者
   } 
}

// 步骤3:解除观察者
 getContentResolver().unregisterContentObserver(uri);
    // 同样需要通过ContentResolver类进行解除

三、相关问题

  • 为什么要使用 ContentProvider?与直接读取数据库相比ContentProvider有什么优势?

ContentProvider是Android系统提供的跨应用数据共享的解决方案。它对数据进行封装并提供了统一的数据访问接口,这样做的好处就是与底层数据源进行解耦。我们可以不用关心数据源的形式(数据源可以是数据库、网络、xml)同时统一的数据访问接口也方便用户的使用降低使用成本。直接读取数据库的方式如果在项目发生变化要更改数据存储方式时访问者也需要做出对应的调整。

  • ContentProvider 是如何实现数据共享的?ContentProvider 是如何在不同应用程序之间传输数据的?

ContentProvider 的底层实现是基于Binder+匿名共享内存。ContentProvider 的数据共享以及在不同进程间传输数据都是基于上述原理实现的。我们知道ContentProvider可以实现进程间数据共享这必然涉及了进程间通信,ContentProvider使用Binder来完成进程间通信。不过ContentProvider并不是通过进程间通信直接传递共享数据,因为共享数据可能很大直接使用Binder机制进行传递并不高效,所以这里传递的是共享数据的文件描述符。那ContentProvider是如何实现数据共享的呢,这就需要匿名共享内存了,ContentProvider会开辟一块匿名共享内存然后把要共享的数据放到匿名共享内存中,然后通过Binder把共享数据的文件描述符发送给用户,用户拿着文件描述符去匿名共享内存中去读取数据,这样就完成了数据的进程间共享。

  • ContentProvider 接口方法运行在哪个线程中呢

ContentProvideronCreate()是运行在UI线程的,而query()insert()delete()update() 是运行在线程池中的工作线程的

  • 运行在主线程的 ContentProvider 为什么不会影响主线程的 UI 操作?

query()insert()delete()update() 是运行在线程池中的工作线程的所以并不会阻塞 ContentProvider 所在进程的主线程但可能会阻塞调用者所在的进程的UI线程,所以调用ContentProvider 的操作要放到子线程中去做

  • 向外提供数据共享,那么如何限制对方的使用呢?
  1. 可以使用android:exported属性。这个属性用于指示该服务是否能够被其他应用程序组件调用或跟它交互。
  2. 对于需要开放的组件应设置合理的权限,如果只需要对同一个签名的其它应用开放 ContentProvider ,则可以设置 signature 级别的权限
  3. 添加访问权限 如读、写权限
  • 多个进程同时调用一个 ContentProvider 的 query 获取数据,ContentPrvoider 是如何反应的呢?

query()insert()delete()update() 都是在 ContentProvider 进程的线程池中被调用执行的,而不是进程的主线程中。这个线程池是由 Binder 创建和维护的,其实使用的就是每个应用进程中的 Binder 线程池。

四、源码分析

ContentProvider的启动过程源代码分析

详情看这里

参考链接:

https://github.com/Omooo/Android-Notes/blob/master/blogs/Android/ContextProvider.md

android_interview/ContentProvider.md at master · LRH1993/android_interview · GitHub

https://www.jianshu.com/p/ea8bc4aaf057

https://www.jianshu.com/p/7a6b786ba728

posted @ 2020-10-12 14:29  Robin132929  阅读(895)  评论(0编辑  收藏  举报