型別 | 描述 | 用時 |
---|---|---|
選題 | silencezwm | 0.5小時 |
寫作時間 | 2017年10月25日 | 5.5小時 |
審稿 | silencezwm、Mya婷婷 | 2小時 |
校對上線 | silencezwm | 1小時 |
Tips:4個環節,共計約9小時的精心打磨完成上線,同時也非常感謝參與審稿的同學。
看漫畫,漲薪資(¥) >>>【小豬的傳奇一生】:
該漫畫描述了小豬仔出生後,就生活在豬圈中,快樂的成長著。有一天,小豬長大了,屠宰場的老闆就會通過船將肥豬運輸過來進行屠宰,然後將豬肉銷往世界各地。
看完該漫畫後,是不是覺得小豬仔的一生有點小悲涼,要怪就怪可惡的人類,無肉不歡,哈哈。
精彩的漫畫背後,總隱藏著一絲絲技術的小知識。本文將為你介紹“Android跨程式通訊”的相關知識,通過本文的學習,你可以瞭解到:
一、單程式通訊與多程式通訊之間的區別
二、跨程式通訊常見的五種實現方式
三、跨程式通訊的注意事項
一、單程式通訊與多程式通訊之間的區別
概念普及:IPC(Inter-Process Communication)機制,即程式間通訊或者跨程式通訊機制,是指兩個程式之間進行資料交換的過程。
1.1、單程式通訊
在很久以前,小豬仔從生到死都是在豬圈中生活的,沒有外來者的入侵。同樣的,在Android開發中,預設情況下,程式執行後,都只是執行在一個以專案包名建立的主程式中(就好比豬圈),例如
專案包名為:com.silencezwm.ipcdemo
預設程式名:com.silencezwm.ipcdemo
複製程式碼
單程式通訊就如:不同品類的豬仔(Android中不同的元件)在相同的豬圈(在同一程式中)中生活(執行)。
1.2、多程式通訊
突然有一天,河流的右邊來了一個商人,他發現河對岸有不少肥豬在遊蕩。於是,他發現了一個發家致富的機會,他在這裡建了一個屠宰場。然後經常通過船將對岸的肥豬運送過來,進行屠宰,賺的盆滿缽滿。
這個就類似Android程式,本來只有一個程式在執行,但是因為產品提了個奇葩的需求,使得我們程式猿們不得不多開一個程式來實現該需求。
程式猿們一頓牢騷後,最後還是動手幹活了。
他們在AndroidManifest.xml檔案中相應的元件新增了android:process屬性,並指定程式名。然後開啟了兩個Activity,但是BActivity被指定執行在新的程式,當程式跑起來後,此時可以看到有兩個程式正在執行,如圖:
我們知道,在單程式中通訊,元件間是可以隨意進行通訊,因為它們都處於同一個記憶體空間。那多程式之間是怎樣通訊的呢?
漫畫中,屠宰場的老闆通過船將河對岸的肥豬運送過來,就因為船的存在,該老闆就可以跨越河流到達對岸。那麼,Android跨程式通訊中,我們也需要擁有同樣功能的船,它就是Binder,通過Binder的中轉,程式之間就能順利的進行資料交換了。
二、跨程式通訊常見的五種實現方式
五種常見的實現方式可分為兩大類:四大元件的跨程式通訊和AIDL。
2.1、四大元件
Activity、Service、BroadcastReceiver、Content Provider四大元件只需要在AndroidManifest.xml檔案相應的元件中新增android:process屬性,並指定程式名。程式執行起來後,它們就會執行在不同的程式中,它們之間的通訊,官方已經給我們做了非常好的封裝,所以使用起來也非常方便,這裡就不多做解釋了。
2.2、AIDL
概念普及:AIDL(Android interface definition Language),即Android介面定義語言。
要想應用AIDL技術,就至少需要有兩個程式存在,A程式通過定義的AIDL介面檔案與B程式進行通訊,具體的實現步驟如下:
1、準備兩個程式:新建一個專案 IPCDemo,然後新建一個Module IPCClient,這樣我們就準備好了兩個程式,完成後的專案結構如圖:
2、建立AIDL檔案:在服務端IPCDemo中新建一個AIDL介面檔案:IMyAidlInterface.aidl,其中會預設實現basicTypes方法,然後我們再定義一個login方法,IMyAidlInterface.aidl完整程式碼如下;
package com.silencezwm.ipcdemo;
/**
* 定義的AIDL介面檔案
*/
interface IMyAidlInterface {
/**
* AIDL預設實現的方法
*/
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,double aDouble, String aString);
/**
* 定義了一個登入方法,包含使用者名稱、密碼兩個引數
*/
void login(String username, String password);
}
複製程式碼
3、build專案:此時,Android Studio會自動為你生成一個繼承自IInterface的java檔案,IMyAidlInterface.java完整程式碼如下;
package com.silencezwm.ipcdemo;
/**
* 定義的AIDL介面檔案
*/
public interface IMyAidlInterface extends android.os.IInterface {
/**
* Local-side IPC implementation stub class.
*/
public static abstract class Stub extends android.os.Binder implements com.silencezwm.ipcdemo.IMyAidlInterface {
private static final java.lang.String DESCRIPTOR = "com.silencezwm.ipcdemo.IMyAidlInterface";
/**
* Construct the stub at attach it to the interface.
*/
public Stub() {
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an com.silencezwm.ipcdemo.IMyAidlInterface interface,
* generating a proxy if needed.
*/
public static com.silencezwm.ipcdemo.IMyAidlInterface asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof com.silencezwm.ipcdemo.IMyAidlInterface))) {
return ((com.silencezwm.ipcdemo.IMyAidlInterface) iin);
}
return new com.silencezwm.ipcdemo.IMyAidlInterface.Stub.Proxy(obj);
}
@Override
public android.os.IBinder asBinder() {
return this;
}
@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
switch (code) {
case INTERFACE_TRANSACTION: {
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_basicTypes: {
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
long _arg1;
_arg1 = data.readLong();
boolean _arg2;
_arg2 = (0 != data.readInt());
float _arg3;
_arg3 = data.readFloat();
double _arg4;
_arg4 = data.readDouble();
java.lang.String _arg5;
_arg5 = data.readString();
this.basicTypes(_arg0, _arg1, _arg2, _arg3, _arg4, _arg5);
reply.writeNoException();
return true;
}
case TRANSACTION_login: {
data.enforceInterface(DESCRIPTOR);
java.lang.String _arg0;
_arg0 = data.readString();
java.lang.String _arg1;
_arg1 = data.readString();
this.login(_arg0, _arg1);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
private static class Proxy implements com.silencezwm.ipcdemo.IMyAidlInterface {
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote) {
mRemote = remote;
}
@Override
public android.os.IBinder asBinder() {
return mRemote;
}
public java.lang.String getInterfaceDescriptor() {
return DESCRIPTOR;
}
/**
* AIDL預設實現的方法
*/
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, java.lang.String aString) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(anInt);
_data.writeLong(aLong);
_data.writeInt(((aBoolean) ? (1) : (0)));
_data.writeFloat(aFloat);
_data.writeDouble(aDouble);
_data.writeString(aString);
mRemote.transact(Stub.TRANSACTION_basicTypes, _data, _reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
/**
* 定義了一個登入方法,包含使用者名稱、密碼兩個引數
*/
@Override
public void login(java.lang.String username, java.lang.String password) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeString(username);
_data.writeString(password);
mRemote.transact(Stub.TRANSACTION_login, _data, _reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
}
static final int TRANSACTION_basicTypes = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_login = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
}
/**
* AIDL預設實現的方法
*/
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, java.lang.String aString) throws android.os.RemoteException;
/**
* 定義了一個登入方法,包含使用者名稱、密碼兩個引數
*/
public void login(java.lang.String username, java.lang.String password) throws android.os.RemoteException;
}
複製程式碼
4、新建Service:繼續在伺服器端src/main/java/包名目錄下新建一個Service,並在配置檔案中註冊,然後設定其action值為com.silencezwm.ipcdemo,以供客戶端進行呼叫,並在Service內部建立一個內部類繼承靜態抽象類IMyAidlInterface.Stub,AidlService.java完整程式碼如下:
package com.silencezwm.ipcdemo;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
public class AidlService extends Service {
private static final String TAG = AidlService.class.getName();
public AidlService() {
}
@Override
public IBinder onBind(Intent intent) {
return new MyBinder();
}
class MyBinder extends IMyAidlInterface.Stub {
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
Log.d(TAG, "=====:basicTypes");
}
@Override
public void login(String username, String password) throws RemoteException {
Log.d(TAG, "=====:login" + username + "==" + password);
}
}
}
複製程式碼
5、拷貝aidl目錄檔案:至此,服務端的程式碼編寫完成,接下來我們只需要完成客戶端的呼叫程式碼即可。相當簡單,首先把服務端aidl整個資料夾拷貝到客戶端src/main目錄下(至於拷貝的原因,稍後會進行闡述),然後build專案。此時,客戶端、服務端就同時擁有AIDL相同的程式碼;
6、繫結服務端Service:在客戶端你想要的地方通過服務端Service所在地的包名以及action來進行繫結,然後將Service連線成功後返回的IBinder物件,通過IMyAidlInterface.Stub.asInterface方法轉換為我們定義的aidl物件,然後根據該物件呼叫我們所定義的方法即可完成整個通訊過程,客戶端MainActivity.java呼叫程式碼如下:
package com.silencezwm.ipcclient;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import com.silencezwm.ipcdemo.IMyAidlInterface;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button mBtnLogin;
private IMyAidlInterface mIMyAidlInterface;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBtnLogin = (Button) findViewById(R.id.btn_login);
mBtnLogin.setOnClickListener(this);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.btn_login:
Intent intent = new Intent();
// 服務端AndroidManifest.xml檔案該Service所配置的action
intent.setAction("com.silencezwm.ipcdemo");
// Service所在的包名
intent.setPackage("com.silencezwm.ipcdemo");
bindService(intent, new ConnectCallBack(), Context.BIND_AUTO_CREATE);
break;
}
}
class ConnectCallBack implements ServiceConnection{
// 服務連線成功回撥
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
mIMyAidlInterface = IMyAidlInterface.Stub.asInterface(iBinder);
login();
}
// 服務斷開連線回撥
@Override
public void onServiceDisconnected(ComponentName componentName) {
mIMyAidlInterface = null;
}
}
private void login() {
try {
mIMyAidlInterface.login("silencezwm", "123456");
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
複製程式碼
7、結果驗證:收穫的季節來了,先將服務端程式跑起來,之後將客戶端程式跑起來,點選登入按鈕,不出意外的話,我們已經繫結了服務端的Service,並呼叫了login方法,將資料傳遞到服務端了,來看看Log的列印資訊:
回顧AIDL整個實現過程,其實並不複雜。此時,聰明的人往往都會有一個疑問:
客戶端的資料到底是如何傳遞到服務端的呢?
複製程式碼
接下來,我們就來一探究竟,以下的程式碼主要涉及到自動生成的IMyAidlInterface.java檔案。
在客戶端呼叫程式碼中,我們知道,一旦繫結Service成功後,會返回一個IBinder物件,呼叫IMyAidlInterface.Stub.asInterface(iBinder)方法將該物件轉換為了我們所定義的AIDL介面物件,該方法具體做了什麼呢?來看看:
private static final java.lang.String DESCRIPTOR = "com.silencezwm.ipcdemo.IMyAidlInterface";
public Stub() {
this.attachInterface(this, DESCRIPTOR);
}
public static com.silencezwm.ipcdemo.IMyAidlInterface asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof com.silencezwm.ipcdemo.IMyAidlInterface))) {
return ((com.silencezwm.ipcdemo.IMyAidlInterface) iin);
}
return new com.silencezwm.ipcdemo.IMyAidlInterface.Stub.Proxy(obj);
}
複製程式碼
上面這段程式碼中,首先Stub構造方法被呼叫,跟著attachInterface方法被呼叫:
private IInterface mOwner;
private String mDescriptor;
...
public void attachInterface(IInterface owner, String descriptor) {
mOwner = owner;
mDescriptor = descriptor;
}
複製程式碼
相當好理解,就是進行介面物件和字串識別符號的賦值。接下來在asInterface方法中,會根據識別符號去IBinder的本地去查詢是否有該物件,也就是呼叫obj.queryLocalInterface(DESCRIPTOR)方法,繼續原始碼中Binder.java
public IInterface queryLocalInterface(String descriptor) {
if (mDescriptor.equals(descriptor)) {
return mOwner;
}
return null;
}
複製程式碼
意思就是如果本地存在這個識別符號的IInterface物件,那就直接返回之前構造方法中初始化的mOwner物件,否則返回null,因為我們這裡涉及到了跨程式通訊,所以這裡會直接返回null。程式碼繼續往下走,很顯然,以下這段程式碼會呼叫:
return new com.silencezwm.ipcdemo.IMyAidlInterface.Stub.Proxy(obj);
複製程式碼
程式碼字面意思就是返回IBinder的程式碼物件,如下:
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote) {
mRemote = remote;
}
複製程式碼
到這裡,我們就拿到了一個IBinder的代理物件,通過代理物件,我們就可以呼叫之前所定義的login方法啦,程式碼:
@Override
public void login(java.lang.String username, java.lang.String password) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeString(username);
_data.writeString(password);
mRemote.transact(Stub.TRANSACTION_login, _data, _reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
複製程式碼
這裡就涉及到了一個重要的類Parcel,Parcel天生具備跨程式傳輸資料能力。在文章開頭的漫畫中,可不是直接把豬仔趕上船就行的,萬一豬仔亂跑掉河裡去了怎麼辦,所以屠宰場老闆就準備了些豬籠。首先將豬仔趕進豬籠中,待船靠岸後,開啟豬籠,將豬仔放出來即可。我們這裡的Parcel就好比豬籠,我們把需要傳遞的資料寫入Parcel中,然後到達目標程式後,將Parcel中的資料讀出即可,所以可以將Parcel稱為資料傳輸載體。Parcel支援的資料型別非常之多,足以滿足我們日常開發所需。
現在你知道客戶端的資料是如何傳遞到服務端了嗎?
三、跨程式通訊的注意事項
3.1、客戶端與服務端aidl檔案以及包名必須一致,否則無法正常通訊。
3.2、在繫結服務端Service的時候,intent最好設定目標Service所在的包名,如intent.setPackage("com.silencezwm.ipcdemo"),當SDK版本大於14的時候,你會碰到這個錯誤 java.lang.IllegalArgumentException: Service Intent must be explicit:。
3.3、跨程式傳遞實體類必須進行序列化,不信你試試看。
3.4、Parcel所佔用的記憶體,會隨著你傳遞的資料量大小而相應變化。
好啦,本篇“Android跨程式通訊”的相關介紹就到這裡了,感謝你的到來!