Android:IPC(进程间通信)机制

news2025/1/18 17:12:55

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 的进程。因此,是否终止某个进程的决定取决于该进程中所运行组件的状态。

当然采取多进程技术势必会带来一些优缺点:

优点:

  1. 并发性:多进程可以在不同的进程中并行执行任务,提高应用程序的并发性和响应能力。这对于需要处理大量计算密集型或I/O密集型任务的应用程序特别有用。

  2. 安全性:通过将敏感的业务逻辑或数据存储在单独的进程中,可以增强应用程序的安全性。如果一个进程受到攻击或崩溃,其他进程仍然可以正常运行。

  3. 模块化和解耦:多进程可以将应用程序的不同模块或组件分离到不同的进程中,实现模块化和解耦。这使得不同的模块可以独立开发、测试和部署,提高了应用程序的可维护性和可扩展性。

  4. 资源隔离:每个进程都有自己的内存空间,可以避免不同组件之间的内存泄漏或资源争用问题。这可以提高应用程序的稳定性和可靠性。

缺点:

  1. 资源消耗:每个进程都需要占用一定的系统资源,包括内存、CPU和IPC通信开销等。多进程应用程序可能会占用更多的系统资源,影响设备的性能和电池寿命。

  2. 数据共享和同步:不同进程之间的数据共享和同步可能变得复杂。由于进程间通信(IPC)的开销较大,需要谨慎处理数据共享和同步的问题,以避免性能和一致性问题。

  3. 调试和开发复杂性:多进程应用程序的调试和开发相对复杂,需要考虑不同进程之间的通信、数据传输和同步等问题。调试跨进程的问题可能会更加困难。

  4. 上下文切换开销:由于多进程应用程序涉及到进程间的切换,会增加上下文切换的开销。这可能对性能产生一定的影响,特别是在频繁进行进程间通信的情况下。

线程

启动应用时,系统会为该应用创建一个称为“main”(主线程)的执行线程。此线程非常重要,因为其负责将事件分派给相应的界面微件,其中包括绘图事件。此外,应用与 Android 界面工具包组件(来自 android.widget 和 android.view 软件包的组件)也几乎都在该线程中进行交互。因此,主线程有时也称为界面线程。我们也只有在主线程中才可以操纵界面元素!!!

系统不会为每个组件实例创建单独的线程。在同一进程中运行的所有组件均在界面线程中进行实例化,并且对每个组件的系统调用均由该线程进行分派。因此,响应系统回调的方法(例如,报告用户操作的 onKeyDown() 或生命周期回调方法)始终在进程的界面线程中运行。所以说,如果我们的回调方法非常耗时的话(比如onClick方法回调),简易新开启一个子线程去执行回调方法,防止主界面被阻塞而产生ANR(应用无响应)的情况。

多进程模式产生的资源隔离

上面关于多进程模式带来的优缺点中我们提到了资源隔离的优点和数据同步和共享的缺点,实际上这两点都源于一个问题:每个进程所分配的内存空间是独立的。

我们可以回想一下进程间通信时也会出现许多问题,所以我们会用到同步机制来使多进程通信得以正确进行。而多进程通信时产生的问题就更大了,由于每个进程之间的内存空间都是隔离的,因此我们不能直接进行进程间的数据共享,而是需要借助一些手段来实现多进程间的通信。

一般来说,使用多进程会带来以下问题:

  1. 静态成员和单例模式完全失效。
  2. 线程同步机制完全失效。
  3. SharedPreferences的可靠性下降。
  4. 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值自动生成。有以下两点需要特别提醒:

  1. 静态成员变量类不属于对象,所以不会参与序列化过程。
  2. 用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来定义跨进程间通信的接口,然后系统会自动为我们实现抽象类,最后我们只需要继承抽象类并且使用这个类就可以进行进程间通信了。

一般我们可以执行以下步骤:

  1. 创建.aidl文件
  2. 实现接口
  3. 向客户端公开接口

前置信息

