android的IPC机制

本文深入探讨了Android中的进程间通信(IPC)机制,介绍了IPC的基本概念及其在Android中的实现方式,如Binder、Socket、Bundle等,并分析了多进程带来的挑战。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


IPC不是android中独有的,任何一个操作系统都需要相应的IPC机制,比如Window上可以通过剪贴板,管道和邮槽来进行进程间通信;Linux上可以通过命名管道、共享内存、信号量等进行进程间通信,可以看到不同的操作系统平台有着不同的进程间通信方式。对于Android来说,它是一种基于Linux内核的移动操作系统,它的进程间通信方式不能完全继承自LInux,相反,它有自己的进程间通信方式,最有特色的进程通信方式就是Binder了,通过Binder可以轻松实现进程间通信,除了Binder,Android还支持Socket,通过Socket也可以实现任意两个终端之间的通信,当然,一个设备上的两个进程通过Socket通信自然也是可以的。

IPC是Inter-Process Communication的缩写,含义是进程间通信或者跨进程通信,具体指两个进程之间进行数据交换的过程。首先得要了解什么事进程,什么事线程。进程和线程是截然不同的概念。

线程是cpu调度的最小单元,同时线程是一种有限的系统资源。进程一般指一个执行单元,在PC和移动设备上指一个程序或者应用。一个进程可以包含多个线程,因此进程和线程是包含与被包含的关系。在最简单的情况下,一个进程可以只有一个线程,即主线程,在Android里面的主线程也叫UI线程,在UI线程里才能操作界面元素。很多时候,一个进程中需要执行大量的耗时任务,如果这些任务放在主线程中去执行就会造成界面无响应,严重影响用户体验,这种情况在PC系统和移动系统中都存在,在Android中有特殊的名字叫做ANR,即应用无响应,解决这个问题就需要用到线程,把一些耗时的任务放在线程中。

Android中的多进程

如何在Android开启多进程呢?首先,在Android中使用多进程只有一种方法,那就是给四大组件(Activity、Service、Receiver、ContentProvider)在AndroidMenifest中指定android:process属性,除此之外没有其他办法,也就是我们无法给一个线程或者一个实体类指定其运行时所在的进程。其实还有另一种非常规的多进程方法,那就是通过JNI在native层去fork一个新的进程,但是这种属于特殊情况,也不是常用的创建多进程的方式。

<activity  

     android:name="com.yy.demo.SecondActivity"

     android:configChanges="screenLayout"

     android:label="@string/app_name"

     android:process=":remote" />


<activity

     android:name="com.yy.demo.ThridActivity"

     android:configChanges="screenLyout"

     android:label="@string/app_name"

     android:process="com.yy.demo.remote"/>


上面的代码分别为SecondActivity和ThirdActivity指定了process属性,并且它们的属性值不同,这意味着当前应用又增加了两个新进程。其中注意到SecondActivity和ThirdActivity的android:process属性分别为“:remote”和“com.yy.demo.remote”.这两种方式是有区别的,首先“:”的含义是指在当前的进程名前面附加上当前的包名,这是一种简写的方法,对于SecondActivity来说,它完整的进程名为com.yy.demo.remote,这是一种完整的命名方式,不会附加包名信息;其次,进程名以:“:”开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,而进程名不以“:”开头的进程属于全局进程,其他应用通过ShareUID方式和它跑在同一个进程中。

Android系统会为每个应用分配一个唯一的UID,具有相同UID的应用才能共享数据,两个应用通过·ShareUID跑在同一个进程是有要求的,需要两个应用有相同的ShareUID并且签名才可以。在这种情况下,它们可以互相访问对方的私有数据,比如data目录、组件信息,还可以共享内存数据,看起来就像是一个应用的两个部分。

所有运行在不同进程中的四大组件,只要它们之间需要通过内存来共享数据,都会共享失败,这也是多进程所带来的主要影响。正常情况下四大组件中间不可能不通过一些中间层来共享数据,那么通过简单地指定进程名来开启进程都会无法正确运行。当然,在特殊情况下,某些组件之前不需要共享数据,这个时候可以直接指定android:process属性来开启多进程,但是这种场景是不常见的,几乎所有情况都需要共享数据。

