Android开发二《IPC机制》
一、Android的IPC简介
IPC:Inter-process Communication的缩写,含义为进程间通信,指两个进程之间进行数据交换的过程.
进程:指一个执行单元,在PC端或移动设备上指一个程序或者一个应用.一个进程可以包含多个线程;只包含一个为主线程(UI线程)
线程:是CPU调度的最小单元,同时线程是一种有限的系统资源.
ANR:Application Not Responding,即应用无响应,主线程中耗时操作导致;
多进程使用场景:
1、应用自身需要多进程模式实现某些功能;
2、当前应用需要向其他应用获取数据;
二、Android中的多进程模式
1、开启多进程
通过给四大组件指定:android:process属性,开启多进程模式
1、android:process=":remote"
进程名以:开头为当前应用私有进程,
2、android:process="com.alan.base.remote"
进程名为完整的为全局进程,其他应用可通过ShareUID方式与它跑同一个进程(要求:两个应用有相同的ShareUID并且签名相同)
Android系统为每个应用都会分配一个唯一的UID,具有相同的UID的应用才能共享数据,可以互相访问私有数据
2、多进程模式运行机制
Android为每一个应用分配了一个独立的虚拟机,或者说为每个进程分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同的虚拟机中访问同一个类的对象会产生多份副本.
多进程问题:
1、静态成员和单例模式完全失效(备份)
2、线程同步机制完全失效(不同进程不是同一个对象)
3、SharedPreferences的可靠性下降(底层读写xml文件,并发写会出现问题)
4、Application会多次创建(运行在同一个进程的组件属于同一个虚拟机和同一个Application)
三、IPC基础概念介绍
1、序列化
序列化:将对象转换为可以传输的二进制流(二进制序列)的过程,从而进行传输数据;
反序列化:就是从二进制流(序列)转化为对象的过程.
1. Serializable接口
Serializable是JAVA提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作,使用只需声明下面标识即可自动实现序列化过程:
private static final long serialVersionUID = 112L;
不声明serialVersionUID也可以,不影响序列化,但会影响反序列化;
静态成员变量属于类不属于对象,不会参与序列化,用transient关键字修饰的成员变量也不会参与序列化;
序列化过程
User user = new User();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.txt"));
out.writeObject(user);
out.close();
反序列化过程
ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.txt"));
User newUser = in.readObject();
in.close();
2. Parcelable接口
Parcelable是Android提供的一个新序列化接口;
Parcel内部包装了可序列化的数据,可以在Binder中自由传输;
Parcel提供了一套机制,可以将序列化之后的数据写入到一个共享内存中,其他进程通过Parcel可以从这块共享内存中读出字节流,并反序列化成对象,下图是这个过程的模型。
Parcelable
public class DataBean implements Parcelable {
private int mData;
/**
* 负责反序列化
*/
public static final Parcelable.Creator<DataBean> CREATOR
= new Parcelable.Creator<DataBean>() {
/**
* 从序列化对象中,获取原始的对象
* @param source
* @return
*/
public DataBean createFromParcel(Parcel in) {
return new DataBean(in);
}
/**
* 创建指定长度的原始对象数组
* @param size
* @return
*/
public DataBean[] newArray(int size) {
return new DataBean[size];
}
};
private DataBean(Parcel in) {
mData = in.readInt();
}
/**
* 描述
* 返回的是内容的描述信息
* 只针对一些特殊的需要描述信息的对象,需要返回1,其他情况返回0就可以
*
* @return
*/
public int describeContents() {
return 0;
}
/**
* 序列化
*
* @param dest
* @param flags
*/
public void writeToParcel(Parcel out, int flags) {
out.writeInt(mData);
}
}
Parcelable实现过程主要分为序列化,反序列化,描述三个过程,下面分别介绍下这三个过程
实现Parcelable的作用
1)永久性保存对象,保存对象的字节序列到本地文件中;
2)通过序列化对象在网络中传递对象;
3)通过序列化在进程间传递对象。
Parcelable和Serializable的区别和比较
Parcelable和Serializable都是实现序列化并且都可以用于Intent间传递数据;
Serializable是Java的实现方式,可能会频繁的IO操作,所以消耗比较大,但是实现方式简单; Parcelable是Android提供的方式,效率比较高,但是实现起来复杂一些;
二者的选取规则是:内存序列化上选择Parcelable, 存储到设备或者网络传输上选择Serializable(当然Parcelable也可以但是稍显复杂)
选择序列化方法的原则
1)在使用内存的时候,Parcelable比Serializable性能高,所以推荐使用Parcelable。
2)Serializable在序列化的时候会产生大量的临时变量,从而引起频繁的GC。
3)Parcelable不能使用在要将数据存储在磁盘上的情况,因为Parcelable不能很好的保证数据的持续性在外界有变化的情况下。尽管Serializable效率低点,但此时还是建议使用Serializable 。
3. 区别
1、作用
Serializable的作用是为了保存对象的属性到本地文件、数据库、网络流、rmi以方便数据传输,当然这种传输可以是程序内的也可以是两个程序间的。
而Android的Parcelable的设计初衷是因为Serializable
效率过慢,为了在程序内不同组件间以及不同Android程序间(AIDL)高效的传输数据而设计,这些数据仅在内存中存在,Parcelable是通过IBinder
通信的消息的载体。
2、效率及选择
Parcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,如activity
间传输数据;
而Serializable可将数据持久化方便保存,所以在需要保存或网络传输数据时选择Serializable
,因为android不同版本Parcelable
可能不同,所以不推荐使用Parcelable
进行数据持久化。
3、编程实现
对于Serializable
,类只需要实现Serializable
接口,并提供一个序列化版本id(serialVersionUID)
即可。
Parcelable
则需要实现writeToParcel
、describeContents
函数以及静态的CREATOR变量,实际上就是将如何打包和解包的工作自己来定义,而序列化的这些操作完全由底层实现。
2、Binder介绍
- 直观来说:Binder是Android中的一个类,实现了IBinder接口.
- 从IPC角度:Binder是Android中的一种跨进程通信方式;
- 硬件方面:Binder可以理解为一种虚拟的物理设备,他的设备驱动是/dev/binder,该通讯方式在Linux中没有;
- Android Framework角度:Binder是ServiceManager链接各种Manager和相应ManagerService的桥梁;
- Android应用层:Binder是客户端和服务端通讯的媒介,当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务.
Android开发中,Binder主要用在Service中,包括AIDL和Messenger;其中Messenger的底层实现是AIDL.
1. Binder概述
Android系统中,涉及到多进程间的通信底层都是依赖于Binder IPC机制。
2. Android使用Binder作为IPC方式原因
- 性能方面:
在移动设备上(性能受限制,比如耗电),广泛使用IPC对通讯机制性能有严格要求,Binder相对于传统IPC方式方式更加高效;Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要两次,共享内存一次数据拷贝都不需要,但实现方式复杂。 - 安全方面:
传统的IPC方式对于通信双方的身份并没有做出严格的验证。Binder机制从协议本身就支持对通信双方做身份验证,从而大大提高安全性。
3. IPC原理
从进程角度看IPC机制
IPC机制
每个Android的进程,只能运行在自己进程所拥有的虛拟地址空间。例如,对应一个4GB的虛拟地址空间,其中3GB是用户空间,1GB是内核空间。当然内核空间的大小是可以通过参数配置调整的。对于用户空间,不同进程之间是不能共享的,而内核空间却是可共享的。Client进程向Server进程通信,恰怡是利用进程间可共享的内核内待空间来完成底层通信工作的。Client端与Server端进程往往采用ioctl等方法与内核空间的驱动进行交互。
4. Binder原理
Binder通信采用C/S架构,从组件视角来说,包含Client 、Server 、ServiceManager以及Binder驱动,其中ServiceManager用 于管理系统中的各种服务。
Binder架构图
Binder通信的四个角色
- Client进程:使用服务的进程。
- Server进程:提供服务的进程。
- ServiceManager进程:ServiceManager的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。
- Binder驱动:驱动负责进程之间Binder通信的建立,Binder在进程之问的传递,Binder引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。
Binder运行机制
Client/Server/ServiceManage之间的相互通信都是基于Binder机制。既然基于
Binder机制通信,那么同样也是C/S架构,则图中的3大步骤都有相应的Client端与Server端。
-
注册服务(addService):
Server进程要先注册Service 到Serviceanager。该过程:Server是客户端,ServiceManager是服务端。(Android开机启动过程中,会初始化系统的各个Service,并将Service向ServiceManager注册,这一步系统自动完成) -
获取服务(getService):
Client进程使用某个Service前,须先向ServiceManager中获取相应的Service。该过程:Client是客户端,ServiceManager是服务端。(例如:WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);) -
使用服务:
Client根据得到的Service信息建立与Service所在的Server进程通信的通路,然后就可以直接与Service交互。该过程:Client是客户端,Server是服务端。
图中的Client,Server, Service Manager之间交 互都是虚线表示,是由于它们彼此之问不是直接交互的,而是都通过与Binder驱动进行交互的,从而实现IPC通信(Interprocess Communication )方式。其中Binder驱动位于内核空间,Client,Server , Service Manager位于用户空间。Binder驱动和Service Manager可以看做是Android平台的基础架构,而Client和Server是 Android的应用层,开发人员只需自定义实现Client、Server端,借助Android的基本平台架构便可以直接进行IPC通信。
Binder通信的实质
Binder通信的实质是利用内存映射,将用户进程的内存地址和内核的内存地址映射为同一块物理地址,也就是说他们使用的同一块物理空间,每次创建Binder的时候大概分配128的空间。数据进行传输的时候,从这个内存空间分配一点,用完了再释放即可。
5. Binder传输大文件
Android中的匿名共享内存(Ashmem)是基于Linux共享内存的,借助Binder+文件描述符(FileDescriptor)实现了共享内存的传递。它可以让多个进程操作同一块内存区域,并且除了物理内存限制,没有其他大小限制。相对于Linux的共享内存,Ashmem对内存的管理更加精细化,并且添加了互斥锁。Java层在使用时需要用到MemoryFile,它封装了native代码。Android平台上共享内存通常的做法如下:
- 进程A通过MemoryFile创建共享内存,得到fd(FileDescriptor);
- 进程A通过fd将数据写入共享内存;
- 进程A将fd封装成实现Parcelable接口的ParcelFileDescriptor对象,通过Binder将ParcelFileDescriptor对象发送给进程B;
- 进程B获从ParcelFileDescriptor对象中获取fd,从fd中读取数据。
1、通过AIDL传输FileDescriptor方式;
2、通过Bundler传输;
创建继承Binder的类
class ImageBinder extends Binder {
private Bitmap bitmap;
public ImageBinder(Bitmap bitmap) {
this.bitmap = bitmap;
}
Bitmap getBitmap() {
return bitmap;
}
}
发送大图
Intent intent = new Intent(this, SecondActivity.class);
Bundle bundle = new Bundle();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
bundle.putBinder("bitmap", new ImageBinder(mBitmap));
}
intent.putExtras(bundle);
startActivity(intent);
接收大图
Bundle bundle = getIntent().getExtras();
if (bundle != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
ImageBinder imageBinder = (ImageBinder) bundle.getBinder("bitmap");
Bitmap bitmap = imageBinder.getBitmap();
mIv.setImageBitmap(bitmap);
}
}
较大的bitmap直接通过Intent传递报错TransactionTooLargeException; 是因为Intent启动组件时,系统禁掉了文件描述符fd,bitmap无法利用共享内存,只能采用拷贝到缓冲区的方式,导致缓冲区超限,触发异常;putBinder 的方式,避免了intent 禁用描述符的影响,bitmap 写parcel时的fd 默认是true,可以利用到共享内存,所以能高效传输图片。
注意:概念了解
1、MemoryFile
MemoryFile是android在最开始就引入的一套框架,其内部实际上是封装了Android特有的内存共享机制Ashmem匿名共享内存;
2、mmap
内存映射
3、FileDescriptor
Linux 系统中,把一切都看做是文件,当进程打开现有文件或创建新文件时,内核向进程返回一个文件描述符,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行I/O操作的系统调用都会通过文件描述符。
4、ParcelFileDescriptor
ParcelFileDescriptor是可以用于进程间Binder通信的FileDescriptor,其实就是实现了Parcelable并能够通过流获取数据。
四、Android中的IPC方式
1、使用Bundle
四大组件中的三大组件(Activity,Service,Receiver)都是支持在Intent中传递Bundle数据;
2、使用文件共享
两个进程通过读写同一个文件来交换数据;
3、使用Messenger
Messenger:信使,通过它可以在不同进程中传递Message对象,只能串行执行;
使用步骤:
- 服务端进程
在服务端创建一个Service来处理客户端的连接请求,同时创建Handler并通过它来创建一个Messenger对象,然后在Service的onBind中返回这个Messenger对象底层的Binder; - 客户端进程
绑定服务端的Service,绑定成功后用服务端返回的IBinder对象创建一个Messenger,通过这个Messenger就可以向服务端发送消息了
4、使用AIDL
AIDL (Android Interface Definition Language) 是一种接口定义语言,用于生成可以在Android设备上两个进程之间进行进程间通信(Interprocess Communication, IPC)的代码。如果在一个进程中(例如Activity)要调用另一个进程中(例如Service)对象的操作,就可以使用AIDL生成可序列化的参数,来完成进程问通信。
Messenger底层实现是AIDL,Messenger只能传递消息Message,Messenger只能一个个处理消息,如果有大量并发请求获取调用服务端方法,则需要使用AIDL;
使用步骤:
- 服务端
创建一个Service用来监听客户端的连接请求,然后创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明,最后在Service中实现这个AIDL接口即可. - 客户端
客户端绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了. - AIDL接口的创建用来传递数据;
- 远程服务端Service的实现;
- 客户端的实现;
注意:
1.AIDL 找不到符号 方法 readFromParcel(Parcel)
重写bean文件添加方法readFromParcel;
2.aidl 找不到符号
把Java的Bean文件放到Java文件夹下,aidl文件放到aidl文件夹下,重新编译。
AIDL原理
Binder通信过程- Binder对象的获取
Binder是实现跨进程通信的基础,Binder在服务端和客户端实现共享,是同一个对象,通过Binder对象获取实现了IInterface接口的对象来调用远程服务;
服务端通过binder保存和获取IInterface两个关键方法: attachInterface和queryLocalInterface。
public class Binder implements IBinder {
private IInterface mOwner;
public void attachInterface(@Nullable IInterface owner, @Nullable String descriptor) {
mOwner = owner;
mDescriptor = descriptor;
}
public @Nullable IInterface queryLocalInterface(@NonNull String descriptor) {
if (mDescriptor != null && mDescriptor.equals(descriptor)) {
return mOwner;
}
return null;
}
//第一个参数:识别调用哪一方法的id
//第二个参数:序列化传入的数据
//第三个参数:调用方法后返回的数据
public final boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply,int flags) throws RemoteException {
if (false) Log.v("Binder", "Transact: " + code + " to " + this);
if (data != null) {
data.setDataPosition(0);
}
boolean r = onTransact(code, data, reply, flags);
if (reply != null) {
reply.setDataPosition(0);
}
return r;
}
protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
...
}
}
Binder具有跨进程传输的能力是因为实现了IBinder接口。系统会为每个实现了该接口的对象提供跨进程传输。
Binder具有完成特定任务的能力是通过它的IInterface对象获取的。
- 调用服务端方法
Proxy对象中的transact调用发生后,会引起系统的注意,系统意识到Proxy对象想找它的真身Binder对象(系统其实一直存着Binder和Proxy的对应关系)。于是系统将这个请求中的数据转发给Binder对象,Binder对象将会在onTransact中收到Proxy对象传来的数据,于是它从data中取出客户端进程传来的数据,又根据第一个参数确定想让它执行添加书本操作,于是它就执行了响应操作,并把结果写回reply。
参考:AIDL使用详解
5、使用ContentProvider
ContentProvider是Android中提供的专门用于不同应用间进行数据共享的方式,和Messenger一样,ContentProvider的底层实现同样也是Binder;
1、创建ContentProvider
public class BankProvider extends ContentProvider {
//ContentProvider的创建,初始化工作,运行在主线程中;
@Override
public boolean onCreate() {
return false;
}
//用于向指定uri的ContentProvider中添加数据,返回该行的Uri,运行在ContentProvider线程中;
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
//用于删除指定uri的数据,返回成功删除的行数,运行在ContentProvider线程中;
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
//用户更新指定uri的数据,返回成功修改的行数,运行在ContentProvider线程中;
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
return 0;
}
//用于查询指定uri的数据返回一个Cursor,运行在ContentProvider线程中;
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
return null;
}
//返回一个Uri请求对应的MIME类型,运行在ContentProvider线程中;
@Override
public String getType(Uri uri) {
return null;
}
}
ContentProvider的主要方法:
public boolean onCreate():在创建 ContentProvider 时使用
public Cursor query():用于查询指定 uri 的数据返回一个 Cursor
public Uri insert():用于向指定uri的 ContentProvider 中添加数据
public int delete():用于删除指定 uri 的数据
public int update():用户更新指定 uri 的数据
public String getType():用于返回指定的 Uri 中的数据 MIME 类型
注:数据访问的方法 insert,delete 和 update 可能被多个线程同时调用,此时必须是线程安全的。
注意:除了onCreate由系统回调运行在主进程中,其余5个方法均由外界回调并运行在Binder线程池中.
2、注册ContentProvider
<provider
android:name=".ui.fragment.BankProvider"
android:authorities="com.alan.user.bank"
android:permission="com.alan.PROVIDER"
android:readPermission="com.alan.PROVIDER"
android:writePermission="com.alan.PROVIDER"
android:enabled="true"
android:exported="true"/>
android:authorities:是ContentProvider的唯一标识,通过这个属性外部应用访问;
android:permission:外界想要访问这个ContentProvider,必须添加的权限.
android:readPermission:读权限
android:writePermission:写权限
3、访问ContentProvider
Uri uri =Uri.parse("com.alan.user.bank");
getContentResolver().query(uri,null, null, null, null);
4、ContentProvider 核心类
1、UriMatcher:操作 Uri 的工具类,用于匹配 Uri;
2、ContentUris:操作 Uri 的工具类,用于操作 Uri 路径后面的 ID 部分;
3、ContentObserver:用于监听 ContentProvider 中指定 Uri 标识数据的变化(增 / 删 / 改)
6、使用Socket
Socket:也称为套接字,是网络通信中的概念,它分为流式套接字和用户数据报套接字两种,分别对应于网络的传输控制层中TCP和UDP协议.Socket可以支持传输任意字节流.
使用步骤:
1、声明网络权限INTERNET和ACCESS_NETWORK_STATE
2、创建服务端Service,并在子线程(不能在主进程中访问网络)中创建ServerSocket监听端口号;
3、创建客户端,开启线程去连接服务端,通过Socket发送消息;
五、Binder连接池
AIDL的流程:
1、创建一个Service和一个AIDL接口;
2、创建一个类继承自AIDL接口中的Stub类并实现Stub中的抽象方法;
3、在Service的onBind方法中返回这个类的对象;
4、然后客户端就可以绑定服务端的Service,建立连接后就可以访问远程服务端的方法.
AIDL是一种常用的进程间通讯方式,每次都要创建Service,如果100个就会创建100个service,这会浪费系统资源.所以需要减少Service数量,将所有的AIDL放在同一个Service中去管理.
服务端提供一个queryBinder接口,根据业务模块的特征来返回相应的Binder对象客户端,不同业务模块拿到所需的Binder对象后就可以进行远程方法调用了.
Binder连接池的主要作用就是将每个业务模块的Binder请求统一转发到远程Service中去执行,从而避免了重复创建Service的过程.