Android开发艺术探索读书笔记(二)-跨进程通信

什么叫IPC

IPC,全称为Inter-Process-Communication。意为进程间通信,也叫跨进程通信。是所有操作系统中普遍存在的一种数据传输机制,主要用于两个进程间的数据交换。在Androi系统中,为了对内存有一个统一的优化管理,通常对每个进程所能使用的最大内存做出限制,比如早期的Android版本对单个进程的内存限制为16M。而在开发大型应用程序或者游戏的时候,为了获得更多的内存来支持程序的运行。往往使用多进程开发的方式来累加可用最大内存。而IPC机制就是使用在这样的一种多进程开发环境中的数据传输机制。

二、Android中的多进程模式

2.1:如何开启多进程

正常情况下:在AndroidMenifest.xml清单文件中,通过给四大组件添加Android:process属性开启

特殊情况下:通过JNI在native层fork一个新的进程。

2.2:开启多进程会造成的一些问题:

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

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

3)SharedPreferences的可靠性下降

4)Application会多次创建

三、IPC基础介绍

1.Serializable接口

Serializable接口是Java提供的一个序列化接口,用来为对象提供标准的序列化和反序列化的操作,其实现方式为在类中定义一个serialVersionUID标志量。实例如下:

public Class User implements Serializable{
    private static final long serialVersionUID =519067123721295773L;
    ...
    ...
}

序列化过程:

User user = new User();
ObjectOutPutStream out = new ObjectOutputStream(new FileOutputStream("cache.txt"));
out.writeObject(user);
out.close();

反序列化过程:

ObjectInputStream in = new ObjectOutputStream(new FileInputStream("cache.txt"));
User newUser = (User) in.readObject();
in.close();

Serializable的工作原理为:

当我们创建ObjectOutputStream时,会将对应类中的serialVersionUID值转换为4个16进制的头部字节值并将其以输出流的方式保存在文本介质中,而后再将其他需要保存的文本内容以流的方式存放在文本介质中。当我们新建ObjectInputStream时,会先自动读取4个头部字节值,通过判断头部字节值是否与serialVersionUID一致的方式来确认后面的数据是否为对象序列化数据。如果是,进行反序列化,取出文本中的数据。如果不是,则无法正常进行序列化。因此,为了保证序列化与反序列化操作能够顺利执行,尽管在JAVA中默认可以不为类添加serialVersionUID,但这样却并不能保证序列化与反序列化操作得以成功。因此一般情况下仍是要求我们手动为其添加一个serialVersionUID。

2.Parcelable接口

Parcelable是Android中实现序列化的方式,其本质上也是一个接口。但和Serializable相比,两者实现序列化的方式却有着较大的区别。一般说来,Parcelable实现序列化的功能包括序列化,反序列化以及对象描述等,而当我们需要实现某种功能时,只需调用相应的方法即可。那么,如何实现一个Parcelable接口去进行序列化与反序列化呢?步骤如下:

1)创建对象类并实现继承 Parcelable 接口

2)重写writeToParcel方法,将对象序列化为一个Parcel对象

3)重写describeContents方法(默认为0)

4)实例化静态内部对象CREATOR实现接口Parcelable.Creator,代码如下:

public static final Parcelable.Creator<T> CREATOR//T为对象类型

实例:实现一个对象类的序列化操作(Parcelable):

`
//继承Parcelable接口
public class User implements Parcelable 
{
 private int userId;
 private String userName;
 private boolean isMale;
 public Book book;
//重写writeToParce方法
public void writeToParcel(Parcel out, int flags) 
 {
     out.writeInt(userId);
 out.writeString(userName);
 out.writeInt(isMale?1:0);
 out.writeParcelable(book,0)
 }
//重写describeContents方法(默认为0)
 public int describeContents() 
 {
     return 0;
 }
 //实例化静态内部对象CREATOR实现接口Parcelable.Creator
 public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() 
 {
     public User createFromParcel(Parcel in) 
     {
         return new User(in);
     }

     public User[] newArray(int size) 
     {
         return new User[size];
     }
 };

 private User(Parcel in) 
 {
    userId = in.readInt();
    userName = in.readString();
    isMale = in.readInt();
    book = in.readParcelable(Thread.currentThread().getContextClass-loader();)  
    }
 }

`
注意的是:因为book对象也是一个序列化的对象,因此在它的反序列化过程中需要传递当前线程的上下文类加载器,否则会出现NotFoundClass错误

3.Binder

