為了賬號安全,請及時綁定郵箱和手機立即綁定

AndroidIPC機制(3)-AIDL

2019.04.06 15:26 1305瀏覽

一、概述

AIDL 意思即 Android Interface Definition Language,翻譯過來就是Android接口定義語言,是用于定義服務器和客戶端通信接口的一種描述語言,可以拿來生成用于 IPC 的代碼。從某種意義上說 AIDL 其實是一個模板,因為在使用過程中,實際起作用的并不是 AIDL 文件,而是據此而生成的一個 IInterface 的實例代碼,AIDL 其實是為了避免我們重復編寫代碼而出現的一個模板

設計 AIDL 這門語言的目的就是為了實現進程間通信。在 Android 系統中,每個進程都運行在一塊獨立的內存中,在其中完成自己的各項活動,與其他進程都分隔開來。可是有時候我們又有應用間進行互動的需求,比較傳遞數據或者任務委托等,AIDL 就是為了滿足這種需求而誕生的。通過 AIDL,可以在一個進程中獲取另一個進程的數據和調用其暴露出來的方法,從而滿足進程間通信的需求

通常,暴露方法給其他應用進行調用的應用稱為服務端,調用其他應用的方法的應用稱為客戶端,客戶端通過綁定服務端的 Service 來進行交互

二、語法

AIDL 的語法十分簡單,與Java語言基本保持一致,需要記住的規則有以下幾點:

  1. AIDL文件以 .aidl 為后綴名
  2. AIDL支持的數據類型分為如下幾種:
    • 八種基本數據類型:byte、char、short、int、long、float、double、boolean
    • String,CharSequence
    • 實現了Parcelable接口的數據類型
    • List 類型。List承載的數據必須是AIDL支持的類型,或者是其它聲明的AIDL對象
    • Map類型。Map承載的數據必須是AIDL支持的類型,或者是其它聲明的AIDL對象
  3. AIDL文件可以分為兩類。一類用來聲明實現了Parcelable接口的數據類型,以供其他AIDL文件使用那些非默認支持的數據類型。還有一類是用來定義接口方法,聲明要暴露哪些接口給客戶端調用,定向Tag就是用來標注這些方法的參數值
  4. 定向Tag。定向Tag表示在跨進程通信中數據的流向,用于標注方法的參數值,分為 in、out、inout 三種。其中 in 表示數據只能由客戶端流向服務端, out 表示數據只能由服務端流向客戶端,而 inout 則表示數據可在服務端與客戶端之間雙向流通。此外,如果AIDL方法接口的參數值類型是:基本數據類型、String、CharSequence或者其他AIDL文件定義的方法接口,那么這些參數值的定向 Tag 默認是且只能是 in,所以除了這些類型外,其他參數值都需要明確標注使用哪種定向Tag
  5. 明確導包。在AIDL文件中需要明確標明引用到的數據類型所在的包名,即使兩個文件處在同個包名下

現在,我來模擬一種 IPC 的流程
服務端**(com.czy.aidl_server)向外提供了進行數學計算的能力(其實就是對兩個整數進行相乘)。客戶端(com.czy.aidl_client)**需要進行計算時就將數據(包含了一個整數值的序列化類)傳遞給服務端進行運算,運算結果會返回給客戶端。注意,服務端和客戶端是兩個不同的應用,因此自然也是處于不同的進程中,以此來進行 IPC

三、服務端

服務端是提供運算操作能力的一方,所以除了需要設定運算參數的格式外,還需要提供運算方法
此處,用 Parameter 類作為運算參數

/**
 * 作者:leavesC
 * 時間:2019/4/4 10:46
 * 描述:包含一個進行運算操作的 int 類型數據
 */
public class Parameter implements Parcelable {

    private int param;

    public Parameter(int param) {
        this.param = param;
    }

    public int getParam() {
        return param;
    }

    public void setParam(int param) {
        this.param = param;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(this.param);
    }

    protected Parameter(Parcel in) {
        this.param = in.readInt();
    }

    public static final Parcelable.Creator<Parameter> CREATOR = new Parcelable.Creator<Parameter>() {
        @Override
        public Parameter createFromParcel(Parcel source) {
            return new Parameter(source);
        }

        @Override
        public Parameter[] newArray(int size) {
            return new Parameter[size];
        }
    };

}

相對應的 AIDL 文件

package leavesc.hello.aidl_server;

parcelable Parameter;

此外,還需要一個向外暴露運算方法的 AIDL 接口

package leavesc.hello.aidl_server;

import leavesc.hello.aidl_server.Parameter;

interface IOperationManager {

   //接收兩個參數,并將運算結果返回給客戶端
   Parameter operation(in Parameter parameter1 , in Parameter parameter2);

}