我们先了解一些前置知识(截取自官网):

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对象传输,具体来说,我们传输的数据一定要能被序列化,比如:

  1. 基本数据类型
  2. 实现了Parcelable或Serializable接口的对象
  3. 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网络数据交换

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/556624.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(学习日记)AD学习 #2

写在前面&#xff1a; 由于时间的不足与学习的碎片化&#xff0c;写博客变得有些奢侈。 但是对于记录学习&#xff08;忘了以后能快速复习&#xff09;的渴望一天天变得强烈。 既然如此 不如以天为单位&#xff0c;以时间为顺序&#xff0c;仅仅将博客当做一个知识学习的目录&a…

npm ERR! code E404 在vscode安装插件时报错的解决方案

答主在配置commit代码提交规范时【即如下代码】遇到了以下问题 npm i cz-customizable6.3.0 --save-dev 出现了 npm ERR! code E404 npm ERR! 404 Not Found - GET https://registry.npmjs.org/vue%2fvue-loader-v15 - Not found npm ERR! 404 ……等报错情况 解决方案1 检查n…

SVN 导出改动差异文件

文章目录 SVN 导出改动差异文件应用场景/背景介绍具体操作方法 SVN 导出改动差异文件 应用场景/背景介绍 当然下面的两个场景介绍可能用分支管理都会有不错的效果&#xff0c;或者更优&#xff0c;只是记录一下思路&#xff0c;用什么还是看大家个人爱好啦 在开发过程中偶尔会…

nexus私服仓库maven-metadata.xml缺失导致的构建失败或者下载504

环境&#xff1a;maven项目&#xff0c;使用Nexus私服&#xff0c;jenkins实现代码的编译和打包。 问题分析思路&#xff1a;某周末前&#xff0c;jenkins上的编译打包任务一直正常工作&#xff0c;但周末后突然所有项目都编译失败&#xff0c;报错很一致都是Could not find a…

【牛客刷题专栏】0x30:JZ38 字符串的排列(C语言编程题)

前言 个人推荐在牛客网刷题(点击可以跳转)&#xff0c;它登陆后会保存刷题记录进度&#xff0c;重新登录时写过的题目代码不会丢失。个人刷题练习系列专栏&#xff1a;个人CSDN牛客刷题专栏。 题目来自&#xff1a;牛客/题库 / 在线编程 / 剑指offer&#xff1a; 目录 前言问…

局域网与城域网 - ARP 地址解析协议

文章目录 1 概述2 ARP 地址解析协议2.1 工作过程2.2 报文格式2.3 ARP 命令 3 扩展3.1 网工软考真题 1 概述 #mermaid-svg-CQnNvTP8xFoJsztk {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-CQnNvTP8xFoJsztk .error-…

plt、fig、axes、axis的含义

plt import matplotlib.pyplot as plt figure,axes与axis 如果将Matplotlib绘图和我们平常画画相类比&#xff0c;可以把Figure想象成一张纸&#xff08;一般被称之为画布&#xff09;&#xff0c;Axes代表的则是纸中的一片区域&#xff08;当然可以有多个区域&#xff0c;这…

剑指 Offer - 字符串合辑

&#x1f34e;道阻且长&#xff0c;行则将至。&#x1f353; &#x1f33b;算法&#xff0c;不如说它是一种思考方式&#x1f340; 算法专栏&#xff1a; &#x1f449;&#x1f3fb;123 题解目录 一、&#x1f331;[剑指 Offer 05. 替换空格](https://leetcode.cn/problems/t…

[笔记]C++并发编程实战 《二》线程管理

文章目录 前言第2章 线程管理2.1 线程管理的基础2.1.1 启动线程2.1.2 等待线程完成2.1.3 特殊情况下的等待2.1.4 后台运行线程2.2 向线程函数传递参数 前言 第2章 线程管理 本章主要内容 启动新线程等待线程与分离线程线程唯一标识符 好的&#xff01;看来你已经决定使用多…

使用压缩包安装jdk多版本并能领过切换

