Android:IPC(进程间通信)机制
进程和线程
我们先来了解一些关于线程和进程基本的概念。
按照操作系统中的描述,线程是CPU调度的最小单元,同时线程是一种有限的系统资源。而进程一般指一个执行单元,在PC和移动设备上指一个程序或者应用。一个进程可以包含多个线程,因此进程和线程是包含与被包含的关系。
在Android系统上可以这样理解:系统管理进程,进程管理线程。
Android系统中的进程和线程
接下来我们需要了解在Android系统上进程和线程的一些基本规则。
概况
当应用组件启动且该应用未运行任何其他组件时,Android 系统会使用单个执行线程为应用启动新的 Linux 进程。默认情况下,同一应用的所有组件会在相同的进程和线程(称为“主”线程)中运行。如果某个应用组件启动且该应用已存在进程(因为存在该应用的其他组件),则该组件会在此进程内启动并使用相同的执行线程。但是,我们可以安排应用中的其他组件在单独的进程中运行,并为任何进程创建额外的线程。
进程
一般情况下,一个APP中只会存在一个进程,所有的组件都将运行在这个线程中。但是我们也可以在一个应用中创建多个进程,比如可以在manifest清单文件中声明process属性,该属性可以指定该组件在哪个进程中运行:
<service
android:process="com.myService"
android:name=".services.mService"
android:enabled="true"
android:exported="true">
</service>
这样就设置我们的Service组件将运行在com.myService进程中而不是默认进程中,这里需要提醒我们设置process属性时有一定规范,就是需要一定至少需要一个小数点分割,比如XX.XX。
此外,< application > 元素还支持 android:process 属性,用来设置适用于所有组件的默认值。
当内存不足,而其他更急于为用户提供服务的进程又需要内存时,Android 可能会决定在某一时刻关闭某个进程。正因如此,系统会销毁在被终止进程中运行的应用组件。当这些组件需再次运行时,系统将为其重启进程。
决定终止哪个进程时,Android 系统会权衡其对用户的相对重要性。例如,相较于托管可见 Activity 的进程而言,系统更有可能关闭托管屏幕上不再可见的 Activity 的进程。因此,是否终止某个进程的决定取决于该进程中所运行组件的状态。
当然采取多进程技术势必会带来一些优缺点:
优点:
-
并发性:多进程可以在不同的进程中并行执行任务,提高应用程序的并发性和响应能力。这对于需要处理大量计算密集型或I/O密集型任务的应用程序特别有用。
-
安全性:通过将敏感的业务逻辑或数据存储在单独的进程中,可以增强应用程序的安全性。如果一个进程受到攻击或崩溃,其他进程仍然可以正常运行。
-
模块化和解耦:多进程可以将应用程序的不同模块或组件分离到不同的进程中,实现模块化和解耦。这使得不同的模块可以独立开发、测试和部署,提高了应用程序的可维护性和可扩展性。
-
资源隔离:每个进程都有自己的内存空间,可以避免不同组件之间的内存泄漏或资源争用问题。这可以提高应用程序的稳定性和可靠性。
缺点:
-
资源消耗:每个进程都需要占用一定的系统资源,包括内存、CPU和IPC通信开销等。多进程应用程序可能会占用更多的系统资源,影响设备的性能和电池寿命。
-
数据共享和同步:不同进程之间的数据共享和同步可能变得复杂。由于进程间通信(IPC)的开销较大,需要谨慎处理数据共享和同步的问题,以避免性能和一致性问题。
-
调试和开发复杂性:多进程应用程序的调试和开发相对复杂,需要考虑不同进程之间的通信、数据传输和同步等问题。调试跨进程的问题可能会更加困难。
-
上下文切换开销:由于多进程应用程序涉及到进程间的切换,会增加上下文切换的开销。这可能对性能产生一定的影响,特别是在频繁进行进程间通信的情况下。
线程
启动应用时,系统会为该应用创建一个称为“main”(主线程)的执行线程。此线程非常重要,因为其负责将事件分派给相应的界面微件,其中包括绘图事件。此外,应用与 Android 界面工具包组件(来自 android.widget 和 android.view 软件包的组件)也几乎都在该线程中进行交互。因此,主线程有时也称为界面线程。我们也只有在主线程中才可以操纵界面元素!!!
系统不会为每个组件实例创建单独的线程。在同一进程中运行的所有组件均在界面线程中进行实例化,并且对每个组件的系统调用均由该线程进行分派。因此,响应系统回调的方法(例如,报告用户操作的 onKeyDown() 或生命周期回调方法)始终在进程的界面线程中运行。所以说,如果我们的回调方法非常耗时的话(比如onClick方法回调),简易新开启一个子线程去执行回调方法,防止主界面被阻塞而产生ANR(应用无响应)的情况。
多进程模式产生的资源隔离
上面关于多进程模式带来的优缺点中我们提到了资源隔离的优点和数据同步和共享的缺点,实际上这两点都源于一个问题:每个进程所分配的内存空间是独立的。
我们可以回想一下进程间通信时也会出现许多问题,所以我们会用到同步机制来使多进程通信得以正确进行。而多进程通信时产生的问题就更大了,由于每个进程之间的内存空间都是隔离的,因此我们不能直接进行进程间的数据共享,而是需要借助一些手段来实现多进程间的通信。
一般来说,使用多进程会带来以下问题:
- 静态成员和单例模式完全失效。
- 线程同步机制完全失效。
- SharedPreferences的可靠性下降。
- Application会多次创建。
Serializable接口和Parcelable接口
在正式介绍进程间通信的手段时,我们需要先了解如何将一个对象进行序列化和反序列化。什么是序列化呢?我的理解就是将一个数据实体转化为字节流,方便我们进行存储或者传输。同理,反序列化就是将字节流重新转化成一个数据实体。相信用C语言实现过管理系统的读者应该会有所体会,需要制定怎样的规则将数据实体表示成一串数据存储到本地这个问题,序列化正是为我们解决了这个问题。
在这里我们介绍两种序列化和反序列化的手段:实现Serializable接口和Parcelable接口。
Serializable接口
Serializable接口是Java中原生提供的一个序列化接口,为对象提供标准的序列化和反序列化方法。实现Serializable接口的方法很简单,我们只要在类的声明里名为serialVersionUID的标识即可:
public class User implements Serializable{
private static final long serialVersionUID = 519067123721295773L;
public User(int userId,String userName,boolean isMale){
this.userId = userId;
this.userName = userName;
this.isMale = isMale;
}
public int userId;
public String userName;
public boolean isMale;
}
接下来,我们就可以用输出输入流直接把这个对象输出或者输入了:
//输出对象到本地--序列化
public void onClick(View v) {
try {
User user = new User(0,"XuMingYang",true);
File file = new File(getExternalFilesDir(null),"myCache.txt");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(user);
out.close();
}catch (Exception e){
e.printStackTrace();
}
}
//从本地读取对象--反序列化
public void onClick(View v) {
try {
File file = new File(getExternalFilesDir(null),"myCache.txt");
FileInputStream fis = new FileInputStream(file);
ObjectInputStream inputStream = new ObjectInputStream(fis);
User newUser = (User) inputStream.readObject();
if(newUser != null){
Toast.makeText(MainActivity.this, newUser.userName, Toast.LENGTH_SHORT).show();
}
inputStream.close();
}catch (Exception e){
e.printStackTrace();
}
}
一般来说,我们应该去手动指定serialVersionUID的值,不过也可以让编译器借助Hash值自动生成。有以下两点需要特别提醒:
- 静态成员变量类不属于对象,所以不会参与序列化过程。
- 用transient关键字标记的成员变量不参与序列化过程。
Parcelable
接下来介绍的就是Android特有的序列化接口了。只要实现了Parcelable接口,一个类的对象就可以实现序列化并可以通过Intent与Binder传递,接下来我们来实现这个接口:
public class User implements Serializable, Parcelable {
private static final long serialVersionUID = 519067123721295773L;
public User(int userId,String userName,boolean isMale){
this.userId = userId;
this.userName = userName;
this.isMale = isMale;
}
public int userId;
public String userName;
public boolean isMale;
protected User(Parcel in) {
userId = in.readInt();
userName = in.readString();
isMale = in.readInt() == 1;
//book = in.readParcelable(Thread.currentThread().getContextClassLoader());
}
//这个方法配合上面那个User(Parcel in) 实现反序列化
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override //这个方法实现序列化
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(userId);
dest.writeString(userName);
dest.writeInt(isMale ? 1 : 0);
//dest.writeParcelable(book,0);
}
}
我们需要实现三个方法和一个CREATOR,我们按照顺序来,首先是writeToParcel方法,该方法是用于将对象进行序列化的。可以看到,在这个方法中我们利用Parcel对象分别将User的I的,name和isMale这三个基本成员变量写入到了Parcel中,实现了序列化。
接下来是describeContents方法,这个方法一般情况下我们返回0即可。
第三个方法与第一个writeToParcel方法对应,第一个方法是实现序列化,那么这第三个方法就是实现反序列化,将之前写入Parcel中的数据依次读出并赋值给成员变量。这里我们需要注意一点,那就是第一个方法写参数的顺序一定要和第三个读参数的顺序一致!!
比如说,我们在第一个方法是先写useId,然后userName,最后写isMale的,那我们在第三个方法读取的时候就要先读useId,然后读userName,最后读isMale。要遵循顺序一致,否则就会报错。
最后我们要实现一个CREATOR对象,CREATOR是配合第一个构造方法来实现反序列化的,这个比较简单,直接看实例即可。
一些限制:
在官网的说明中,提到了一些限制:
Parcel 不是通用序列化机制,您绝不能将任何 Parcel 数据存储在磁盘上或通过网络发送。
Binder 事务缓冲区的大小固定有限,目前为 1MB,由进程中正在处理的所有事务共享。由于此限制是进程级别而不是 Activity 级别的限制,因此这些事务包括应用中的所有 binder 事务,例如 onSaveInstanceState,startActivity 以及与系统的任何互动。超过大小限制时,将引发 TransactionTooLargeException。
对于 savedInstanceState 的具体情况,应将数据量保持在较小的规模,因为只要用户可以返回到该 Activity,系统进程就需要保留所提供的数据(即使 Activity 的进程已终止)。我们建议您将保存的状态保持在 50k 数据以下。
进程间通信
这里我们介绍四种方法来实现IPC,分别是使用AIDL,Bundle传输,使用文件共享和使用ContentProvider。
使用AIDL
AIDL全称Android Interface Define Language(Android接口定义语言),利用它定义客户端与服务均认可的编程接口,以便二者使用进程间通信 (IPC) 进行相互通信。
简而言之,我们可以通过AIDL来定义跨进程间通信的接口,然后系统会自动为我们实现抽象类,最后我们只需要继承抽象类并且使用这个类就可以进行进程间通信了。
一般我们可以执行以下步骤:
- 创建.aidl文件
- 实现接口
- 向客户端公开接口
前置信息
我们先了解一些前置知识(截取自官网):
AIDL 使用一种简单语法,允许您通过一个或多个方法(可接收参数和返回值)来声明接口。参数和返回值可为任意类型,甚至是 AIDL 生成的其他接口。
您必须使用 Java 编程语言构建 .aidl 文件。每个 .aidl 文件均须定义单个接口,并且只需要接口声明和方法签名。
默认情况下,AIDL 支持下列数据类型:
- Java 编程语言中的所有原语类型(如 int、long、char、boolean 等)
- String
- CharSequence
- List
List 中的所有元素必须是以上列表中支持的数据类型,或者您所声明的由 AIDL 生成的其他接口或 Parcelable 类型。您可选择将 List 用作“泛型”类(例如,List< String>)。尽管生成的方法旨在使用 List 接口,但另一方实际接收的具体类始终是 ArrayList。 - Map
Map 中的所有元素必须是以上列表中支持的数据类型,或者您所声明的由 AIDL 生成的其他接口或 Parcelable 类型。不支持泛型 Map(如 Map<String,Integer> 形式的 Map)。尽管生成的方法旨在使用 Map 接口,但另一方实际接收的具体类始终是 HashMap。
即使您在与接口相同的包内定义上方未列出的附加类型,亦须为其各自加入一条 import 语句。
定义服务接口时,请注意:
方法可带零个或多个参数,返回值或空值。
所有非原语参数均需要指示数据走向的方向标记。这类标记可以是 in、out 或 inout(见下方示例)。
原语默认为 in,不能是其他方向。
注意:您应将方向限定为真正需要的方向,因为编组参数的开销较大。
生成的 IBinder 接口内包含 .aidl 文件中的所有代码注释(import 和 package 语句之前的注释除外)。
您可以在 ADL 接口中定义 String 常量和 int 字符串常量。例如:const int VERSION = 1;。
方法调用由 transact() 代码分派,该代码通常基于接口中的方法索引。由于这会增加版本控制的难度,因此您可以向方法手动配置事务代码:void method() = 10;。
使用 @nullable 注释可空参数或返回类型。
创建.aidl文件
首先我们需要在main目录下新建一个与Java文件夹同级的aidl文件夹。在Android Studio中,我们可以很方便地创建该文件夹:
然后,在弹出的界面中选择aidl即可:
这里我们为了演示更多特性,我们先创建一个实现Parcelable接口的实体类,而且这个实体类所在的包的结构要和aidl中的结构一致,如图:
首先我们在java目录下新建这个类:
//以下是java目录下的实体类
package com.example.ipcdemo2.aidl;
public class Book implements Parcelable {
public String BookName;
public String Author;
public Book(String BookName,String Author){
this.BookName = BookName;
this.Author = Author;
}
protected Book(Parcel in) {
BookName = in.readString();
Author = in.readString();
}
public static final Creator<Book> CREATOR = new Creator<Book>() {
@Override
public Book createFromParcel(Parcel in) {
return new Book(in);
}
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString(BookName);
dest.writeString(Author);
}
}
然后我们在aidl目录中声明这个类:
package com.example.ipcdemo2.aidl;
parcelable Book;
这样就相当于是声明了Book类可以通过AIDL使用。我们之后就会用这个类进行进程间通信的演示。
声明了实体类之后我们来声明我们的接口类,在aidl目录下直接创建:
package com.example.ipcdemo2.aidl;
import com.example.ipcdemo2.aidl.Book;
interface BookManager {
void addBook(in Book book);
List<Book> getBookList();
}
第一个方法是添加图书,第二个方法是获取图书列表
需要注意的是虽然Book和BookManager接口位于同一包中,我们仍然需要导入类。接下来我们点击build Project,系统就会自动为我们生成抽象类。
实现接口
当我们构建应用时,Android SDK 工具会生成以 .aidl 文件命名的 .java 接口文件。生成的接口包含一个名为 Stub 的子类(例如,BookManager.Stub),该子类是其父接口的抽象实现,并且会声明 .aidl 文件中的所有方法。
实际上,我们也可以在gen目录中找到这个抽象类:
感兴趣我们也可以自己点进去看一下,这里我们就不深入了解这个中间过程了,接下来我们直接实现这个抽象类:
private class mBinder extends BookManager.Stub{
@Override
public void addBook(Book book) throws RemoteException {
Log.d(TAG, "addBook currentThread:"+Thread.currentThread().getName());
if(book != null){
mList.add(book);
Message message = mHandler.obtainMessage();
message.what = UPDATE;
Bundle mBundle = new Bundle(); //Parcelable 先放 Bundle
mBundle.putParcelable("newBook",book);//Bundle 再放 Message
message.setData(mBundle);
mHandler.sendMessage(message);
}
}
@Override
public List<Book> getBookList() throws RemoteException {
return mList;
}
}
这里我们在Service里实现了抽象类,我们就可以借助这个mBinder类实现进程间通信了,下面是完整的Service代码:
public class BookManagerService extends Service {
private static final String TAG = "BookManagerService";
public static final int UPDATE = 1;
private String ErrorTest = null;
private CopyOnWriteArrayList<Book> mList = new CopyOnWriteArrayList<>();//支持并发读写的ArrayList
private HandlerThread WorkThread = new HandlerThread("BookManagerService's Thread",
Process.THREAD_PRIORITY_BACKGROUND);
private Looper mLooper ;
private mHandler mHandler;
private class mHandler extends Handler{
public mHandler(Looper mLooper) {
super(mLooper);
}
@Override
public void handleMessage(@NonNull Message msg) {
switch (msg.what){
case UPDATE:{
try {
Book book = msg.getData().getParcelable("newBook");
return;
} catch (RemoteException e) {
Log.d(TAG, "处理信息时出错!");
throw new RuntimeException(e);
}
}
default:
break;
}
}
};
public BookManagerService() {
}
@Override
public void onCreate() {
WorkThread.start();
Log.d(TAG, "onCreate: currentThread = " + Thread.currentThread().getName());
mLooper = WorkThread.getLooper();
mHandler = new mHandler(mLooper);
}
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG, "onBind currentThread:"+Thread.currentThread().getName());
return mBinder;
}
@Override
public boolean onUnbind(Intent intent) {
Log.d(TAG, "onUnbind: ");
return super.onUnbind(intent);
}
@Override
public void onDestroy() {
Log.d(TAG, "服务终止");
super.onDestroy();
}
private mBinder mBinder = new mBinder();
private class mBinder extends BookManager.Stub{
@Override
public void addBook(Book book) throws RemoteException {
Log.d(TAG, "addBook currentThread:"+Thread.currentThread().getName());
if(book != null){
mList.add(book);
Message message = mHandler.obtainMessage();
message.what = UPDATE;
Bundle mBundle = new Bundle(); //Parcelable 先放 Bundle
mBundle.putParcelable("newBook",book);//Bundle 再放 Message
message.setData(mBundle);
mHandler.sendMessage(message);
}
}
@Override
public List<Book> getBookList() throws RemoteException {
return mList;
}
}
向客户端公开接口
接下来只要通过service组件的onbind方法返回实现的抽象类即可,我们查看客户端是如何处理这个binder的:
private class mServiceConnection implements ServiceConnection{
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
manager = BookManager.Stub.asInterface(service);
try {
service.linkToDeath(mDeathPro,0); //设置一个死亡代理
} catch (RemoteException e) {
throw new RuntimeException(e);
}
Toast.makeText(MainActivity.this, "绑定成功", Toast.LENGTH_SHORT).show();
}
@Override
public void onServiceDisconnected(ComponentName name) {
Toast.makeText(MainActivity.this, "连接已丢失", Toast.LENGTH_SHORT).show();
Log.d(TAG, "onServiceDisconnected: 连接已丢失");
}
}
这里注意,我们要想使用这个binder一定要调用Stub的asInterface方法,这个方法会根据调用方是不是和服务端处于同一个进程而决定是否使用远程代理。然后我们就可以用获取的Binder进行通信了:
binding.btGetList.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
try {
manager.getBookList();
} catch (RemoteException e) {
e.printStackTrace();
}
}
}).start();
}
});
这样点击按钮之后处于myService进程的service就可以和处于默认进程中的Activity进行通信了。这里仅放出了部分代码,完整代码可以查看我的➡github
Bundle传输
我们可以使用Bundle附加我们需要传输给远程进程的信息并通过Intent发送出去,比如:
Bundle bundle = new Bundle();
bundle.putParcelable("book",new Book("dd","dd"));
Intent intent = new Intent(MainActivity.this,SecondActivity.class);
startActivity(intent);
我们可以这样通过Intent附加Bundle,Bundle中附加Parcelable对象,发送给处于不同进程中的SecondActivity,然后在SecondActivity的Intent中读取信息:
Intent intent = getIntent();
Book book = intent.getParcelableExtra("book");
当然,除了使用Parcelable以外,我们也可以使用Serializable对象传输,具体来说,我们传输的数据一定要能被序列化,比如:
- 基本数据类型
- 实现了Parcelable或Serializable接口的对象
- Android支持的特殊对象
使用文件共享
这个原理很简单,就是将要共享的数据通过序列化存到本地,然后另一个进程在去读取本地上的文件进行反序列化。
我们知道,在Window上,一个文件如果被加了排斥锁,会导致其他进程无法对其进行访问,包括读写。但是Android系统是基于Linux的,使得并发读写可以没有限制的进行,尽管这可能会出现一些问题,但还是为文件共享进行进程间通信提供了一个途径。这个例子实际上我们之前已经提到过了,就是用Serializable接口进行序列化存储然后反序列化读取,这个例子比较简单,直接放在我的👆我的github⭐
使用ContentProvider
ContentProvider是Android中提供的专门用于不同应用间进行数据共享的方式,它天生适合进行进程间通信,实际上这所有的IPC都是通过Binder来实现的,ContentProvider也不例外,不过本篇文章不是专门介绍ContentProvider和Binder的,这里就介绍Room数据库结合ContentProvider进行进程间通信。
关于Room数据库的使用,可以结合我的这篇文章Room数据库的使用;
这里就直接给出Room数据库的三板斧内容:
Book表:
@Entity(tableName = "Books")
public class Book implements Parcelable {
@PrimaryKey(autoGenerate = true) //定义主键
public int id;
@ColumnInfo(name = "uid")
public int uid;
@ColumnInfo(name = "name")
public String name;
public int getId(){
return id;
}
public Book(int uid,String name){
this.uid = uid;
this.name = name;
}
protected Book(Parcel in) {
id = in.readInt();
uid = in.readInt();
name = in.readString();
}
public static final Creator<Book> CREATOR = new Creator<Book>() {
@Override
public Book createFromParcel(Parcel in) {
return new Book(in);
}
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(id);
dest.writeInt(uid);
dest.writeString(name);
}
}
User表:
@Entity(tableName = "Users")
public class User implements Parcelable {
@PrimaryKey(autoGenerate = true)
public int id;
@ColumnInfo(name = "uid")
public int uid;
@ColumnInfo(name = "name")
public String name;
@ColumnInfo(name = "isMale")
public boolean isMaile;
public User(int uid, String name, boolean isMaile) {
this.uid = uid;
this.name = name;
this.isMaile = isMaile;
}
protected User(Parcel in) {
id = in.readInt();
uid = in.readInt();
name = in.readString();
isMaile = in.readByte() != 0;
}
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(id);
dest.writeInt(uid);
dest.writeString(name);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
dest.writeBoolean(isMaile);
}
}
}
数据访问类Dao:
@Dao
public interface DataBaseDao {
@Insert
public void insertUser(User... users);
@Insert
public void insertBook(Book... books);
@Update
public void updateUser(User... users);
@Update
public void updateBook(Book... books);
@Delete
public void deleteUser(User... users);
@Delete
public void deleteBook(Book... books);
@Query("SELECT * FROM Users")
public List<User> loadAllUsers();
@Query("SELECT * FROM Users")
public Cursor loadAllUsersWithCursor();
@Query("SELECT * FROM Books")
public Cursor loadAllBooksWithCursor();
@Query("SELECT * FROM Books")
public List<Book> loadAllBooks();
}
数据库类:
@Database(entities = {Book.class,User.class},version = 2)
public abstract class DemoDataBase extends RoomDatabase {
public abstract DataBaseDao getMyDao();
}
接着我们创建ContentProvider类并在manifest中声明:
<provider
android:process=":provider"
android:permission="com.example.PROVIDER"
android:name=".cps.BookProvider"
android:authorities="com.example.cps.BookProvider"
android:enabled="true"
android:exported="true">
</provider>
ContentProvider类:
public class BookProvider extends ContentProvider {
private static final String TAG = "BookProvider";
public static final String AUTHORITY = "com.example.cps.BookProvider";
public static final Uri BOOK_CONTENT_URI = Uri.parse("content://"+AUTHORITY
+"/book");
public static final Uri USER_CONTENT_URI = Uri.parse("content://"+AUTHORITY
+"/user");
public static final int BOOK_URI_CODE = 0;
public static final int USER_URI_CODE = 1;
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);//用于匹配URI的工具类
static{
sUriMatcher.addURI(AUTHORITY,"book",BOOK_URI_CODE);//说明AUTHORITY+"/book"的组合uri将会返回BOOK_URI_CODE的值
sUriMatcher.addURI(AUTHORITY,"user",USER_URI_CODE);
}
public DemoDataBase mDataBase ; //数据库
public DataBaseDao mDao;//数据访问接口
public BookProvider() {
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
//就删除最后一条
Log.d(TAG, "delete");
String tableName = getTableName(uri);
if(tableName == null){
Log.d(TAG, "delete: null table");
throw new IllegalArgumentException("Unsupported URI:"+uri);
}
int length = mDao.loadAllBooks().size();
if(length <= 0){
Log.d(TAG, "Error !! delete: "+"Length is :"+length);
}
Log.d(TAG, "delete: length is + "+length);
List<Book> books = mDao.loadAllBooks();
List<User> users = mDao.loadAllUsers();
for(Book book:books){
Log.d(TAG, "item: +"+book.getId());
}
mDao.deleteUser(users.get(length-1));
mDao.deleteBook(books.get(length-1));
return 1;
}
@Override
public String getType(Uri uri) {
Log.d(TAG, "getType");
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
Log.d(TAG, "insert");
String tableName = getTableName(uri);
if(tableName == null){
throw new IllegalArgumentException("Unsupported URI:"+uri);
}
if(tableName.equals("books")){
Book book = new Book(values.getAsInteger("book_id"),values.getAsString("book_name"));
mDao.insertBook(book);
}else{
User user = new User(values.getAsInteger("user_id"), values.getAsString("user_name"),
values.getAsBoolean("user_isMale"));
mDao.insertUser(user);
}
return uri;
}
@Override
public boolean onCreate() {
Log.d(TAG, "onCreate, currentThread is :"+Thread.currentThread().getName());
//初始化Room数据库 和 数据访问接口
mDataBase = Room.databaseBuilder(getContext(),DemoDataBase.class,"DemoData5").build();
mDao = mDataBase.getMyDao();
return true;
}
@Override //这是一个比较死的查找方法,要想更加灵活的话可以修改Dao中的Query语句并配置参数
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
Log.d(TAG, "query, currentThread is :"+Thread.currentThread().getName());
String tableName = getTableName(uri);
if(tableName == null){
throw new IllegalArgumentException("Unsupported URI:"+uri);
}
Cursor cursor = null;
if(selection == null){
if(tableName.equals("books")){
try{
cursor = mDao.loadAllBooksWithCursor();
}catch (Exception e){
e.printStackTrace();
}
}else {
try{
cursor = mDao.loadAllUsersWithCursor();
}catch (Exception e){
e.printStackTrace();
}
}
}
return cursor;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
Log.d(TAG, "update");
return 0;
}
private String getTableName(Uri uri){
String tableName = null;
switch (sUriMatcher.match(uri)){
case BOOK_URI_CODE:
tableName = "books";
break;
case USER_URI_CODE:
tableName = "users";
break;
default:
break;
}
return tableName;
}
}
我们在这里通过Room数据库在ContentProvider类里实现了基本的增删改查的功能。
总结:
本篇文章里我们介绍了如何实现IPC,主要有使用AIDL,Bundle传输,使用文件共享和使用ContentProvider四种方法。实际上还有使用Messenger,Socket套接字等方法也可以实现IPC,下面总结一下IPC方式的优缺点和适用场景:
名称 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Bundle | 简单易用 | 只能传输Bundle支持的数据类型 | 四大组件间的进程间通信 |
文件共享 | 简单易用 | 不适合高并发场景,并且无法做到进程间的即时通信 | 无并发访问情形,交换简单的数据实时性不高的场景 |
Messenger | 功能一般,支持一对多串行通信,支持实时通信 | 不能很好地处理高并发情形,不支持RPC,数据通过Message传输,因此只能传输Bundle支持的数据类型 | 低并发的一对多即时通信,无RPC需求,或者无需要返回结果的RPC需求 |
AIDL | 功能强大,支持一对多并发通信,支持实时通信 | 使用稍复杂,需要处理好线程同步 | 一对多通信且有RPC需求 |
ContentProvider | 在数据源访问方面功能强大,支持一对多并发数据共享,可通过Call方法拓展其他操作 | 可以理解为受约束的AIDL,主要提供数据源的CRUD操作 | 一对多的进程间数据共享 |
Socket | 功能强大,可以通过网络传输字节流,支持一对多并发实时通信 | 实现细节稍微有点繁琐,不支持直接的RPC | 网络数据交换 |