然后,在 Service 中進行實際的運算操作,并將運算結果返回

/**
 * 作者:葉應是葉
 * 時間:2018/3/18 17:35
 * 描述:https://github.com/leavesC
 */
public class AIDLService extends Service {

    private static final String TAG = "AIDLService";

    private IOperationManager.Stub stub = new IOperationManager.Stub() {
        @Override
        public Parameter operation(Parameter parameter1, Parameter parameter2) throws RemoteException {
            Log.e(TAG, "operation 被調用");
            int param1 = parameter1.getParam();
            int param2 = parameter2.getParam();
            return new Parameter(param1 * param2);
        }
    };

    public AIDLService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        return stub;
    }

}

這樣,服務端的接口就設計好了,文件目錄如下所示

四、客戶端

將服務端的兩個 AILD 文件以及 Parameter 類復制到客戶端,保持文件路徑(包名)不變

文件目錄如下所示

指定服務端的包名和 Service 路徑,綁定服務,向其傳遞兩個待運算參數并將運算結果展示出來

/**
 * 作者:葉應是葉
 * 時間:2018/3/18 17:51
 * 描述:https://github.com/leavesC
 * 客戶端
 */
public class MainActivity extends AppCompatActivity {

    private EditText et_param1;

    private EditText et_param2;

    private EditText et_result;

    private IOperationManager iOperationManager;