使用压缩包安装jdk多版本并能领过切换 1.下载2.解压包到指定位置3.使用pdate-alternatives 进行版本切换管理3.1. jdk173.2. jdk1.8 3.切换版本4.解决JAVA_HOME环境变量识别的问题 1.下载 官网的下载地址&#xff1a; 下载地址&#xff1a; jdk17: jdk1.8在当前页面的下面: …

基于差分进化算法的微电网调度研究(Matlab代码实现)​

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

MAC突然打不开Notion,你遇到过这个问题吗?

目录 解决办法 为什么Notion会突然打不开呢&#xff1f; Notion是一款适合记录/规划的应用&#xff0c;而且页面简洁&#xff0c;模板强大&#xff0c;深得大家喜爱。我也经常在Notion上制定计划、记录学习笔记等。不过&#xff0c;今天突然打不开了&#xff0c;网页版、本地…

基于SpringBoot的生鲜管理系统的设计与实现

背景 困扰交易市场的许多问题当中,生鲜交易管理一定是交易市场不敢忽视的一块。但是管理好生鲜交易又面临很多麻烦需要解决,例如有几个方面:第一,生鲜市场往往人数都比较多,如何保证能够管理到每一个商家,如何在工作琐碎,记录繁多的情况下将生鲜交易的当前情况反应给领导相关部…

【大数据】Hadoop高可用集群搭建

知识目录 一、写在前面&#x1f495;二、Zookeeper安装✨三、Hadoop配置✨四、Hadoop HA自动模式✨五、HA脚本分享✨七、结语&#x1f495; 一、写在前面&#x1f495; 大家好&#xff01;这篇文章是我在搭建Hdfs的HA(高可用)时写下的详细笔记与感想&#xff0c;希望能帮助到大…

分布式调度XXL-JOB

分布式调度XXL-JOB 1.概述 1.1什么是任务调度 比如: 某电商平台需要每天上午10点&#xff0c;下午3点&#xff0c;晚上8点发放一批优惠券某银行系统需要在信用卡到期还款日的前三天进行短信提醒某财务系统需要在每天凌晨0:10分结算前一天的财务数据&#xff0c;统计汇总 以…

【图床】SpringBoot上传图片

知识目录 一、写在前面✨二、新建开源仓库✨2.1 新建仓库2.2 将仓库设置为开源2.3 生产私人令牌 三、代码实现&#x1f604;3.1 工具类3.2 上传图片 四、总结撒花&#x1f60a; 一、写在前面✨ 大家好&#xff01;我是初心&#xff0c;很高兴再次和大家见面。 今天跟大家分享…

【Unity】Animation Playable Bug、限制及解决方案汇总

【Unity】Animation Playable Bug、限制及解决方案汇总 先自荐一下我的PlayableGraph监控工具&#xff0c;比官方的Visualizer好用得多&#xff1a;https://github.com/SolarianZ/UnityPlayableGraphMonitorTool Bug 文中提及的各项Bug及解决方案的最小化测试工程可在此仓库下…

基于java的竞赛预约管理信息系统的设计与实现

背景 本系统提供给管理员对首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;项目分类管理&#xff0c;竞赛项目管理&#xff0c;赛事预约管理&#xff0c;系统管理等诸多功能进行管理。本系统对于用户输入的任何信息都进行了一定的验证&#xff0c;为管理员操作提高…

2023语言与智能技术竞赛开辟“双赛道”:寻找“全民测评官”,探索AI多模态能力...

开年以来&#xff0c;人工智能大语言模型&#xff08;LLM&#xff09;掀起新一轮全球科技竞赛&#xff0c;全球科技巨头打响“百模大战”。当大语言模型正深刻改变人类生产生活方式时&#xff0c;该如何进一步释放其潜能&#xff0c;成为业界关注的问题&#xff0c;也成为了202…

计网之HTTP请求的构造

文章目录 1. form表单请求构造2. ajax请求构造3. Postman的简单使用 常见的构造 HTTP 请求的方式有以下几种: 直接通过浏览器地址栏, 输入一个 URL 就可以构造出一个 GET 请求.直接点击收藏夹, 得到的也是 GET 请求.HTML 中的一些特殊标签也会触发 GET 请求, 如: link, script…