Aidl程式間通訊詳細介紹

瀟湘劍雨發表於2018-09-27

目錄介紹

  • 1.問題答疑
  • 2.Aidl相關屬性介紹
    • 2.1 AIDL所支援的資料型別
    • 2.2 服務端和客戶端
    • 2.3 AIDL的基本概念
  • 3.實際開發中案例操作
    • 3.1 aidl通訊業務需求
    • 3.2 操作步驟虛擬碼
    • 3.3 服務端操作步驟
    • 3.4 客戶端操作步驟
    • 3.5 測試
  • 4.可能出現的問題
    • 4.1 客戶端在子執行緒中發起通訊訪問問題
    • 4.2 什麼情況下會導致遠端呼叫失敗
    • 4.3 設定aidl的許可權,需要通過許可權才能呼叫
  • 5.部分原始碼解析
    • 5.1 服務端aidl編譯生成的java檔案
    • 5.2 客戶端繫結服務端service原理

關於aidl應用案例

1.問題答疑

  • 1.1.0 AIDL所支援的資料型別有哪些?
  • 1.1.1 提供給客戶端連線的service什麼時候執行?
  • 1.1.2 Stub類是幹什麼用的呢?
  • 1.1.3 如何解決遠端呼叫失敗的問題?

2.Aidl相關屬性介紹

2.1 AIDL所支援的資料型別

  • 在AIDL中,並非支援所有資料型別,他支援的資料型別如下所示:
    • 基本資料型別(int、long、char、boolean、double、float、byte、short)
    • String和CharSequence
    • List:只支援ArrayList,並且裡面的每個元素必須被AIDL支援
    • Map: 只支援HashMap, 同樣的,裡面的元素都必須被AIDL支援,包括key和value
    • Parcelable:所有實現了Parcelable介面的物件
    • AIDL: 所有的AIDL介面本身也可以在AIDL 檔案中使用

2.2 服務端和客戶端

  • 2.2.1 服務端

    • 注意:服務端就是你要連線的程式。服務端給客戶端一個Service,在這個Service中監聽客戶端的連線請求,然後建立一個AIDL介面檔案,裡面是將要實現的方法,注意這個方法是暴露給客戶端的的。在Service中實現這個AIDL介面即可
  • 2.2.2 客戶端

    • 客戶端首先需要繫結服務端的Service,繫結成功後,將服務端返回的Binder物件轉換成AIDL介面所屬的型別,最後呼叫AIDL的方法就可以了。

2.3 AIDL的基本概念

  • AIDL:Android Interface Definition Language,即Android介面定義語言;用於讓某個Service與多個應用程式元件之間進行跨程式通訊,從而可以實現多個應用程式共享同一個Service的功能。

3.實際開發中案例操作

3.1 aidl通訊業務需求

  • aidl多程式通訊應用——服務端:某app;客戶端:app除錯工具。注意:aidl多程式通訊是指兩個獨立app之間的通訊……
  • 開啟app除錯工具,可以通過繫結服務端某app的service,獲取到公司app的資訊,比如渠道,版本號,簽名,打包時間,token等屬性
  • 通過app除錯工具,可以通過aidl介面中的方法設定屬性,設定成功後,檢視某app是否設定屬性成功

3.2 操作步驟虛擬碼

  • 3.2.1 服務端

    • 步驟1:新建定義AIDL檔案,並宣告該服務需要向客戶端提供的介面
    • 補充,如果aidl中有物件,則需要建立物件,並且實現Parcelable
    • 步驟2:在Service子類中實現AIDL中定義的介面方法,並定義生命週期的方法(onCreat、onBind()、blabla)
    • 步驟3:在AndroidMainfest.xml中註冊服務 & 宣告為遠端服務
  • 3.2.2 客戶端

    • 步驟1:拷貝服務端的AIDL檔案到目錄下
    • 步驟2:使用Stub.asInterface介面獲取伺服器的Binder,根據需要呼叫服務提供的介面方法
    • 步驟3:通過Intent指定服務端的服務名稱和所在包,繫結遠端Service

3.3 服務端操作步驟

  • 3.3.1 建立一個aidl檔案【注意:在main路徑下建立】
    • 可以看到裡面有一個AppInfo,注意這個類需要自己建立,並且手動導包進來。否則編譯時找不到……
// ICheckAppInfoManager.aidl
package cn.ycbjie.ycaudioplayer;
import cn.ycbjie.ycaudioplayer.AppInfo;
// Declare any non-default types here with import statements

interface ICheckAppInfoManager {