    private ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            iOperationManager = IOperationManager.Stub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            iOperationManager = null;
        }
    };


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        bindService();
    }

    private void bindService() {
        Intent intent = new Intent();
        intent.setClassName("com.czy.aidl_server", "com.czy.aidl_server.AIDLService");
        bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
    }

    private void initView() {
        et_param1 = findViewById(R.id.et_param1);
        et_param2 = findViewById(R.id.et_param2);
        et_result = findViewById(R.id.et_result);
        Button btn_operation = findViewById(R.id.btn_operation);
        btn_operation.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (TextUtils.isEmpty(et_param1.getText()) || TextUtils.isEmpty(et_param2.getText())) {
                    return;
                }
                int param1 = Integer.valueOf(et_param1.getText().toString());
                int param2 = Integer.valueOf(et_param2.getText().toString());
                Parameter parameter1 = new Parameter(param1);
                Parameter parameter2 = new Parameter(param2);
                if (iOperationManager != null) {
                    try {
                        Parameter resultParameter = iOperationManager.operation(parameter1, parameter2);
                        et_result.setText("運算結果: " + resultParameter.getParam());
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (serviceConnection != null) {
            unbindService(serviceConnection);
        }
    }

}

運行結果如下所示

可以看到,得到了正確的運算結果了,這就完成了一次簡單的 IPC :客戶端將參數傳遞給了服務端,服務端接收參數并進行計算,并將計算結果返回給客戶端

五、注冊回調函數

在上一節的例子里的運算操作只是將參數進行乘法操作,當然能夠很快獲得返回值,但如果是要進行耗時操作,那這種方式就不太合適了,所以可以以注冊回調函數的方式來獲取運算結果。即客戶端向服務端注冊一個回調函數用于接收運算結果,而不用傻乎乎地一直等待返回值

因此,首先需要先聲明一個 AIDL 接口 IOnOperationCompletedListener,用于傳遞運算結果

package com.czy.aidl_server;

import com.czy.aidl_server.Parameter;

interface IOnOperationCompletedListener {

    void onOperationCompleted(in Parameter result);

}

IOperationManager 的**operation** 方法改為無返回值,新增注冊回調函數和解除注冊函數的方法

package com.czy.aidl_server;

import com.czy.aidl_server.Parameter;
import com.czy.aidl_server.IOnOperationCompletedListener;

interface IOperationManager {

   void operation(in Parameter parameter1 , in Parameter parameter2);

   void registerListener(in IOnOperationCompletedListener listener);

   void unregisterListener(in IOnOperationCompletedListener listener);

}

operation 方法中讓線程休眠五秒,模擬耗時操作,然后再將運算結果傳遞出去

/**
 * 作者:葉應是葉
 * 時間:2018/3/18 17:35
 * 描述:https://github.com/leavesC
 */
public class AIDLService extends Service {

    private static final String TAG = "AIDLService";

    private CopyOnWriteArrayList<IOnOperationCompletedListener> copyOnWriteArrayList;

    private IOperationManager.Stub stub = new IOperationManager.Stub() {
        @Override
        public void operation(Parameter parameter1, Parameter parameter2) throws RemoteException {
            try {
                Log.e(TAG, "operation 被調用,延時5秒,模擬耗時計算");
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int param1 = parameter1.getParam();
            int param2 = parameter2.getParam();
            Parameter result = new Parameter(param1 * param2);
            for (IOnOperationCompletedListener listener : copyOnWriteArrayList) {
                listener.onOperationCompleted(result);
            }
            Log.e(TAG, "計算結束");
        }

        @Override
        public void registerListener(IOnOperationCompletedListener listener) throws RemoteException {
            Log.e(TAG, "registerListener");
            if (!copyOnWriteArrayList.contains(listener)) {
                Log.e(TAG, "注冊回調成功");
                copyOnWriteArrayList.add(listener);
            } else {
                Log.e(TAG, "回調之前已注冊");
            }
        }

        @Override
        public void unregisterListener(IOnOperationCompletedListener listener) throws RemoteException {
            Log.e(TAG, "unregisterListener");
            if (copyOnWriteArrayList.contains(listener)) {
                copyOnWriteArrayList.remove(listener);
                Log.e(TAG, "解除注冊回調成功");
            } else {
                Log.e(TAG, "該回調沒有被注冊過");
            }
        }
    };

    public AIDLService() {
        copyOnWriteArrayList = new CopyOnWriteArrayList<>();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return stub;
    }

}

客戶端這邊一樣要修改相應的 AIDL 文件
新增兩個按鈕用于注冊和解除注冊回調函數,并在回調函數中展示運算結果

/**
 * 作者:葉應是葉
 * 時間:2018/3/18 17:51
 * 描述:https://github.com/leavesC
 * 客戶端
 */
public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    private EditText et_param1;

    private EditText et_param2;

    private EditText et_result;

    private IOperationManager iOperationManager;

    private IOnOperationCompletedListener completedListener = new IOnOperationCompletedListener.Stub() {
        @Override
        public void onOperationCompleted(Parameter result) throws RemoteException {
            et_result.setText("運算結果: " + result.getParam());
        }
    };

    private ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            iOperationManager = IOperationManager.Stub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            iOperationManager = null;
        }
    };


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        bindService();
    }

    private void bindService() {
        Intent intent = new Intent();
        intent.setClassName("com.czy.aidl_server", "com.czy.aidl_server.AIDLService");
        bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
    }

    private void initView() {
        et_param1 = findViewById(R.id.et_param1);
        et_param2 = findViewById(R.id.et_param2);
        et_result = findViewById(R.id.et_result);
        Button btn_registerListener = findViewById(R.id.btn_registerListener);
        Button btn_unregisterListener = findViewById(R.id.btn_unregisterListener);
        Button btn_operation = findViewById(R.id.btn_operation);
        View.OnClickListener clickListener = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                switch (v.getId()) {
                    case R.id.btn_registerListener: {
                        if (iOperationManager != null) {
                            try {
                                iOperationManager.registerListener(completedListener);
                            } catch (RemoteException e) {
                                e.printStackTrace();
                            }
                        }
                        break;
                    }
                    case R.id.btn_unregisterListener: {
                        if (iOperationManager != null) {
                            try {
                                iOperationManager.unregisterListener(completedListener);
                            } catch (RemoteException e) {
                                e.printStackTrace();
                            }
                        }
                        break;
                    }
                    case R.id.btn_operation: {
                        if (TextUtils.isEmpty(et_param1.getText()) || TextUtils.isEmpty(et_param2.getText())) {
                            return;
                        }
                        int param1 = Integer.valueOf(et_param1.getText().toString());
                        int param2 = Integer.valueOf(et_param2.getText().toString());
                        Parameter parameter1 = new Parameter(param1);
                        Parameter parameter2 = new Parameter(param2);
                        if (iOperationManager != null) {
                            try {
                                iOperationManager.operation(parameter1, parameter2);
                            } catch (RemoteException e) {
                                e.printStackTrace();
                            }
                        }
                        break;
                    }
                }
            }
        };
        btn_registerListener.setOnClickListener(clickListener);
        btn_unregisterListener.setOnClickListener(clickListener);
        btn_operation.setOnClickListener(clickListener);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (serviceConnection != null) {
            unbindService(serviceConnection);
        }
    }

}

運行結果如下所示:

六、正確使用 AIDL 回調接口

在上面的代碼中我提供了一個按鈕用于解除回調函數,但當點擊按鈕時,Logcat 卻會打印出如下信息

該回調沒有被注冊過?但在注冊回調函數和解除回調函數時,使用的都是同個對象啊!其實,這是因為回調函數被序列化了的原因,Binder 會把客戶端傳過來的對象序列化后轉為一個新的對象傳給服務端,即使客戶端使用的一直是同個對象,但對服務端來說前后兩個回調函數其實都是兩個完全不相關的對象,對象的跨進程傳輸本質上都是序列化與反序列化的過程