一般来说,使用多进程会造成如下几方面的问题:

1.静态成员和单例模式完全失效。

2.线程同步机制完全失效

3.SharePreferences的可靠性下降

4.Application会多次创建

第一个问题上面已经进行分析,第2个问题本质上和第一个问题类似的,既然都不是一块内存了,那么不管是锁对象还是锁全局类都无法保证线程同步,因为不同进程锁的对象不是同一个对象,第3个问题是因为SharedPreferences不支持两个进程同时去执行写操作,否则会导致一定几率的数据丢失,这是因为SharedPrefences不支持两个进程同时去执行写操作,否则会导致一定几率的数据丢失,这是因为SharedPreferences底层是通过读/写XML文件实现的,并发写显然是可能出问题的,甚至并发读/写都有可能出问题。第4个问题也是显而易见的,当一个组件跑在一个新的进程中的时候,由于系统要在创建新的进程同时分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程。因此,相当于系统又把这个应用重新启动了一遍,既然重新启动了,那么自然会创建新的Application。这个问题其实可以这么理解,运行在同一个进程中的组件是属于同一个虚拟机和同一个Application的,同理,运行在不同进程中的组件是属于两个不同的虚拟机和Application的。


IPC基础概念

1.Serializable是JAva所提供的一个序列化接口,它是一个空接口,为对象提供标准的序列号和反序列化操作。使用Serializable实现序列化相当简单,只需要在类的声明中指定一个类似下面的标识即可自动实现默认的序列化过程。
public class People implements Serializable{
   privte static final long = serialVersionUID = 871136882493367832

}
即使不指定serialVersionUID也可以实现序列化,serialVersionUID这个是用来辅助序列化和反序列化过程的,原则上序列化数据中的serialVersionUID只有和当前类的serialVersionUID相同才能正常地反被序列化,serialVersionUID的详细工作机制是:序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中,当反序列华的时候系统会去检测文件中的serialVersionUID,看它是否和当前类的serialVersioUID一致,如果一致就说明序列化的类的版本和当前类的版本是相同的这时候可以成功反序列化;否则就说明当前类和序列化的类相比发生了某些变换,比如成员变量的数量,类型可能发生了改变,这个时候是无法正常序列化的。
一般来说,应该手动去指定serialVersionUID,比如1L,当然,如果不手动指定serialVersionUID,当反序列化的当前类有所改变,比如增加或删除了某些成员变量,那么系统就会重新计算当前类的hash值并把它赋值给serialVersionUID,这个时候当前类的serialVersionUID就和反序列化的数据中的serialVersionUID不一致,于是反序列化失败,就会出现cash,因此,当手动指定了它之后,就很大程度上避免反序列化过程的失败。当然,如果类结构发生了非常规性改变,比如修改了类名,修改了成员变量的类型,这时候还是反序列化失败,因为类结构有了毁灭性的改变,根本无法从老版本的数据中还原出一个新的类结构的·对象。注意:静态成员变量属于类不属于对象,所以不会参与序列化过程;其次用transient关键字标记的成员变量不参与序列化过程
2.Parcelable接口
Parcelable也是一个接口,只要实现了这个接口,一个类的·对象就可以实现序列化并可以通过Intent和Binder传递。
下面是典型的例子:
public class MyParcelable implements Parcelable {
    // You can include parcel data types
    private int mData;
    private String mName;
    
    // We can also include child Parcelable objects. Assume MySubParcel is such a Parcelable:
    private MySubParcelable mInfo;