    //獲取app資訊,比如token,版本號,簽名,渠道等資訊
    List<AppInfo> getAppInfo(String sign);
    
    boolean setToken(String sign,String token);

    boolean setChannel(String sign,String channel);

    boolean setAppAuthorName(String sign,String name);

}
複製程式碼
  • 3.3.2 建立一個AppInfo類,實現Parcelable介面
    • 這個類就是需要用的實體類,因為是跨程式,所以實現了Parcelable介面,這個是Android官方提供的,它裡面主要是靠Parcel來傳遞資料,Parcel內部包裝了可序列化的資料,能夠在Binder中自由傳輸資料。
    • 注意:如果用到了自定義Parcelable物件,就需要建立一個同名的AIDL檔案,包名要和實體類包名一致。我之前這個地方沒加,導致出現錯誤!
    • 如圖所示:
  • image
import android.os.Parcel;
import android.os.Parcelable;

public class AppInfo  implements Parcelable {

    private String key;
    private String value;

    public AppInfo(String key, String value) {
        this.key = key;
        this.value = value;
    }

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(this.key);
        dest.writeString(this.value);
    }

    public AppInfo() {
    }

    protected AppInfo(Parcel in) {
        this.key = in.readString();
        this.value = in.readString();
    }

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

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

    @Override
    public String toString() {
        return "AppInfo{" +
                "key='" + key + '\'' +
                ", value='" + value + '\'' +
                '}';
    }
}
複製程式碼
  • 3.3.3 在Service子類中實現AIDL中定義的介面方法,並定義生命週期的方法(onCreat、onBind()等)
    • 重寫的onBinde()方法中返回Binder物件,這個Binder物件指向IAdvertManager.Stub(),這個Stub類並非我們自己建立的,而是AIDL自動生成的。系統會為每個AIDL介面在build/source/aidl下生成一個資料夾,它的名稱跟你命名的AIDL資料夾一樣,裡面的類也一樣。
    • 建立binder物件,在這個getAppInfo方法中,可以設定app基本資訊,方便後期多程式通訊測試
/**
 * <pre>
 *     @author yangchong
 *     blog  :
 *     time  : 2018/05/30
 *     desc  : 用於aidl多程式通訊服務service
 *     revise:
 * </pre>
 */
public class AppInfoService extends Service {

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        LogUtils.i("AppInfoService--IBinder:");
        return binder;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        LogUtils.i("AppInfoService--onCreate:");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        LogUtils.i("AppInfoService--onDestroy:");
    }

    /**
     * 1.核心,Stub裡面的方法執行的binder池中。
     * 2.Stub類並非我們自己建立的,而是AIDL自動生成的。
     *   系統會為每個AIDL介面在build/generated/source/aidl下生成一個資料夾,它的名稱跟你命名的AIDL資料夾一樣
     * 3.Stub類,是一個內部類,他本質上是一個Binder類。當服務端和客戶端位於同一個程式時,方法呼叫不會走跨程式的transact過程,
     *   當兩者處於不同晉城市,方法呼叫走transact過程,這個邏輯由Stub的內部代理類Proxy完成。
     */
    private final IBinder binder = new ICheckAppInfoManager.Stub() {
        @Override
        public List<AppInfo> getAppInfo(String sign) throws RemoteException {
            List<AppInfo> list=new ArrayList<>();
            String aidlCheckAppInfoSign = AppToolUtils.getAidlCheckAppInfoSign();
            LogUtils.e("AppInfoService--AppInfoService",aidlCheckAppInfoSign+"-------------"+sign);
            if(!aidlCheckAppInfoSign.equals(sign)){
                return list;
            }
            list.add(new AppInfo("app版本號(versionName)", BuildConfig.VERSION_NAME));
            list.add(new AppInfo("app版本名稱(versionCode)", BuildConfig.VERSION_CODE+""));
            list.add(new AppInfo("打包時間", BuildConfig.BUILD_TIME));
            list.add(new AppInfo("app包名", getPackageName()));
            list.add(new AppInfo("app作者", SPUtils.getInstance(Constant.SP_NAME).getString("name","楊充")));
            list.add(new AppInfo("app渠道", SPUtils.getInstance(Constant.SP_NAME).getString("channel")));
            list.add(new AppInfo("token", SPUtils.getInstance(Constant.SP_NAME).getString("token")));
            list.add(new AppInfo("App簽名", AppToolUtils.getSingInfo(getApplicationContext(), getPackageName(), AppToolUtils.SHA1)));
            return list;
        }


        @Override
        public boolean setToken(String sign, String token) throws RemoteException {
            if(!AppToolUtils.getAidlCheckAppInfoSign().equals(sign)){
                return false;
            }
            SPUtils.getInstance(Constant.SP_NAME).put("token",token);
            LogUtils.i("AppInfoService--setToken:"+ token);
            return true;
        }

        @Override
        public boolean setChannel(String sign, String channel) throws RemoteException {
            if(!AppToolUtils.getAidlCheckAppInfoSign().equals(sign)){
                return false;
            }
            SPUtils.getInstance(Constant.SP_NAME).put("channel",channel);
            LogUtils.i("AppInfoService--setChannel:"+ channel);
            return true;
        }

        @Override
        public boolean setAppAuthorName(String sign, String name) throws RemoteException {
            if(!AppToolUtils.getAidlCheckAppInfoSign().equals(sign)){
                return false;
            }
            SPUtils.getInstance(Constant.SP_NAME).put("name",name);
            LogUtils.i("AppInfoService--setAppAuthorName:"+ name);
            return true;
        }
    };
}
複製程式碼
  • 3.3.4 在AndroidMainfest.xml中註冊服務 & 宣告為遠端服務
    • 在清單檔案註冊即可,需要設定action。這個在客戶端中繫結服務service需要用到!