為了能夠無誤地注冊和解除注冊回調函數,系統為開發者提供了 RemoteCallbackList,RemoteCallbackList 是一個泛型類,系統專門提供用于刪除跨進程回調函數,支持管理任意的 AIDL 接口,因為所有的 AIDL 接口都繼承自 IInterface,而 RemoteCallbackList 對于泛型類型有限制

	public class RemoteCallbackList<E extends IInterface>

RemoteCallbackList 在內部有一個 ArrayMap 用于 保存所有的 AIDL 回調接口

	ArrayMap<IBinder, Callback> mCallbacks  = new ArrayMap<IBinder, Callback>();

其中 Callback 封裝了真正的遠程回調函數,因為即使回調函數經過序列化和反序列化后會生成不同的對象,但這些對象的底層 Binder 對象是同一個。利用這個特征就可以通過遍歷 RemoteCallbackList 的方式刪除注冊的回調函數了
此外,當客戶端進程終止后,RemoteCallbackList 會自動移除客戶端所注冊的回調接口。而且 RemoteCallbackList 內部自動實現了線程同步的功能,所以我們使用它來注冊和解注冊時,不需要進行線程同步

以下就來修改代碼,改為用 RemoteCallbackList 來存儲 AIDL 接口

    //聲明
    private RemoteCallbackList<IOnOperationCompletedListener> callbackList;

注冊接口和解除注冊接口

		@Override
        public void registerListener(IOnOperationCompletedListener listener) throws RemoteException {
            callbackList.register(listener);
            Log.e(TAG, "registerListener 注冊回調成功");
        }

        @Override
        public void unregisterListener(IOnOperationCompletedListener listener) throws RemoteException {
            callbackList.unregister(listener);
            Log.e(TAG, "unregisterListener 解除注冊回調成功");
        }

遍歷回調接口

			//在操作 RemoteCallbackList 前,必須先調用其 beginBroadcast 方法
            //此外,beginBroadcast 必須和 finishBroadcast配套使用
            int count = callbackList.beginBroadcast();
            for (int i = 0; i < count; i++) {
                IOnOperationCompletedListener listener = callbackList.getBroadcastItem(i);
                if (listener != null) {
                    listener.onOperationCompleted(result);
                }
            }
            callbackList.finishBroadcast();

按照上面的代碼來修改后,客戶端就可以正確地解除所注冊的回調函數了

還有一個地方需要強調下,是關于遠程方法調用時的線程問題。客戶端在調用遠程服務的方法時,被調用的方法是運行在服務端的 Binder 線程池中,同時客戶端線程會被掛起,這時如果服務端方法執行比較耗時,就會導致客戶端線程被堵塞。就如果上一節我為了模擬耗時計算,使線程休眠了五秒,當點擊按鈕時就可以明顯看到按鈕有一種被“卡住了”的反饋效果,這就是因為 UI 線程被堵塞了,這可能會導致 ANR。所以如果確定遠程方法是耗時的,就要避免在 UI 線程中去調用遠程方法。
所以,客戶端調用遠程方法 operation 的操作可以放到子線程中進行

    new Thread(new Runnable() {
        @Override
        public void run() {
            Parameter parameter1 = new Parameter(param1);
            Parameter parameter2 = new Parameter(param2);
            if (iOperationManager != null) {
                try {
                    iOperationManager.operation(parameter1, parameter2);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
                }
            }
    }).start();

此外,客戶端的 ServiceConnection對象的 onServiceConnectedonServiceDisconnected都是運行在 UI 線程中,所以也不能用于調用耗時的遠程方法。而由于服務端的方法本身就運行在服務端的 Binder 線程池中,所以服務端方法本身就可以用于執行耗時方法,不必再在服務端方法中開線程去執行異步任務

同理,當服務端需要調用客戶端的回調接口中的方法時,被調用的方法也運行在客戶端的 Binder 線程池中,所以一樣不可以在服務端中調用客戶端的耗時方法

最后,我們還需要考慮一個問題,那就是安全問題。假設有人反編譯了服務端應用的代碼,取得了 AIDL 接口,知道了應用的包名以及 Service 路徑名后,就可以直接通過 AIDL 直接調用服務端的遠程方法了,這當然不是應用開發者所希望面對的,因此服務端就需要對請求連接的客戶端進行權限驗證了

Android 平臺下的權限驗證機制我在以前的文章中有介紹過,這里不再贅述,可以參考這兩篇文章的內容:

這里提供本系列文章所有的 IPC 示例代碼:IPCSamples

點擊查看更多內容

本文原創發布于慕課網 ,轉載請注明出處,謝謝合作

4人點贊

若覺得本文不錯,就分享一下吧!

評論

相關文章推薦

正在加載中
意見反饋 幫助中心 APP下載
官方微信

舉報

0/150
提交
取消
lpl竞猜