Android Aidl 通信案例

01.aidl通信业务需求

  • aidl多进程通信应用
    • 服务端:某app;客户端:app调试工具。
    • 注意:aidl多进程通信是指两个独立app之间的通信……
  • 打开app调试工具(客户端)
    • 可以通过绑定服务端某app的service,获取到公司app的信息,比如渠道,版本号,签名,打包时间,token等属性
    • 通过app调试工具,可以通过aidl接口中的方法设置属性,设置成功后,查看某app是否设置属性成功
    • 比如,我现在需要修改登陆用户,那么通过助手设置token成功后,则某app接收到信息后则会更换登陆用户。

02.操作步骤伪代码

  • 服务端
    • 步骤1:新建定义AIDL文件,并声明该服务需要向客户端提供的接口
    • 补充,如果aidl中有对象,则需要创建对象,并且实现Parcelable
    • 步骤2:在Service子类中实现AIDL中定义的接口方法,并定义生命周期的方法(onCreat、onBind()、blabla)
    • 步骤3:在AndroidMainfest.xml中注册服务 & 声明为远程服务
  • 客户端
    • 步骤1:拷贝服务端的AIDL文件到目录下
    • 步骤2:使用Stub.asInterface接口获取服务器的Binder,根据需要调用服务提供的接口方法
    • 步骤3:通过Intent指定服务端的服务名称和所在包,绑定远程Service

03.服务端操作步骤

  • 创建一个aidl文件【注意:在main路径下创建】

    • 可以看到里面有一个AppInfo,注意这个类需要自己创建,并且手动导包进来。否则编译时找不到……
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 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);

    }
  • 创建一个AppInfo类,实现Parcelable接口

    • 这个类就是需要用的实体类,因为是跨进程,所以实现了Parcelable接口,这个是Android官方提供的,它里面主要是靠Parcel来传递数据,Parcel内部包装了可序列化的数据,能够在Binder中自由传输数据。
    • 注意:如果用到了自定义Parcelable对象,就需要创建一个同名的AIDL文件,包名要和实体类包名一致。我之前这个地方没加,导致出现错误!
    • 如图所示:
    • image
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    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 + '\'' +
    '}';
    }
    }
  • 在Service子类中实现AIDL中定义的接口方法,并定义生命周期的方法(onCreat、onBind()等)

    • 重写的onBinde()方法中返回Binder对象,这个Binder对象指向IAdvertManager.Stub(),这个Stub类并非我们自己创建的,而是AIDL自动生成的。系统会为每个AIDL接口在build/source/aidl下生成一个文件夹,它的名称跟你命名的AIDL文件夹一样,里面的类也一样。
    • 创建binder对象,在这个getAppInfo方法中,可以设置app基本信息,方便后期多进程通信测试
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    /**
    * 用于aidl多进程通信服务service
    */
    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;
    }
    };
    }
  • 在AndroidMainfest.xml中注册服务 & 声明为远程服务

    • 在清单文件注册即可,需要设置action。这个在客户端中绑定服务service需要用到!
    1
    2
    3
    4
    5
    6
    7
    8
    <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>

04.客户端操作步骤

  • 拷贝服务端的AIDL文件到目录下

    • 注意:复制时不要改动任何东西!
    • 如图所示:
    • image
  • 通过Intent指定服务端的服务名称和所在包,绑定远程Service

    • 通过Intent指定服务端的服务名称和所在包,进行Service绑定;
    • 创建ServiceConnection对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    /**
    * 跨进程绑定服务
    */
    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();
    }
    }
    }
    };
  • 使用Stub.asInterface接口获取服务器的Binder,根据需要调用服务提供的接口方法

    • 通过步骤3.4.2完成了跨进程绑定服务,接下来通过调用方法获取到数据。这里可以调用getAppInfo方法获取到服务端[app]的数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    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();
    }
    }

05.案例测试调试

  • 最后看看通过测试工具[客户端]跨进程获取服务端app信息截图
    • 如图所示:
      image

06.可能出现的问题

6.1 客户端在子线程中发起通信访问问题

  • 当客户端发起远程请求时,客户端会挂起,一直等到服务端处理完并返回数据,所以远程通信是很耗时的,所以不能在子线程发起访问。由于服务端的Binder方法运行在Binder线程池中,所以应采取同步的方式去实现,因为它已经运行在一个线程中呢。

6.2 什么情况下会导致远程调用失败

  • Binder是会意外死亡的。如果服务端的进程由于某种原因异常终止,会导致远程调用失败,如果我们不知道Binder连接已经断裂, 那么客户端就会受到影响。不用担心,Android贴心的为我们提供了连个配对的方法linkToDeath和unlinkToDeath,通过linkToDeath我们可以给Binder设置一个死亡代理,当Binder死亡时,我们就会收到通知。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 在创建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();
    }
    };

6.3 设置aidl的权限,需要通过权限才能调用

  • 如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <!--给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;
    }

07.aidl不要做耗时操作

  • 是关于远程方法调用时的线程问题。
    • 客户端在调用远程服务的方法时,被调用的方法是运行在服务端的 Binder 线程池中,同时客户端线程会被挂起,这时如果服务端方法执行比较耗时,就会导致客户端线程被堵塞。
    • 比如使线程休眠了五秒,当点击按钮时就可以明显看到按钮有一种被“卡住了”的反馈效果,这就是因为 UI 线程被堵塞了,这可能会导致 ANR。所以如果确定远程方法是耗时的,就要避免在 UI 线程中去调用远程方法。
  • 客户端的 ServiceConnection对象的 onServiceConnectedonServiceDisconnected都是运行在 UI 线程中
    • 所以也不能用于调用耗时的远程方法。而由于服务端的方法本身就运行在服务端的 Binder 线程池中,所以服务端方法本身就可以用于执行耗时方法,不必再在服务端方法中开线程去执行异步任务。