3.1什么是Binder:

Binder是实现IBinder接口的一个Android类,是Android中的一种通讯方式。它可以理解为一种虚拟的物理设备,其设备驱动为/dev/binder。注意的是,这种通信方式在Linux中是不存在的,从Android的框架层(Framework)来看,Binder是ServiceManager连接各种Manager以及相应的ManagerService的桥梁。而从应用层(Application)而言,Binder是客户端和服务端进行通信的一个媒介。

3.2:为什么Android选择Binder作为IPC机制的原因:

我们知道,Android是基于Linux的内核而进行开发的。但不管是Android还是Linux,都是独立的操作系统。两者之间有某些联系,但不代表就是简单的继承关系。在Android的开发过程中,针对特定的用户体验和系统通讯等一些特定的问题,Android是在linux内核的基础上进行了进一步的封装及改造,使得Android系统更实用于移动端设备,也使得Android和linux之间出现了一些比较大的差异。而这里所要介绍的Binder,就是Android系统的产物,该通讯方式是linux所没有的。那么,linux的IPC机制有很多,包括管道传输、共享内存、Socket等。为什么Android不直接采用这些线程的IPC,而选定了Binder呢?我们来分析一下:

我们知道,随着Web技术的发展,B/S架构的通讯方式越来越火。但在移动端方面,C/S架构仍是实现嵌入式手持设备访问互联网或者数据库的主要通讯方式。对于Android而言,所有的server(媒体播放,传感器等)都是在不同的进程里面执行的,作为Client的移动端如何使用这些不同的server,使得应用开发者能够用最少的时间去开发更丰富的应用功能,就成了Android系统设计的一个难点,而这个难点就在于C/S架构如何快速、安全地实现跨进程通讯。而Binder就是基于Client-Server通信模式,传输过程只需一次拷贝,传输效率相对较高,并且操作简单。同时Binder支持向发送方添加UID/PID身份,既支持实名Binder也支持匿名Binder,使得数据不易被恶意程序获得,安全性较高。

3.3:Binder的工作机制(图示):

这里写图片描述

四、Android中的IPC方式:

1.使用Bundle

因为Bundle内部实现了Parcelable接口,所以它可以在不同的进程间进行数据。在Android中,Acrivity、Service、和Receiver都支持在Intent中传递Bundle数据。而ContentProvider本身就已经使用了不同进程间的数据传输了。当然,使用Bundle传输的数据必须是能够被序列化的。

2.使用文件共享

使用文件共享的方式的工作原理为两个进程通过访问同一个文件的方式来实现数据的交换。相当于一个进程向文件中写入数据,而另一个进程从文件中读取数据。但这种方法也是有局限的。一来将所需的数据保存下来,再供其他进程使用,会使得跨进程间的数据传输效率降低。同时处于生产者-消费者模式的思考,一旦写方进程并未从中写入相应的数据,而读方进程就已经需要
从文件中读取数据时,就会出现得不到对应数据的情况。另外,将数据持久化的方式容易受恶意程序影响而被外泄,造成信息安全问题。所以,文件共享方式适合在对数据同步要求不高的进程间进行通讯,并且要妥善处理好并发读写的问题。

3.使用Messenger

Messenger是一种轻量级的IPC方案,是在Aidl的基础上进行封装而成。它的工作形式为一次处理一个请求,因此不需要考虑并发读写的问题,但同样的,该方法的数据传输效率也会偏低。

Messenger的工作机制:

这里写图片描述

4.使用AIDL

AIDL是Messenger的底层实现,可以实现跨进程调用服务端方法的功能。但操作比Messenger更为复杂。对于两者的选择可以根据数据传递的大小以及频率等方法来分析。数据较小,频率较低的可选择Messenger。

4.1 使用AIDL的步骤

服务端方面:

1.创建一个Service来监听客户端的连接请求

2.创建一个AIDL文件,将要暴露给客户端的接口在这个AIDL文件中进行声明

3.在Service中实现该AIDL接口

客户端方面:

1.绑定服务端的Service

2.将服务端返回的Binder对象转化为AIDL接口所属的类型

3.调用AIDL中的方法

注意:AIDL中允许声明的变量有6种,分别为:

1.基本数据类型

2.String和CharSequence

3.List:只支持ArrayList并且里面的每个元素都要被AIDL所支持

4.Map:只支持HashMap并且里面的每个元素都要被AIDL所支持,包括key和value

5.Parcelable:所有实现了Parcelable接口的对象,需要显式地用import导入