    // This is where you write the values you want to save to the `Parcel`.  
    // The `Parcel` class has methods defined to help you save all of your values.  
    // Note that there are only methods defined for simple values, lists, and other Parcelable objects.  
    // You may need to make several classes Parcelable to send the data you want.
    @Override
    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(mData);
        out.writeString(mName);
        out.writeParcelable(mInfo, flags);
    }

    // Using the `in` variable, we can retrieve the values that 
    // we originally wrote into the `Parcel`.  This constructor is usually 
    // private so that only the `CREATOR` field can access.
    private MyParcelable(Parcel in) {
        mData = in.readInt();
        mName = in.readString();
        mInfo = in.readParcelable(MySubParcelable.class.getClassLoader());
    }

    public MyParcelable() {
        // Normal actions performed by class, since this is still a normal object!
    }

    // In the vast majority of cases you can simply return 0 for this.  
    // There are cases where you need to use the constant `CONTENTS_FILE_DESCRIPTOR`
    // But this is out of scope of this tutorial
    @Override
    public int describeContents() {
        return 0;
    }

    // After implementing the `Parcelable` interface, we need to create the 
    // `Parcelable.Creator<MyParcelable> CREATOR` constant for our class; 
    // Notice how it has our class specified as its type.  
    public static final Parcelable.Creator<MyParcelable> CREATOR
            = new Parcelable.Creator<MyParcelable>() {

        // This simply calls our new constructor (typically private) and 
        // passes along the unmarshalled `Parcel`, and then returns the new object!
        @Override
        public MyParcelable createFromParcel(Parcel in) {
            return new MyParcelable(in);
        }

        // We just need to copy this and change the type to match our class.
        @Override
        public MyParcelable[] newArray(int size) {
            return new MyParcelable[size];
        }
    };
}
Parcelable和Serizable都能实现序列化都可以用于Intent间的数据传递,Serializable是java中的序列化接口,使用起来简单但是开销大,Parceable是android中的序列化方式,使用稍微麻烦,但是效率高。
3.binder
Binder很复杂,这里简单说一下binder,binder是android中的一个类实现了IBinder接口。从IPC角度看。Binder是Android中的一种跨进程通信方式,Binder还可以理解为虚拟的物理设备,它的设备驱动是/dev/binder,该通信方式在Linux中没有;。从Android Framework角度来说,Bind是ServiceManager连接各种Manager(ActivityManager,WindowManager等等)和相应ManagerService的桥梁。从Android应用层来说,BInder是客户端和服务端进行通信的媒介,当bindService时候,服务端会返回一个包含服务端业务调用的bInder对象,通过Binder对象,客户端可以获取服务端提供的服务或者数据,服务包括普通服务和基于AIDL的服务。
下面通过一种图讲诉Binder的工作机制:


Android中的IPC方式

1.使用Bundle
四大组件中的三大组件(Activity,service,Receiver)都是支持在Intent中传递Bundle数据的,由于Bundle实现lParcelable接口,所以它可以方便地在不同的进程间传输。当在一个进程中启动了另一个进程的Activity,service和Receive,就可以在Bundle中附加需要传输给远程进程的信息并通过Intent发送出去,当然,传输的数据必须能够被序列化
2.使用文件共享
两个进程通过读/写同一个文件来交换数据,比如A进程把数据写入文件,B进程通过读取这个文件来获取数据,在windows上,一个文件如果被加了排斥锁将会导致其他线程无法对其进行访问,包括读和写,而由于Android系统基于Linux,使得并期并发读/写文件可以没有限制地进行,甚至两个线程同时对同一个文件进行写操作都是允许的。注意:SharedPreferences是个特例,SharedPrefences属于文件的一种,但是由于系统对它的读/写有一定的缓存策略,即在内存中会有一份SharedPrefences文件的缓存,因此在多进程的模式下,系统对它的读/写就变得不可靠,当面对高并发的读/写,有很大几率会丢失数据,因此,不建议在进程间通信使用SharedPrefernces.
3.使用Messenger
信使,通过它可以在不同进程中传递Message对象,在Message中放入需要传递的数据,就可以实现数据间的传递,Messenger是一种轻量级IPC方案,底层实现是AIDL,实现Messenger有两个步骤:分为客户端和服务端,具体原理如下图所示:

另外还有AID(远程服务跨进程),ContentProvider(不同应用之间数据共享),socket(网络通信概念,分为流式套接字和数据套接字)这三种方法。
最后解释什么样的场景才适合用于哪种方法进行进程间通信:


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值