<service android:name=".service.AppInfoService"
    android:process=":remote"
    android:exported="true">
    <intent-filter>
        <action android:name="cn.ycbjie.ycaudioplayer.service.aidl.AppInfoService"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</service>
複製程式碼

3.4 客戶端操作步驟

  • 3.4.1 拷貝服務端的AIDL檔案到目錄下

    • 注意:複製時不要改動任何東西!
    • 如圖所示:
      image
  • 3.4.2 通過Intent指定服務端的服務名稱和所在包,繫結遠端Service

    • 通過Intent指定服務端的服務名稱和所在包,進行Service繫結;
    • 建立ServiceConnection物件
    /**
     * 跨程式繫結服務
     */
    private void attemptToBindService() {
        Intent intent = new Intent();
        //通過Intent指定服務端的服務名稱和所在包,與遠端Service進行繫結
        //引數與伺服器端的action要一致,即"伺服器包名.aidl介面檔名"
        intent.setAction("cn.ycbjie.ycaudioplayer.service.aidl.AppInfoService");
        //Android5.0後無法只通過隱式Intent繫結遠端Service
        //需要通過setPackage()方法指定包名
        intent.setPackage(packName);
        //繫結服務,傳入intent和ServiceConnection物件
        bindService(intent, connection, Context.BIND_AUTO_CREATE);
    }
    
    
    /**
     * 建立ServiceConnection的匿名類
     */
    private ServiceConnection connection = new ServiceConnection() {
        //重寫onServiceConnected()方法和onServiceDisconnected()方法
        // 在Activity與Service建立關聯和解除關聯的時候呼叫
        @Override public void onServiceDisconnected(ComponentName name) {
            Log.e(getLocalClassName(), "無法繫結aidlServer的AIDLService服務");
            mBound = false;
        }
    
        //在Activity與Service建立關聯時呼叫
        @Override public void onServiceConnected(ComponentName name, IBinder service) {
            Log.e(getLocalClassName(), "完成繫結aidlServer的AIDLService服務");
            //使用IAppInfoManager.Stub.asInterface()方法獲取伺服器端返回的IBinder物件
            //將IBinder物件傳換成了mAIDL_Service介面物件
            messageCenter = ICheckAppInfoManager.Stub.asInterface(service);
            mBound = true;
            if (messageCenter != null) {
                try {
                    //連結成功
                    Toast.makeText(MainActivity.this,"連結成功",Toast.LENGTH_SHORT).show();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    };
    複製程式碼
  • 3.4.3 使用Stub.asInterface介面獲取伺服器的Binder,根據需要呼叫服務提供的介面方法

    • 通過步驟3.4.2完成了跨程式繫結服務,接下來通過呼叫方法獲取到資料。這裡可以呼叫getAppInfo方法獲取到服務端[app]的資料
    private void getAppInfo() {
        //如果與服務端的連線處於未連線狀態,則嘗試連線
        if (!mBound) {
            attemptToBindService();
            Toast.makeText(this, "當前與服務端處於未連線狀態,正在嘗試重連,請稍後再試",
                    Toast.LENGTH_SHORT).show();
            return;
        }
        if (messageCenter == null) {
            return;
        }
        try {
            List<AppInfo> info = messageCenter.getAppInfo(Utils.getSign(packName));
            if(info==null || (info.size()==0)){
                Toast.makeText(this, "無法獲取資料,可能是簽名錯誤!", Toast.LENGTH_SHORT).show();
            }else {
                mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
                FirstAdapter adapter = new FirstAdapter(info, this);
                mRecyclerView.setAdapter(adapter);
                adapter.setOnItemClickListener(new FirstAdapter.OnItemClickListener() {
                    @Override
                    public void onItemClick(View view, int position) {
    
                    }
                });
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
    複製程式碼

3.5 測試

  • 最後看看通過測試工具[客戶端]跨程式獲取服務端app資訊截圖
    • 具體可以通過實際案例操作:後來發現跨程式通訊原來挺好玩的……專案地址:github.com/yangchong21…
    • 如圖所示:
      image

4.可能出現的問題

4.1 客戶端在子執行緒中發起通訊訪問問題

  • 當客戶端發起遠端請求時,客戶端會掛起,一直等到服務端處理完並返回資料,所以遠端通訊是很耗時的,所以不能在子執行緒發起訪問。由於服務端的Binder方法執行在Binder執行緒池中,所以應採取同步的方式去實現,因為它已經執行在一個執行緒中呢。

4.2 什麼情況下會導致遠端呼叫失敗

  • Binder是會意外死亡的。如果服務端的程式由於某種原因異常終止,會導致遠端呼叫失敗,如果我們不知道Binder連線已經斷裂, 那麼客戶端就會受到影響。不用擔心,Android貼心的為我們提供了連個配對的方法linkToDeath和unlinkToDeath,通過linkToDeath我們可以給Binder設定一個死亡代理,當Binder死亡時,我們就會收到通知。
// 在建立ServiceConnection的匿名類中的onServiceConnected方法中
// 設定死亡代理
messageCenter.asBinder().linkToDeath(deathRecipient, 0);


/**
 * 給binder設定死亡代理
 */
private IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() {

    @Override
    public void binderDied() {
        if(messageCenter == null){
            return;
        }
        messageCenter.asBinder().unlinkToDeath(deathRecipient, 0);
        messageCenter = null;
        //這裡重新繫結服務
        attemptToBindService();
    }
};
複製程式碼

4.3 設定aidl的許可權,需要通過許可權才能呼叫

<!--給aidl多程式通訊,服務加入許可權驗證功能-->
<permission android:name="aidl.AppInfoService"
    android:protectionLevel="normal"/>
    

//在AppInfoService服務中驗證許可權
@Nullable
@Override
public IBinder onBind(Intent intent) {
    LogUtils.i("AppInfoService--IBinder:");
    int check = checkCallingOrSelfPermission("aidl.AppInfoService");
    if(check == PackageManager.PERMISSION_DENIED){
        return null;
    }
    return binder;
}
複製程式碼

5.部分原始碼解析

5.1 服務端aidl編譯生成的java檔案

  • 5.1.1 首先找到aidl編譯生成的Java檔案

    image

  • 5.1.2 分析生成的java檔案

    • 這個ICheckAppInfoManager.java就是系統為我們生成的相應java檔案,簡單說下這個類。它宣告瞭三個方法getAppInfo,setToken和setChannel,分明就是我們AIDL介面中的三個方法。同時他宣告瞭3個id用來標識這幾個方法,id用於標識在transact過程中客戶端請求的到底是哪個方法。接著就是我們的Stub,可以看到它是一個內部類,他本質上是一個Binder類。當服務端和客戶端位於同一個程式時,方法呼叫不會走跨程式的transact過程,當兩者處於不同晉城市,方法呼叫走transact過程,這個邏輯由Stub的內部代理類Proxy完成。
    • 這個Stub物件之所以裡面有我們AIDL的介面,正是因為官方替我們做好了,我們只要在這裡具體實現就好了。

5.2 客戶端繫結服務端service原理

  • 客戶端也非常簡單,首先我們連線到服務端Service,在連線成功時,也就是onServiceConnected方法裡,通過asInterface(service)方法可以將服務端的Binder物件轉換成客戶端所需的AIDL的介面的物件。這種轉換是區分程式的,如果是同一程式,那麼此方法返回的就是Stub本身,否則返回的就是系統Stub.proxy物件。拿到介面物件之後,我們就能夠呼叫相應方法進行自己的處理

參考文章

關於其他內容介紹

01.關於部落格彙總連結

02.關於我的部落格

相關文章