6.AIDL:所有的AIDL接口本身也可以在AIDL文件中使用,需要显式地用import导入

注意:如果在AIDL中用到自定义的Parcelable对象,那么必须新建一个和它同名的AIDL文件
,并在其中声明它为Parcelable类型。

4.2:AIDL的权限验证方法:

4.2.1:在OnBind方法中进行验证用户绑定验证,例子:使用Permission验证。

步骤1: 在AndroidManifest中声明所需的权限,例如:

<permission
    android:name="com.ryg.chapter_2.permission.AXXESS_BOOK_SERVICE"
    android:protectionLevel="Normal"/>

步骤2:在OnBind方法中进行权限验证。例如:

public IBinder onBind(Intent intent){
    int check=checkCallingOrSelfpermission("com.ryg.chapter_2.
                                permission.AXXESS_BOOK_SERVICE");
    if(check == PackageManager.PERMISSION_DEBIND){
    return null;
    }
    return mBinder;
}

4.2.2:在服务端的onTransact方法中进行权限认证。例子:使用Permission验证。(也可以使用Uid和Pid来做认证)

public boolean onTransact(int Code. Parcel data, Parcel reply, int flag)
    throws RemoteException{
    int check=checkCallingOrSelfpermission("com.ryg.chapter_2.
                                permission.AXXESS_BOOK_SERVICE");
    if(check == PackageManager.PERMISSION_DEBIND){
        return false;
     }
    String packageName = null;
    String[] packages = getPackageManager().getPackagesFprUid(getCalling-Uid());
    if(packages != null && package.length > 0){
        packageName = packages[0];
    }
    if(!packageName.StartWith("com.ryg")){
        return false;
    }
    return super.onTransact(code,data,reply,flage);
}

如果一个应用中需要用到多种service,可以通过建立 Binder线程池的方式对所有的AIDL进行统一的管理,将其加入到一个远程的Service中,当我们需要调用某种service时,先从远程Service中进行查询,成功后通过远程service进行返回,如此,便减少了service的创建次数,也使得多种AIDL的从属关系更加明确。

5.使用ContentProvider

Content是Android中提供的专门用于不同进程间进行数据共享的方式,天生就适用于进程间通信。和Messenger一样,ContentProvider的底层实现同样是Binder,并且其操作方式相较于AIDL更为简单。因为ContentProvider作为Android的四大组件之一,系统已经为我们进行了一定封装,所以我们能够不需要经过底层而直接实现IPC。

ContentProvider的使用方式:

1.新建目标类,继承ContentProvider类并实现6个抽象方法:

onCreate: 代表着ContentProvider的创建

query:    查询数据

update:   更改数据

insert:   插入数据

delete:   删除数据

getType:  返回一个uri请求对应的MIME类型

2.注册目标类,使外部应用能够调用给ContentProvider。主要为android:authorities 属性.例子:

<provider
    android:name=".provider.BookProvider"
    android:authorities = "com.ryg.chapter_2.aidl.provider"
    android.permission = "com.ryg.PROVIDER"
    android:process = ":provider"/>
</provider>

6.使用Socket

socket属于数据传输的范畴,一般说来,Socket是一套适用于多方面的普遍性数据传输方案。不管是蓝牙数据传输,网络数据传输以及跨进程间的数据传输,都可以运用到Socket,但注意的是,我们在日常的开发过程中,更倾向于把Socket作为网络数据传输的方式,因为,从狭义上讲,Socket其实属于网络数据传输的范畴。它主要分为流式套接字和用户数据报套接字两种,分别对应于网络的传输控制协议层中的TCP和UDP协议。从交互形式而言,Socket更倾向于c/s架构间需要通过互联网进行数据检索,发送,接受等的部分。使得不同的进程通过互联网而实现数据的交互。比如常见的聊天功能,就是基于Socket而实现的。

注意:

  1. 使用Socket需要添加相应的权限:
  <uses-permission android:name="android.permission.INTERNET"/>
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

2.不能在UI线程中进行网络访问,否则会使得UI线程出现延时阻塞而出现ANR(Application Not Responding)。从android4.0之后开始甚至已经设定为当在UI线程中访问网络时出现NetWorkOnMainTheradException错误。

3.Socket是一种长期连接的网络访问方式,会造成比较大的带宽消耗。因此使用Socket时,不再需要进行网络访问时要断开Socket连接

如何 选用合适的IPC方式,如表:

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值