1.ContentProvider内容提供者
ContentProvider是android四大组件之一,它是应用程序之间共享数据的一种接口机制,用于在不同应用程序之间共享和管理数据。

Android中用来保存持久化数据的方式有5种:
①使用SharedPreferences存储数据
②文件存储数据
③SQLite存储数据
④使用ContentProvider存储数据
⑤网络存储数据

ContentProvider提供了一种标准化的方式来访问和管理应用程序的数据,使多个应用程序可以安全地共享数据,而无需直接访问彼此的数据库或文件。
ContentProvider通常使用SQLite数据库、文件、网络或其他存储方式来保存数据,其他应用程序可以通过ContentResolver接口来对这些数据进行增删改查等操作,而不需要关心数据的具体存储方式。

假如应用A提供了数据,应用B要操作应用A的数据,那么应用A需要使用ContentProvider共享自己的数据,应用B使用ContentResolver操作应用A的数据,并且通过ContentObserver监听应用A的数据变化,当应用A的数据发生改变时,通知ContentObserver去告诉应用B数据变化了及时更新。这就是通信的大致流程。

ContentProvider的主要特点和用途:
①数据共享:ContentProvider允许不同应用程序之间共享数据,以实现数据交换和共享功能。
②数据查询:其他应用程序可以通过ContentResolver接口向ContentProvider发起查询请求,获取特定数据的查询结果。
③数据更新:ContentProvider允许其他应用程序通过ContentResolver接口对数据进行增删改操作。
④权限控制:ContentProvider可以对数据访问进行权限控制,限制某些应用程序对数据的访问权限。

使用ContentProvider共享数据的好处就是统一了数据的访问方式。ContentProvider可以指定需要共享的数据,而其他应用程序则可以在不知道数据来源、路径的情况下,对共享数据进行增删改查等操作。

在Android系统中很多Android系统内置的数据也是通过ContentProvider提供给用户使用,比如联系人信息、短信信息、图片库、音频库等,这些信息可以使用Uri直接访问。

ContentProvider提供了对底层数据存储方式的抽象,比如下图,底层使用了Sqlit数据库,在用ContentProvider进行封装后,把sqlit换成其他数据库也不会影响其功能。

Android跨进程调用服务如何启动 android跨进程共享数据_android


2.ContentProvider用法

①Uri

Uri指通用资源标志符,代表要操作的数据的地址,即这个ContentProvider所提供的数据。每一个ContentProvider都会有唯一的地址。

ContentProvider使用的Uri格式如下:
content://authority/data_path/id
content://:通用前缀,说明该Uri用于ContentProvider定位资源;
authority:授权者名称,用来确定具体由哪一个ContentProvider提供资源。因此一般authority都由类的小写全称组成,以保证唯一性;
data_path:数据路径,用来确定请求的是哪个数据集;
id:数据编号,用来请求单条数据。如果无指定,默认返回全部记录;

使用UriMatch来匹配URI,比如应用A提供了数据,但并不是所有的应用都可以操作这些数据,只有提供了满足应用A的URI才能访问A的数据,而UriMatch就是做这个匹配工作的。
常用方法:
(1)public UriMatcher(int code) 创建UriMatch对象。
uriMatcher = new UriMatcher( UriMatcher.NO_MATCH); //给定一个默认值,如果匹配不成功返回默认值NO_MATCH(-1)
(2)public void addURI(String authority,String path, int code) 添加一个用于匹配的Uri,当匹配成功时返回第三个参数code。
authority:主机名(用于唯一标识一个ContentProvider,这个要和清单文件中的authorities属性相同);
path:路径,用来表示要操作的数据;
code:返回值(用于匹配uri的时候,作为匹配成功的返回值)
(3)public int match(Uri uri) 这里的Uri就是传过来的要进行验证匹配的Uri。
Uri可以是精确的字符串,Uri中带*表示可匹配任意text,#表示只能匹配数字。

使用举例:

