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方式可以和它跑在同一個進程中。
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 | 網絡數據交換 |