1. IPC基础概念——多进程
IPC是Inter Process Communication的缩写,意为进程间通信或者跨进程通信,是指两个进程之间进行数据交换。
1.1 Android中的多进程
在了解Android多进程之前,得先了解什么是进程?
按照操作系统的描述,线程是CPU调度的最小单元,而进程一般指一个执行单元,在移动设备上指一个程序或应用;一个进程可以包含多个线程。
1.2 Android开启多进程模式
在Android中要开启多进程模式,只要给四大组件指定 android:process 属性。
是不是很简单的就开启了多进程,其实远没有那么简单,这只是挖坑的开始,先看一下开启多进程的实例操作:
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
// 注意进程以:开头
<activity android:name=".SecondActivity"
android:process=":remote"/>
<activity android:name=".ThirdActivity"
android:process="com.czj.ipcdemo.remote2"/>
在终端输入adb shell ps | com.czj.ipcdemo查看一个包名中当前应用所存在的进程信息。
会发现一共存在3个进程。
此处划重点:
- 以:开头的属于当前应用私有进程,其它应用的组件不可以和它跑在同一个进程;
- 进程名不以:开头的,属于全局进程,其它应用通过shareUID方式可以和它跑在同一个进程中。
![](https://img.haomeiwen.com/i2296863/f02ca449483c273c.png)
1.3 多进程造成的问题
- 静态成员和单例模式失效
- 线程同步机制失效
- SharedPreferences的可靠性降低
- Application会创建多次
Android虚拟机分配规则
Android会为每一个进程都分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址看空间,这就导致在不同的虚拟机(进程)中访问同一个对象会产生多份副本。所以,在多进程环境中,想要依靠内存来共享数据,是不会达到你预期的结果的。
现在来分析一下上述列出的问题:
第1个问题和第2个问题都是因为不同进程的虚拟机是独立的,不同虚拟机内存地址当然就是不一样的,既然不是同一块内存地址,锁对象或者锁类也达不到线程同步。
第3个问题,SharedPreferences不支持两个进程同时做写操作。
第4个问题,一个组件跑在新的进程中,系统要为其分配独立的虚拟机,也就相当于重新启动一个应用。
在进行跨进程通信之前,还得了解Android的序列化(Serializable 和 Parcelable)以及Binder的概念。
2. IPC基础概念——序列化
关于Android序列化,附个传送门:Android序列化基础知识
3. IPC基础概念——Binder
从各个角度看Binder:
IPC:Binder是Android中的跨进程通信方式。
Android Framework:BInder是ServiceManger连接各种Manager和响应MangerService的桥梁。
Android应用层:Binder是客户端和服务端进行通信的媒介。
Binder主要用在Service中,包括AIDL和Messenger(底层是AIDL),普通的Service中的Binder不涉及进程间通信。后面主要对AIDL的使用,进行分析,一个AIDL过程,即是一个RPC的过程。
后面第四部分会对AIDL的使用进行实例分析。
4. Android实现IPC的几种方式
- Bundle
- 文件共享
- Messenger
- AIDL
- ContentProvider
- Socket
4.1 Bundle
Android中的Activity、Service、Receiver都支持使用Intent来传递Bundle数据,Bundle实现了Parcelable接口,所以能够使用Bundle来实现数据的进程间传输。
需要注意的一点是,传输的对象必须能够被序列化(基本类型、Parcelable、Serializable)
4.2 文件共享
两个进程对同一个文件进行读和写,来实现进程间的通信。实现无非就是文件IO操作,实例此处略过。
但是,这里说的文件共享不包括SharedPreferences。
虽然SharedPreferences是XML文件,但是系统会对它的读写有缓存策略,就是说再内存中会有一份SharedPreferences缓存,但是多进程是分配在独立的虚拟机上的,所以SharedPreferences数据在多进程是不可靠的。
4.3 Messenger
Messenger是一种轻量级的IPC方案,它的底层实现是AIDL。
服务端进程:
public class MessengerService extends Service {
private static final String TAG = "MessengerService";
private Messenger mMessenger = new Messenger(new MessengerHandler());
@Override
public void onCreate() {
super.onCreate();
}
@Override
public IBinder onBind(Intent intent) {
return mMessenger.getBinder();
}
private static class MessengerHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case Constants.MSG_CLIENT:
// 收到客户端消息
Log.e(TAG, "收到客户端进程消息:" + msg.getData().getString("msg"));
// 回复客户端消息
Messenger client = msg.replyTo;
Message replyMessage = Message.obtain(null, Constants.MSG_SERVER);
Bundle bundle = new Bundle();
bundle.putString("reply", "来自Service的回复");
replyMessage.setData(bundle);
try {
client.send(replyMessage);
} catch (RemoteException e) {
e.printStackTrace();
}
break;
default:
super.handleMessage(msg);
}
}
}
}
创建一个Service来处理客户端的连接请求,同时创建一个Handler内部类来处理消息,并用来实例化Messenger对象。最后在onBind返回Messenger对象的Binder。日志打印如下:
10-24 14:08:47.542 31636-31636/com.czj.ipcdemo:remote3 E/MessengerService: 收到客户端进程消息:client msg
客户端进程:
public class MessengerActivity extends AppCompatActivity {
private static final String TAG = "MessengerActivity";
private Messenger mService;
private Messenger mMessenger = new Messenger(new MessengerHandler());
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = new Messenger(service);
Message msg = Message.obtain(null, Constants.MSG_CLIENT);
Bundle bundle = new Bundle();
bundle.putString("msg", "client msg");
msg.setData(bundle);
// 将客户端的 Messenger 传递给服务端
msg.replyTo = mMessenger;
try {
mService.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_messenger);
Intent intent = new Intent(this, MessengerService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
unbindService(mConnection);
super.onDestroy();
}
private static class MessengerHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case Constants.MSG_SERVER:
Log.e(TAG, "收到服务端回信:" + msg.getData().get("reply"));
break;
default:
super.handleMessage(msg);
}
}
}
}
客户端进程,首先要绑定服务端的Service,然后用服务端给的IBinder去创建Messenger来与服务端进行通信。
如果需要服务端能够回复客户端,则客户端要定义个Messenger,然后赋值给Message.replyTo传递给服务端。
日志打印如下:
10-24 14:08:47.548 31568-31568/com.czj.ipcdemo E/MessengerActivity: 收到服务端回信:来自Service的回复
使用Messenger进行进程间的通信,使用方法简单,但它也有些局限性。
- Messenger是以串行的方式处理客户端消息,服务端只能一个个处理,不适合有大量并发请求的情况
- Messenger的主要作用是传递消息,无法跨进程调用服务端的方法(也是RPC过程)
4.4 AIDL
AIDL(Android Interface Definition Language,Android接口定义语言):如果在一个进程中要调用另一个进程中对象的方法,可使用AIDL生成可序列化的参数,AIDL会生成一个服务端对象的代理类,通过它客户端实现间接调用服务端对象的方法。
4.4.1 AIDL文件定义
1. AIDL接口创建
AIDL接口就是暴露给客户端调用的方法,此AIDL文件编译后在build目录下会生成对应的Java代码,所以从本质上来说AIDL是系统提供了一套可快速实现Binder的工具。示例代码如下:
package com.czj.ipcdemo;
// 这里要注意,即使在同一个包下,也要把引用的类import进来
import com.czj.ipcdemo.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
AIDL文件中的数据类型要求:
- 基本数据类型:byte,int,long,float,double,boolean,char
- String类型
- CharSequence类型
- ArrayList、HashMap且里面的每个元素都能被AIDL支持
- 实现Parcelable接口的对象
- 所有AIDL接口本身
需要注意的是,AIDL中除了基本类型外,其它类型的参数必须标明传递方向:
-
in:输入型参数
表示数据只能由客户端流向服务端。
服务端将会接收到这个对象的完整数据,但在服务端修改它不会对客户端输入的对象产生影响。 -
out: 输出型参数
表示数据只能由服务端流向客户端。
服务端将会接收到这个对象的的空对象,但在服务端对接收到的空对象有任何修改之后客户端将会同步变动。 -
inout:输入输出型参数
表示数据可在服务端与客户端之间双向流通。
服务端将会接收到客户端传来对象的完整信息,且客户端将会同步服务端对该对象的任何变动。
2. 自定义Parcelable类对应AIDL文件
如果AIDL文件中用到了自定义的Parcelable对象,则必须新建一个和它同名的AIDL文件:
package com.czj.ipcdemo;
// Declare any non-default types here with import statements
parcelable Book;
3. 服务端Service实现
public class BookManagerService extends Service {
private static final String TAG = "BMS";
// CopyOnWriteArrayList支持并发,当多个客户端同时访问时
// 前面提到AIDL只支持ArrayList
// AIDL中使用的是List接口去访问,此处服务端返回CopyOnWriteArrayList实例,但是Binder会转成ArrayList给客户端,所以通过AIDL接口去获取的时候已经是ArrayList
private List<Book> mBookList = new CopyOnWriteArrayList<>();
private Binder mBinder = new IBookManager.Stub() {
@Override
public List<Book> getBookList() throws RemoteException {
return mBookList;
}
@Override
public void addBook(Book book) throws RemoteException {
mBookList.add(book);
}
};
@Override
public void onCreate() {
super.onCreate();
mBookList.add(new Book("忒修斯之船"));
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}
4. 客户端实现
public class BookManagerActivity extends AppCompatActivity {
private final static String TAG = "BookManagerActivity";
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 将服务端Binder转成AIDL接口类型
IBookManager bookManager = IBookManager.Stub.asInterface(service);
try {
// 获取服务端数据
List<Book> list = bookManager.getBookList();
Log.e(TAG, "list type : " + list.getClass().getCanonicalName());
Log.e(TAG, "book list : " + list);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.e(TAG, "onServiceDisconnected");
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_book_manager);
Intent intent = new Intent(this, BookManagerService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
unbindService(mConnection);
super.onDestroy();
}
}
输出(注意此处的List输出类型):
10-24 09:51:26.122 13500-13500/com.czj.ipcdemo E/BookManagerActivity: list type : java.util.ArrayList
10-24 09:51:26.123 13500-13500/com.czj.ipcdemo E/BookManagerActivity: book list : [[bookName:忒修斯之船]]
AIDL变量和方法解释(存在于AIDL文件编译转换成的Java类中)
-
DESCRIPTOR
Binder的唯一标识,用当前Binder的类名表示,如:com.czj.ipcdemo.IBookManager -
asInterface(android.os.IBinder obj)
客户端调用,将服务端的返回的Binder对象,转换成客户端所需要的AIDL接口类型对象。如果客户端和服务端同属一个进程,此方法返回的是服务端对象本身,否则返回系统封装后的Stub.proxy对象。 -
asBinder()
返回当前Binder对象 -
onTransact()
运行服务端的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。 -
transact()
运行在客户端,当客户端发起远程请求的同时将当前线程挂起。之后调用服务端的onTransact()直到远程请求返回,当前线程才继续执行。
Binder死亡的处理
如果服务端进程意外终止,则客户端到服务端的Binder连接也就终止了,这会导致客户端远程调用失败。
面对这种情况,有两种解决方案:
- 给Binder设置死亡监听:DeathRecipient
- 在onServiceDisconnected中重新绑定远程服务
下面给出为Binder设置死亡监听的示例:
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
if (bookManager == null) return;
bookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
bookManager = null;
// TODO 此处重新绑定远程Service
}
}
然后在客户端绑定Service的回调中,设置Binder的死亡代理:
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
IBookManager bookManager = IBookManager.Stub.asInterface(service);
// 设置死亡代理
service.linkToDeath(mDeathRecipient, 0);
......
}
......
};
Binder的跨进程传输
不知道有没有发现一个问题,就是服务端和客户端是运行在两个进程上的,那么服务端的Binder是如何传递给客户端的ServiceConnection中,显然不是通过内存直接传递Binder对象。
Binder的传递:将Binder打包进Parcel来传输,发现如果是同进程的,则收到的是原始对象,而不是对象的拷贝。如果是跨进程的,则收到的BinderProxy。
Service注册到Service Manager时,将Binder实体跨进程传输给ServiceManager,由于是跨进程的,所以必须先经过Binder驱动,驱动会保存这个Binder实体,并生成一个Binder引用返给ServiceManager。Client端如果向Service Manager请求该Service,ServiceManager会将该Service的Binder引用返回,经过Binder驱动时,如果Client和Service在同一个进程,那么Binder驱动会直接返回该Service的Binder实体,如果不在同一个进程,则Binder驱动会返回Binder的引用。
推荐参考 关于Binder的跨进程传输
4.5 ContentProvider
ContentProvider用于在多个应用程序之间共享数据,所以适合多进程通信的场景,它的底层实现也是Binder。
- 除了onCreate是在UI线程,query、update、insert、delete都运行在Binder线程
- SQLiteDatabase内部有同步处理,但是多个SQLiteDatabase的情况,要做好同步处理
4.6 Socket
Socket:套接字,能够建立网络长连接。不仅支持进程间通信,还能进行跨设备通信。
在Service进程中创建一个Socket服务,客户端去连接该服务,也能够实现进程间通信。直接看实例:
- 首先需要声明网络访问权限
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
- 服务端代码
public class TCPServerService extends Service {
private final static String TAG = "TCPServerService";
private boolean mIsDestory = false;
private String[] mReturnMsgAry = { "你好傻猫威~", "买了否冷?", "鬼刀一开,看不见,走位走位", "学猫叫" };
@Override
public void onCreate() {
Log.e(TAG, "开启服务");
new Thread(new TcpServer()).start();
super.onCreate();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
mIsDestory = true;
super.onDestroy();
}
private class TcpServer implements Runnable {
@Override
public void run() {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(9527);
Log.e(TAG, "服务已开启,正在监听客户端消息");
} catch (IOException e) {
Log.e(TAG, "run socket server failed, port:8688");
e.printStackTrace();
return;
}
while (!mIsDestory) {
try {
final Socket client = serverSocket.accept();
new Thread() {
@Override
public void run() {
// 处理客户端请求
try {
responseClient(client);
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private void responseClient(Socket client) throws IOException {
// 接收客户端消息
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
// 向客户端发送消息
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(client.getOutputStream())), true);
out.println("成功连接服务端");
while (!mIsDestory) {
String str = in.readLine();
if(str == null)
break;//客户端断开连接
int i = new Random().nextInt(mReturnMsgAry.length);
String msg = mReturnMsgAry[i];
out.println(msg);
}
in.close();
out.close();
client.close();
}
}
- 客户端代码:
public class TCPClientActivity extends AppCompatActivity implements View.OnClickListener{
private final static int MSG_RECEIVE_MSG = 1;
private final static int MSG_SOCKET_CONNECTED = 2;
private Button mSendBtn;
private TextView mMsgTextView;
private EditText mMsgEditText;
private PrintWriter mPrintWriter;
private Socket mClient;
private Handler mHander = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_RECEIVE_MSG:
mMsgTextView.setText(mMsgTextView.getText() + (String)msg.obj);
break;
case MSG_SOCKET_CONNECTED:
mSendBtn.setEnabled(true);
break;
default:
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tcpclient);
mMsgEditText = findViewById(R.id.et_msg);
mMsgTextView = findViewById(R.id.tv_content);
mSendBtn = findViewById(R.id.btn_send);
mSendBtn.setOnClickListener(this);
// 启动服务进程
Intent intent = new Intent(this, TCPServerService.class);
startService(intent);
new Thread() {
@Override
public void run() {
connectTCPServer();
}
}.start();
}
@Override
protected void onDestroy() {
if(mClient != null) {
try {
mClient.shutdownInput();
mClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
super.onDestroy();
}
private void connectTCPServer() {
Socket socket = null;
while (socket == null) {
try {
socket = new Socket("localhost", 9527);
mClient = socket;
mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
// 发送连接成功的消息
mHander.sendEmptyMessage(MSG_SOCKET_CONNECTED);
} catch (IOException e) {
SystemClock.sleep(1000);
System.out.println("连接失败,重连中...");
}
}
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while (!TCPClientActivity.this.isFinishing()) {
String msg = br.readLine();
System.out.println("receive: " + msg);
if (msg != null) {
String time = formateTime(System.currentTimeMillis());
// 追加消息
final String showMsg = "server(" + time + "):" + msg + "\n";
mHander.obtainMessage(MSG_RECEIVE_MSG, showMsg).sendToTarget();
}
}
System.out.println("断开连接...");
mPrintWriter.close();
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private String formateTime(long time) {
return new SimpleDateFormat("HH:mm:ss").format(new Date(time));
}
@Override
public void onClick(View v) {
if(v.getId() == mSendBtn.getId()) {
final String msg = mMsgEditText.getText().toString();
if(!TextUtils.isEmpty(msg) && mPrintWriter != null) {
new Thread() {
@Override
public void run() {
mPrintWriter.println(msg);
}
}.start();
mMsgEditText.setText("");
String time = formateTime(System.currentTimeMillis());
String showMsg = "client(" + time + "):" + msg + "\n";
mMsgTextView.setText(mMsgTextView.getText() + showMsg);
}
}
}
}
-
程序截图
Screenshot_2018-10-26-13-39-35-286_IPCDemo.png
IPC方式的优缺点和适用场景
名称 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Bundle | 简单易用 | 只能传输Bundle支持的数据 | 四大组件之间的进程通信 |
文件共享 | 简单易用 | 不适合高并发场景,并且无法做到进程间的即时通信 | 无并发访问,交换简单的数据,且实时性不高的场景 |
AIDL | 功能强大,支持一对多并发通信,支持实时通信 | 使用较复杂,需要处理好线程同步 | 一对多通信且有RPC需求 |
Messenger | 支持一对多串行通信,实时通讯 | 不能很好处理高并发情形,不支持RPC,数据通过Message进行传输,因此只能传输Bundle数据 | 低并发的一对多即时通信,无RPC需求,或者无须返回结果的RPC需求 |
ContentProvider | 在数据源访问方面功能强大,支持一对多并发共享数据,可通过Call方法扩展其他操作 | 可以理解为受约束的AIDL,主要提供数据源的CRUD操作 | 一对多的进程间的数据共享 |
Socket | 功能强大,可以通过网络传输数据,支持一对多并发实时通信 | 实现细节繁琐,不支持直接的RPC | 网络数据交换 |
网友评论