public static final String AUTHORITY = “com.scc.userprovider”; //这里的名称必须与AndroidManifest.xml中android:authorities保持一致
 public static final String PATH_USERS = “user”;
 public static final Uri CONTENT_URI = Uri.parse(“content://” + AUTHORITY + “/” + PATH_USERS);
 public static final int USER_INFO = 1;
 private static UriMatcher uriMatcher;
 static {
 uriMatcher = new UriMatcher( UriMatcher.NO_MATCH);
 uriMatcher.addURI( UserInfoContent.AUTHORITY, UserInfoContent.PATH_USERS, USER_INFO);
 }

②自定义ContentProvider
进程间共享数据的本质就是对数据进行增删改查等操作,ContentProvider提供了对应的方法。

以实现一个k-v数据查询为例,首先新建一个类继承自ContentProvider,并重写6个方法:

public class KVDataProvider extends ContentProvider {
 private static final String AUTHORITY = “com.example.contentprovider_KVContentProvider”;
 private static final String PATH = “kvdata”;
 // 创建UriMatcher对象,主要作用是根据 Uri 匹配对应的数据表
 private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
 private static final int CODE_ALL = 1;
 private static final int CODE_STRING = 2;
 static {
 uriMatcher.addURI(AUTHORITY, PATH, CODE_ALL);
 uriMatcher.addURI(AUTHORITY, PATH + “/string”, CODE_STRING);
 uriMatcher.addURI(AUTHORITY, PATH + “/string/*”, CODE_STRING);
 }
 private SharedPreferences sharedPreferences; //使用sp进行数据存储
 //ContentProvider初始化时调用,一般用于创建数据库或升级操作,只有在ContentResolver访问时才会触发onCreate方法。此方法在主线程执行,不能做耗时操作
 @Override
 public boolean onCreate() {
 sharedPreferences = getContext().getSharedPreferences(PATH, Context.MODE_PRIVATE);
 return false; //return true表示初始化成功,false表示失败
 }
 //查询数据,参数uri表示查询具体哪张数据表;projection表示查询表中哪些列,传null则返回所有列;selection表示查询哪些行,传null则返回所有行;sortOrder用于对查询结果进行排序,传null则使用默认的排序方式,也可以是无序的。返回值表示查询的结果,是个Cursor对象,注意取完数据需进行关闭,否则会内存泄露
 @Override
 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
 int code = uriMatcher.match(uri);
 if (code == CODE_STRING) {
 // 用selection 作为sp的key去查询
 String value = sharedPreferences.getString(selection, null);
 MatrixCursor cursor = new MatrixCursor(new String[]{“string”});
 cursor.addRow(new Object[]{value});
 return cursor;
 }
 return null;
 }
 //返回指定的Uri中数据的MIME类型,比如图片、视频等,可直接返回null
 @Override
 public String getType(Uri uri) {
 return null;
 }
 //插入一条新数据,参数uri表示要把数据插入到哪张数据表,返回值表示添加成功后返回这条数据的uri
 @Override
 public Uri insert(Uri uri, ContentValues values) {
 String key = uri.getLastPathSegment();
 String value = values.getAsString( “string”);
 sharedPreferences.edit().putString(key, value).apply();
 return null;
 }
 //删除数据,参数uri表示要删除哪张表的数据,返回值表示被删除的行数
 @Override
 public int delete(Uri uri, String selection, String[] selectionArgs) {
 int code = uriMatcher.match(uri);
 if (code ==CODE_STRING) {
 // 用selection作为sp的key去查询
 sharedPreferences.edit().remove( selection).apply();
 return 1;
 }
 return 0;
 }
 //更改数据,返回值表示更新的行数
 @Override
 public int update(Uri uri, ContentValues values, String selection,String[] selectionArgs){
 Log.d(TAG, “>>>delete<<<”);
 return 0;
 }
 }


注意:
1)onCreate方法在主线程中运行,不能做耗时操作。
2)query、insert、delete、update四个核心方法运行在Binder线程中,并不是主线程。
3)数据访问的方法(如insert和update)可能被多个线程同时调用,此时必须是线程安全的,需要在核心方法中处理同步问题。

自定义ContentProvider完成后,记得在Mainfest文件中声明注册:

(1)authorities是ContetProvider唯一标识;
permission用于声明访问ContentProvider需要的权限,其他app需要访问该ContentProvider时需要声明该权限,否则外界应用会异常终止;
(2)关于权限,可分别声明读写所需不同的权限
android:writePermission=”” 写权限android:readPermission=”” 读权限android:permission=”” 全部权限
(3)外部Activity调用

Uri uri = Uri.parse(“content://com.demo.prog ress”);
 getContentResolver().query(uri, null, null, null, null);

③ContentResolver内容解析器
当其他应用需要对ContentProvider中的数据进行增删改查操作时,可以使用ContentResolver类来完成。
ContentResolver是个抽象类,具体实例在Context中就已经初始化了,也就是说应用一启动就初始化了。可以使用Context提供的getContentResolver()方法来获取该对象。

一般ContentProvider是单例模式,多个其他应用可以通过ContentResolver调用ContentProvider的增删改查操作数据,ContentResolver调用的数据操作会让同一个ContentProvider处理。

ContentResolver提供了与ContentProvider类一样的增删改查方法:
(1)public Uri insert(Uri uri, ContentValues values)
往ContentProvider添加数据。
(2)public int delete(Uri uri, String selection, String[] selectionArgs)
从ContentProvider删除数据。
(3)public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
更新ContentProvider中的数据。
(4)public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
从ContentProvider中获取数据。

ContentResolver里的增删改查是指在当前项目中向另一个项目中的ContentProvider中增删改查一条数据,注意不要弄混淆。

这些方法的第一个参数为Uri,表示要操作的是哪个ContentProvider和对其中的什么数据进行操作。假如给的是 Uri.parse(“content://com.test.provider.personprovider/contact/13”),就会对authority为com.test.provider.personprovider的ContentProvider进行操作,path为contact/13的数据。比如现在另一个app使用ContentResolver去访问刚才自定义的ContentProvider中的数据:
 public final class KVDataQuery {
 private static final String SCHEME = “content://”;
 private static final String AUTHORITY = “com.example.contentprovider_KVContentProvider”;
 private static final String PATH_STRING = “kvdata/string”;
 public static String getData(Context context, String key) {
 Cursor cursor = context.getContentReso lver().query(Uri.parse(SCHEME + AUTHORITY + “/” + PATH_STRING), null, key, null, null);
 if (cursor != null && cursor.getCount()> 0){
 cursor.moveToNext();
 String value = cursor.getString( cursor.getColumnIndex(“string”));
 cursor.close();
 return value;
 }
 return null;
 }
 public static void setData(Context context, String key, String value) {
 ContentValues values = new ContentValues(1);
 values.put(“string”, value);
 context.getContentResolver().insert( Uri.parse(SCHEME + AUTHORITY + “/” + PATH_STRING + “/” + key), values);
 }
 }
 在Activity中执行:
 public class MainActivity extends AppCompatActivity {
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 String value = KVDataQuery.getData(this, “test”);
 Log.d(TAG, “get: key=test value=” + value);
 KVDataQuery.setData(this, “test”, “12345”);
 Log.d(TAG, “set: key=test value=12345”);
 value = KVDataQuery.getData(this, “test”);
 Log.d(TAG, “get: key=test value=” + value);
 }
 }


注意,需要在该app的AndroidManifest.xml文件中增加要访问的ContentProvider所在的包名:

Android跨进程调用服务如何启动 android跨进程共享数据_数据_02


注意:ContentResolver请求被自动转发到合适的内容提供者实例,所以子类不需要担心跨进程调用的细节。

④ContentObserver内容观察者
ContentObserver用于在指定的Uri中的数据发生变化时,可以通知主线程根据需求做出处理。

要实现这样的功能,首先要做的就是注册观察者,这里的注册是在需要监测ContentProvider的应用中进行注册,并不是在ContentProvider中。在ContentProvider中要做的就是当数据变化时进行通知,通知的方法谷歌已经写好,直接调用就行了。

注册内容观察者的方法:
public final void registerContentObserver (Uri uri, boolean notifyForDescendents, ContentObserver observer)
uri:需要观察的Uri;
notifyForDescendents:为true表示以这个Uri开头的所有Uri都会被匹配到;为false表示精确匹配,即只会匹配这个给定的Uri。

既然有registerContentObserver 方法,ContentResolver必然还有一个解注册的方法:
public final void unregisterContentObserver( ContentObserver observer),它的作用就是取消对注册的那个Uri的观察,这里传进去的参数就是在registerContentObserver中传递进去的ContentObserver对象。

还是上面的例子:
在定义ContentProvider的应用程序中,比如在insert或update方法里,数据发生变化时,调用如下代码:
getContext().getContentResolver().notifyChange(uri, null);
用于通知注册过ContentObserver的应用数据已经更新了。

在另一个app中自定义ContentObserver:
 public class MyContentOberserver extends ContentObserver { 
 private Handler handler;public MyContentOberserver(Handler handler) {
 super(handler);
 this.handler=handler;
 }//当指定的Uri中的数据发生变化后,自动回调该方法,此时可以借助构造方法中的Handler对象将这个变化的消息发送给主线程,主线程接收到这个消息后就可以完成相应的操作
 @Override
 public void onChange(boolean selfChange) {
 super.onChange(selfChange);
 Message msg = Message.obtain();
 msg.omg = “数据更新了something”;
 handler.sendMessage(msg);
 }
 }
 在构造方法中接收了Handler,然后当监听到指定的Uri的数据变化时就会通过Handler消息机制发送一条消息。然后注册观察者:
 context.getContentResolver().registerContentObserver(Uri.parse(SCHEME + AUTHORITY + “/” + PATH_STRING), true, new MyContentOberserver(handler));private Handler handler=new Handler(){
 public void handleMessage(Message msg) {
 //当ContentObsever收到数据改变的消息后通过handler做一些操作
 Log.e(TAG, “收到消息:” + msg.obj);
 }
 }

3.ContentProvider call
ContentProvider用于跨进程通信,一般是通过继承ContentProvider并实验增删改查等方法,来让其他进程对本进程的数据进行操作。这是在涉及到数据大的时候这样操作,如果涉及的数据量很小,可以通过重写ContentProvider的call方法来简单实现跨进程通信。

在 API 11 (Android 3.0) 中,ContentProvider加入了call方法,可以用来进行跨进程的方法调用,该方法的定义如下:
Bundle call(String method, String arg, Bundle extras)
利用call方法通过Bundle进行进程间通信,比广播延时低,比AIDL轻量。

在应用A中继承ContentProvider,重写必须实现的方法和call方法:
 public class MyContentProvider extends ContentProvider {
 ……
 @Override
 public Bundle call(String method, String arg, Bundle extras) {
 Log.e(TAG, “call方法:method=” + method + “;arg=” + arg);
 if (“STATUS”.equals(method)) {
 Bundle bundle = new Bundle();
 // put一个boolean值
 bundle.putBoolean(method, true);
 return bundle;
 }
 return null;
 }
 @Override
 public Bundle call(String authority, String method, String arg, Bundle extras) {
 Log.e(TAG, “call方法:method=” + method + “;arg=” + arg);
 return super.call(authority, method, arg, extras);
 }
 }B中实现(调用A的call方法):
 private void update(String status) {
 Bundle bundle = context.getContentResol ver().call(Uri.parse(uri), “STATUS”, null, null);
 Log.e(“调用结果:” + bundle.getBoolean( method)); //通过返回值取出boolean变量知道是否调用成功
 }


应用B通过调用call方法会执行A中call方法里的对应代码,A中的返回值就是B调用call方法的返回值,从而使B获取了A中传过来的数据。

这样B就能触发了A的具体逻辑操作,类似复写,A也可以触发B的操作。

4.总结
整理一下整个操作的运行流程:
首先有两个项目,一个是有ContentProvider的项目A,在这个ContentProvider中初始化了一个持久化存储方式(比如数据库、sp或文件等)并实现其增删改查等方法。
在另一个项目B中通过ContentResolver来操作项目A中的数据,假如要进行insert操作只需调用ContentResolver的insert(uri, ContentValues)将Uri和ContentValues对象经过一系列操作传递到ContentProvider中,然后在ContentProvider会对这个Uri进行匹配,若匹配成功则执行相应的操作,如插入数据、查询数据等。
也就是app B想要操作appA中的数据,因此app A提供一个ContentProvider,在app B中通过ContentResolver的增删改查方法,实际上调用了app A中ContentProvider的增删改查方法,增删改查操作后数据发生了变化,自动回调app B中ContentObserver类中的onChange方法,从而在app B中进行数据变化后的操作。(这一切交互的基础是同一个uri,app A中ContentProvider提供该uri,app B中通过ContentResolver调用增删改查方法时都要传入这个uri,同时注册ContentObserver时也要传入这个uri)

通过一张图再来说一下这个过程:

Android跨进程调用服务如何启动 android跨进程共享数据_应用程序_03


从图中可以看出在OtherApplication中注册了ContentObserver之后,当Application1中的数据库发生变化时,只需要在ContentProvider中调用ContentResolver的notifyChange(Uri, ContentObserver observer),由于在OtherApplication中注册了ContentObserver(注册时用的Uri和ContentProvider中发生变化的Uri一样),因此在ContentObserver中会收到这个变化信息,它就可以将这个消息通过Handler发送给OtherApplication。