ContentProvider底层原理

前言

在android系统中,每一个应用程序在安装时,都会分配一个不同的Linux用户ID,这样,android系统就可以基于Linux用户ID来保护每一个应用程序的数据不会被其他应用程序破坏,即每一个应用程序只可以访问自己创建的数据。然而,有时候我们需要在不同的应用进程之间进行数据共享,这时候就需要使用Content Provider组件在不同的应用程序之间进行数据共享。

Content Provider为android数据的存储和获取抽象了统一的接口,并支持在不同的应用程序之间共享数据,android内置的许多数据都是使用Content Provider形式供开发者调用的(如视频,音频,图片,通信录等),它采用索引表的形式来组织数据,无论数据来源是什么,Content Provider都会认为是一种表,这一点从Content Provider提供的抽象接口就能看出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class XXXContentProvider extends ContentProvider{
@Override
public boolean onCreate() {
return false;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
return null; // 查询
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
return null; // 插入
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0; // 删除
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0; // 更新
}
}

可以看到每个Content Provider都需要自己实现增删改查的功能,因此,可以将Content Provider看作android提供的一个抽象接口层,用于访问表格类的存储媒介,表格只是一种抽象,至于底层存储媒介到底是如何组织,完全由用户实现,也就是说Content Provider自身是没有数据更新及操作能力,它只是将这种操作进行了统一抽象。

基于android的软件架构平台

一个软件平台无非就是数据和业务组成的,一般来说,数据是统一的,而业务则是百花齐放的。由于不同的业务使用到了统一的数据,因此,从垂直的方向上看,一个软件平台至少要由数据层、数据访问层和业务层构成,而从水平方向来看,业务层应该由一系列相对独立的模块组成,以便可以适应越来越复杂的业务环境。在android系统中,数据层使用数据库、文件或者网络来实现,业务层可以使用一系列应用来实现,而数据访问层可以使用Content Provider组件来实现。

在这个软件平台架构中,为了降低业务层中各个应用之间的耦合度,每一个应用都使用一个android应用程序来实现,并且它们都是运行在独立的进程中的。同样,为了降低业务层和数据层的耦合度,我们也将数据访问层,即Content Provider组件,运行在一个独立的应用程序进程中。通过这样的划分,Content Provider组件就可以按照自己的方式来管理平台数据,而上层的android应用程序不需要关心它的具体实现,只要和它约定好数据访问接口就行了。

因为Content Provider组件是运行在一个独立的应用程序进程中的,即它本身也是一个android应用程序,因此,业务层的android应用程序是肯定不可以直接访问它的数据的。所以,Content Provider结合Binder进程间通信机制以及匿名共享内存机制,就可以高效地将它里面的数据传递给业务层中的android应用程序访问了。

Content Provider组件的启动过程

Activity组件第一次访问Provider组件的过程如下所示:

  • Activity组件通过Content URl来访问Provider组件
  • Activity组件所运行在的应用程序进程发现它里面不存在一个用来访问Provider组件的代理对象,于是,它会通过Content URI来请求AMS返回一个用来访问Provider组件的代理对象
  • AMS发现Provider组件还没有启动起来,于是,它就会创建一个新的应用程序进程,然后在这个新创建的应用程序进程中启动Provider组件
  • Provider组件启动起来之后,就会将自己发布到AMS中,以便AMS可以将它的一个的代理对象返回给Activity组件使用

Content Provider组件的数据共享原理

Content Provider组件通过Binder进程间通信机制来突破以应用程序为边界的权限控制,同时,它又以匿名共享内存作为数据传输媒介,从而提供了一种高效的数据共享方式。

Content Provider组件将要传输的共享数据抽象为一个游标,因此,我们在实现自己的Content Provider组件时,需要提供一个游标,以便可以将要共享的数据从一个应用程序传递到另外一个应用程序来访问。

假设Content Provider的数据源是由SQLite来实现的,当Activity组件请求Provider组件返回其内部的数据时,Provider组件首先将其内部的数据封装在一个SQLite数据库游标中,然后再返回给Activity组件。由于Provider组件内部的数据就是保存在一个SQLite数据库中的,因此,当这些数据被读取出来时,它们就很自然地保存在一个SQLite数据库游标中。SQLite数据库游标是由SQLiteCursor类来实现的,接下来通过它来介绍Content Provider组件的数据共享模型。

Activity组件在请求Provider组件返回其内部的数据之前,首先会在当前应用程序中创建一个Cursor Window对象。Cursor Window类实现了Parcelable接口,并且在内部包含了一块匿名共享内存。接下来Activity组件就会通过Binder进程间通信机制将前面所创建的Cursor Window对象(连同它内部的匿名共享内存)传递给Provider组件。

Provider组件获得了Activity组件发送过来的Cursor Window对象之后,就会创建一个SQLiteCursor对象。SQLiteCursor类继承了AbstractWindowedCursor类,而AbstractWindowedCursor类又是从Cursor接口继承下来的。因此,SQLiteCursor类可以用来描述一个游标,即可以用来传输Content Provider组件中的共享数据。

AbstractWindowedCursor类有一个类型为CursorWindow的成员变量mWindow。Provider组件在创建SQLiteCursor对象时,会调用它的成员函数将setWindow将Activity组件发送过来的Cursor Window对象保存在其父类AbstractWindowedCursor的成员变量mWindow中。

SQLiteCursor对象创建完成之后,Provider组件就会将Activity组件所请求的数据保存在这个SQLiteCursor对象中,实际上是保存在与它所关联的一个Cursor Window对象内部的一块匿名共享内存中。由于Activity组件是可以访问这块匿名共享内存的,因此,它就可以通过这块匿名共享内存来获得Provider组件返回给它的数据。

由于SQLiteCursor对象并不是一个Binder本地对象,因此,Provider组件就不能直接将它返回给Activity组件使用。Provider组件首先会创建一个CursorToBulkCursorAdaptor对象,用来适配前面所创建的一个SQLiteCursor对象,即将这个SQLiteCursor对象保存在它的成员变量mCursor中。CursorToBulkCursorAdaptor类继承了BulkCursorNative类,而BulkCursorNative类实现了IBulkCursor接口,并且继承了IBinder类,因此,它可以用来描述一个实现了IBulkCursor接口的Binder本地对象。Provider组件接着就会将这个CursorToBulkCursorAdaptor对象返回给Activity组件。

Activity组件在请求Provider组件返回其内部的数据之前,除了会创建一个CursorWindow对象之外,还会创建一个BulkCursorToCursorAdaptor对象,并且前面所创建的Cursor Window对象就是保存在它的父类AbstractWindowedCursor的成员变量mWindow中的。Activity组件接收到Provider组件返回来的CursorToBulkCursorAdaptor对象之后,实际上获得的是一个CursorToBulkCursorAdaptor代理对象,接着就会将它保存在之前所创建的一个BulkCursorToCursorAdaptor对象的成员变量mBulkCursor中。这时候Activity组件就可以通过这个BulkCursorToCursorAdaptor对象来读取Provider组件返回的数据了。

至此,我们可以得到Content Provider组件的数据共享模型,如下所示。

从Content Provider组件的数据共享模型中,可以看出:

  • Activity组件中的BulkCursorToCursorAdaptor对象的成员变量mBulkCursor引用了Provider组件的CursorToBulkCursorAdaptor对象
  • Activity组件的Cursor Window对象和Provider组件的Cursor Window对象引用了同一块匿名共享内存

当Activity组件第一次通过BulkCursorToCursorAdaptor对象来读取Provider组件返回的数据时,BulkCursorToCursorAdaptor对象首先会通过其成员变量mBulkCurosr来请求Provider组件的CursorToBulkCursorAdaptor对象将它所请求的数据写入到Provider组件的Cursor Window对象所引用的一块匿名共享内存中。接着Activity组件的BulkCursorToCursorAdaptor对象就可以通过其内部的Cursor Window对象来获得Activity组件所请求的数据了,因为两个组件的Cursor Window对象引用了同一块匿名共享内存。Provider组件的CursorToBulkCursorAdaptor对象实际上是通过其内部的SQLiteCursor对象来获得Activity组件所请求的数据的,而这个SQLiteCursor对象又是通过调用其内部的数据库查询计划mQuery的成员函数fillWindow来从数据库mDatabase中获得Activity组件所请求的数据的。

Content Provider组件的数据更新通知机制

Content Provider组件的数据更新通知机制类似于android系统的广播机制,它们都是一种消息发布和订阅的事件驱动模型。在Content Provider组件的数据更新通知机制中,内容观察者(Content Observer)负责接收数据更新通知,而Content Provider组件负责发送数据更新通知。

内容观察者在接收数据更新通知之前,必须要先注册到ContentService中,并且告诉ContentService它要接收什么样的数据更新通知,这是通过一个URI来描述的。当Content Provider组件中的数据发生更新时,Content Provider组件就会将用来描述这个数据的一个URI发送给ContentService,以便ContentService可以找到与这个URI对应的内容观察者,最后向它们发送一个数据更新通知。

Content Provider的同步

多进程对于Content Provider的访问请求最终都会按照队列进入Content Provider进程,而在单进程中,Content Provider对于数据的访问很容易做到多线程互斥,一个synchronized关键字就能搞定。

如果Content Provider的底层实现数据是数据库,可以不用考虑同步问题,但是如果底层是内存数据,则必须做同步处理。

android:multiprocess在Content Provider中的作用

默认情况下是不指定android:process跟multiprocess的,它们的值默认为false,会随着应用启动的时候加载,如果对provider指定android:process和android:multiprocess,表现就会不一致,如果设置android:process,那Content Provider就不会随着应用启动,如果设置了android:multiprocess,则可能存在多个Content Provider实例。

android:multiprocess的作用是:是否允许在调用者的进程里实例化provider,如果android:multiprocess=false,则系统中只会存在一个provider实例,否则,可以存在多个,多个的话,可能会提高性能,因为它避免了跨进程通信,毕竟,对象就在自己的进程空间,可以直接访问,但是,这会增加系统负担,另外,对于单进程能够保证的互斥问题,也会无效,如果APP需要数据更新,还是保持不开启的好。

小结

  • Content Provider只是android为了跨进程共享数据的一种机制,在操作数据上只是一种抽象,具体要自己实现
  • Content Provider主要以表格的形式来组织数据,并且可以包含多个表,和数据库很相似
  • Content Provider的底层实现是Binder,所以Content Provider的6个方法都是运行在Content Provider所在应用的进程中,其中,除了onCreate由系统回调并运行在主线程中,其他5个方法均由外界回调并运行在Binder线程池里,外界每调用一次Content Provider的方法都会独自开启一个线程
  • Content Provider只能保证进程间的互斥,无法保证进程内,需要自己实现
  • 在manifest文件里注册提供器时,使用属性android:authorities=”包名.provider”来唯一指定提供器,name属性用来指定java文件,一般还使用permission属性来指定访问权限,其中可以细分为readPermission和writePermission