车载Android开发-AIDL(二)

前言

在上一篇文章车载Android应用开发-AIDL(一)中,简单介绍了什么是AIDL以及如何简单使用。本篇文章将对AIDL的进阶知识进行介绍。

一、AIDL支持的数据类型

AIDL支持的数据类型包括以下几种:

1.Java中的8种基本数据类型:int,short,long,char,double,byte,float,boolean;
2.CharSequence类型,如String、SpannableString等;
3.集合类型中的ArrayList、HashMap;
4.所有Parceable接口的实现类,因为跨进程传输对象时,本质上是序列化与反序列化的过程;
5.AIDL接口,所有的AIDL接口本身也可以作为可支持的数据类型;

需要注意以下几点:

  • 集合类中的所有元素也必须是AIDL所支持的数据类型;
  • 在使用默认支持的数据类型的时候是不需要导包的,但是除了这些类型之外的数据类型,在使用之前必须导包,就算目标文件与当前正在编写的 .aidl 文件在同一个包下——在 Java 中,这种情况是不需要导包的。比如,现在我们编写了两个文件,一个叫做 Book.java ,另一个叫做 BookManager.aidl,它们都在 com.lypeer.aidldemo 包下 ,现在我们需要在 .aidl 文件里使用 Book 对象,那么我们就必须在 .aidl 文件里面写上 import com.lypeer.aidldemo.Book; 哪怕 .java 文件和 .aidl 文件就在一个包下。

二、定向tag数据流向

in、out、inout是AIDL语法中的关键字,在Android官网有这一部分的介绍:

All non-primitive parameters require a directional tag indicating which way the data goes . Either in , out , or inout . Primitives are in by default , and connot be otherwise .

翻译过来就是:所有的非基本参数都需要一个定向tag来指出数据的流向,不管是 in , out , 还是 inout 。基本参数的定向tag默认是并且只能是 in 。

这三者的区别如下:

1、in表示输入型参数:只能由客户端流向服务端,服务端收到该参数对象的完整数据,但服务端对该对象的后续修改不会影响到客户端传入的参数对象;

2、out表示输出型参数:只能由服务端流向客户端,服务端收到该参数的空对象,服务端对该对象的后续修改将同步改动到客户端的相应参数对象;

3、inout表示输入输出型参数:可在客户端与服务端双向流动,服务端接收到该参数对象的完整数据,且服务端对该对象的后续修改将同步改动到客户端的相应参数对象;

定向tag需要一定的开销,根据实际需要去确定选择什么tag,不能滥用,以避免不必要的数据拷贝和传输 。

三、创建AIDL文件报错如何检查

在创建AIDL文件时,如果有报错,通常说明某个AIDL文件书写不规范,需要检查的点有:

1、自定义对象是否实现Parceable接口;

2、引用的AIDL对象是否显式import;

3、定向tag的使用是否正确;

4、定向tag为 inout 时,自定义对象是否同时实现 writeToParcel 和 readFromParcel;

5、如果有修改过java文件的包名,检查AIDL文件的包名是否正确(是否与 applicationId 一致);

当发现问题并修改后,可以尝试 Build->Clean Project 或 Build -> Rebuild 以重新刷新或构建项目;

四、使用AIDL导致ANR

由于使用AIDL进行进程间通信是会涉及到数据的序列化/反序列化以及传输的过程,因此这种方式往往是耗时的。如果在客户端的主线程中调用AIDL接口,而服务端的方法比较耗时,就可能会导致客户端主线程阻塞,出现ANR。

可以采取以下方案来避免这种情况:

  • 不要在onServiceConnected () 或者 onServiceDisconnected ()中直接调用AIDL接口执行服务端的耗时方法,通过查看源码能够发现这两个方法都是在主线程中执行。(服务端的方法运行在服务端的Binder线程池中,所以在编写服务端代码时,不需要新建线程去执行服务端方法。)
  • 不要在客户端主线程调用AIDL接口,可以选择采取子线程或异步的方式。
  • 使用oneway修饰AIDL接口,使其调用方式转为非阻塞的。

oneway是AIDL中的关键字,它能够使远程调用的方式变为非阻塞式的,客户端无需等待服务端处理,可以立即返回。oneway可以修饰AIDL接口中的方法,也可以修饰AIDL接口。修饰接口时则该接口中所有的方法都会隐式的被oneway修饰。

使用oneway的场景通常是当你不需要等待服务端返回值或回调时,能够提高效率。

关于oneway有以下几点需要注意:

1.oneway不能用来修饰有返回值或者抛出异常的方法,因为客户端无法收到。

2.同一个IBinder对象的oneway调用,会按照调用顺序依次执行,这是因为在内核中每个IBinder对象都有一个oneway的事务队列,队列中的事务会按顺序被取出。不同IBinder对象的oneway调用则不一定会按顺序执行。

3.oneway修饰频繁调用的接口,可能会出现transaction failed。因为如果我们在短时间非常频繁的发起调用,就很容易导致来不及处理的调用申请不到共享内存。在实战中很有可能会遇到这种情况。

五、服务端向客户端回调

在基本用法中只实现了客户端向服务端发送调用请求的单向通信,但在很多场景下,同时也需要实现服务端主动向客户端发送数据进行双向通信,比如在观察者模式中,当有多个客户端绑定服务端,如果想要实现在服务端数据变化时主动通知所有与它建立绑定的客户端时,这个时候就需要用到AIDL的回调机制了。

我们需要在服务端保存一个客户端的Binder实例,在需要时可以通过这个Binder实例与客户端交互。

在服务端aidl文件夹下新建一个AIDL文件,用于定义回调接口,并声明onSuccess和onFailed方法,这两个方法是用于业务层的,比如服务端添加数据失败时调用onFailed,取决于具体场景:

// ITaskCallback.aidl
package com.sqchen.aidltest;

interface ITaskCallback {

    void onSuccess(String result);

    void onFailed(String errorMsg);
}

在IStudentService.aidl中添加register和unregister方法用于客户端注册回调和解除回调:

// IStudentService.aidl
package com.sqchen.aidltest;

import com.sqchen.aidltest.Student;
//注意:aidl接口也要显式import
import com.sqchen.aidltest.ITaskCallback;

interface IStudentService {

    List getStudentList();

    void addStudent(inout Student student);

    void register(ITaskCallback callback);

    void unregister(ITaskCallback callback);
}

StudentService.java:

package com.sqchen.aidltest;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class StudentService extends Service {

    private static final String TAG = "StudentService";

    private CopyOnWriteArrayList mStuList;

    private static RemoteCallbackList sCallbackList;

    private Binder mBinder = new IStudentService.Stub() {

        @Override
        public void register(ITaskCallback callback) throws RemoteException {
            if (callback == null) {
                Log.i(TAG, "callback == null");
                return;
            }
            sCallbackList.register(callback);
        }

        @Override
        public void unregister(ITaskCallback callback) throws RemoteException {
            if (callback == null) {
                return;
            }
            sCallbackList.unregister(callback);
        }

        @Override
        public List getStudentList() throws RemoteException {
            return mStuList;
        }

        @Override
        public void addStudent(Student student) throws RemoteException {
            if (mStuList == null) {
                dispatchResult(false, "add student failed, mStuList = null");
            } else {
                mStuList.add(student);
                dispatchResult(true, "add student successfully");
            }
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(TAG, "onCreate");
        init();
    }

    private void init() {
        mStuList = new CopyOnWriteArrayList<>();
        sCallbackList = new RemoteCallbackList<>();
    }

    /**
     * 分发结果
     * @param result
     * @param msg
     */
    private void dispatchResult(boolean result, String msg) {
        int length = sCallbackList.beginBroadcast();
        for (int i = 0; i < length; i++) {
            ITaskCallback callback = sCallbackList.getBroadcastItem(i);
            try {
                if (result) {
                    callback.onSuccess(msg);
                } else {
                    callback.onFailed(msg);
                }
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        sCallbackList.finishBroadcast();
    }

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

在StudentService.java中,Binder对象实现了IStudentService.aidl中新声明的两个方法,register和unregister,并创建了一个RemoteCallbackList。

RemoteCallbackList 是系统专门提供的用于跨进程传递callback的一种接口,这个接口是泛型,支持管理所有AIDL接口。这里不能使用普通的List来存放callback,因为在进程间通信时,客户端的List对象和服务端接收到的List对象不在不同的内存空间中。正是因为不是在同一个内存空间中,不同进程之间的数据不能进行共享,所以才有进程间通信这个机制。

那么,为什么RemoteCallbackList能实现传输前后都是相同对象呢?查看RemoteCallbackList源码可以发现,其内部创建了一个ArrayMap用于保存callback:

ArrayMap mCallbacks = new ArrayMap();

这个Map的key是IBinder对象,而value是Callback对象,当客户端通过register方法注册回调时,将callback传递给服务端,服务端再通过RemoteCallbackList.register方法真正将回调进行保存:

//RemoteCallbackList
public boolean register(E callback, Object cookie) {
    synchronized (mCallbacks) {
        if (mKilled) {
            return false;
        }
        // Flag unusual case that could be caused by a leak. b/36778087
        logExcessiveCallbacks();
        IBinder binder = callback.asBinder();
        try {
            Callback cb = new Callback(callback, cookie);
            binder.linkToDeath(cb, 0);
            mCallbacks.put(binder, cb);
            return true;
        } catch (RemoteException e) {
            return false;
        }
    }
}

将我们关心的部分抽出来:

IBinder binder = callback.asBinder();
Callback cb = new Callback(callback, cookie);
mCallbacks.put(binder, cb);

将客户端传递过来的Callback对象转为IBinder对象作为key,封装一个Callback作为value。客户端传递过来的Callback对象虽然在服务端被重新序列化生成一个对象,但它们底层的Binder对象是同一个,能够根据底层的唯一Binder来区分每个注册的接口,所以可以实现Callback的跨进程传输。

在服务端注册客户端的回调后,服务端就可以通过这个回调主动向客户端传递数据了。比如,在addStudent中,当添加数据成功时,将操作的执行结果或者其他数据分发给所有向该服务端注册监听的客户端:

/**
 * 分发结果
 * @param result
 * @param msg
 */
private void dispatchResult(boolean result, String msg) {
    int length = sCallbackList.beginBroadcast();
    for (int i = 0; i < length; i++) {
        ITaskCallback callback = sCallbackList.getBroadcastItem(i);
        try {
            if (result) {
                callback.onSuccess(msg);
            } else {
                callback.onFailed(msg);
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
    //在调用beginBroadcast之后,必须调用该方法
    sCallbackList.finishBroadcast();
}

在客户端中创建ITaskCallback对象:

//MainActivity.java

ITaskCallback mCallback = new ITaskCallback.Stub() {
    @Override
    public void onSuccess(String result) throws RemoteException {
        Log.i(TAG, "result = " + result);
    }

    @Override
    public void onFailed(String errorMsg) throws RemoteException {
        Log.e(TAG, "errorMsg = " + errorMsg);
    }
};

修改 ServiceConnection,在建立连接、调用 onServiceConnected 方法时,进行Callback的注册:

private ServiceConnection mConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        mStudentService = IStudentService.Stub.asInterface(service);
        if (mStudentService == null) {
            Log.i(TAG, "mStudentService == null");
            return;
        }
        try {
            if (mCallback != null) {
                Log.i(TAG, "mCallback != null");
                mStudentService.register(mCallback);
            } else {
                Log.i(TAG, "mCallback == null");
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {

    }
};

此时,客户端与服务端的连接已经建立,且客户端向服务端注册了回调,当客户端向服务端添加数据,服务端执行 addStudent 方法时,服务端会通过回调将添加数据的执行结果返回给客户端,从而实现了双向通信。

此外,使用 RemoteCallbackList 还能够避免 DeadObjectException,它给每个注册的接口附加了一个IBinder.DeathRecipient,这样如果接口所在的进程死亡了,它就可以从列表中清除掉。

RemoteCallbackList还具有良好的线程安全性,其内部维护了两个不同的列表:一个是注册的回调接口列表,另一个是正在通知的回调接口列表。当客户端注册或注销回调接口对象时,会先获取锁,然后将操作同步到注册的回调接口列表中。而当服务端需要通知客户端时,也会获取锁,然后将正在通知的回调接口列表设置为注册的回调接口列表的副本,以确保通知期间注册和注销回调接口对象不会影响到正在通知的列表。

六、死亡回调

当客户端与服务端之间的连接断开,我们称之为Binder死亡,此时虽然客户端和服务端都在运行,但因为连接断开,客户端发出的请求是不会得到响应的,所以我们需要知道什么时候连接断开,以便进行重新绑定,或者执行其他操作。

在看ServiceConnection的源码时我们发现,当连接断开时,会调用onServiceDisconnected方法,所以,我们可以在这个方法进行重新绑定服务。

此外,Binder中还有两个很重要的方法,linkToDeath和unlinkToDeath,通过linkToDeath我们可以给Binder设置一个死亡代理IBinder.DeathRecipient,当Binder死亡时,将会调用DeathRecipient的binderDied方法。

修改MainActivity.java,创建一个死亡代理,当客户端与服务端建立连接时,为Binder设置死亡代理:

private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
    @Override
    public void binderDied() {
        if (mStudentService == null) {
            return;
        }
        //解除死亡代理
        mStudentService.asBinder().unlinkToDeath(mDeathRecipient, 0);
        mStudentService = null;
        //重新绑定服务
        bindStudentService();
        Log.i(TAG, "binderDied, bindService again");
    }
};

private ServiceConnection mConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        mStudentService = IStudentService.Stub.asInterface(service);
        if (mStudentService == null) {
            Log.i(TAG, "mStudentService == null");
            return;
        }
        try {
            //设置死亡代理
            mStudentService.asBinder().linkToDeath(mDeathRecipient, 0);
            if (mCallback != null) {
                Log.i(TAG, "mCallback != null");
                mStudentService.register(mCallback);
            } else {
                Log.i(TAG, "mCallback == null");
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        //也可以在这里重新绑定服务
    }
};

接着,我们模拟服务端意外死亡导致连接断开的情况,进入adb shell,查找服务端进程remote的pid,并kill掉:

E:BlogsrcAIDLTest>adb shell
mido:/ # ps
USER           PID  PPID     VSZ    RSS WCHAN            ADDR S NAME
root         26112 28641    8968   1900 sigsuspend 74a7005e08 S sh
root         26116 26112   10540   1992 0          7115eac768 R ps
mido:/ # ps -A | grep com.sqchen
u0_a140      26015   745 5238588  68324 SyS_epoll_wait 7269051c40 S com.sqchen.aidltest
u0_a140      26046   745 5217176  39364 SyS_epoll_wait 7269051c40 S com.sqchen.aidltest:remote
mido:/ # kill 26046

然后,查看日志发现remote被kill之后,确实调用了DeathRecipient的binderDied方法,再次查看remote进程,观察发现remote进程的pid在被kill掉前后是不一样的,说明成功地重新绑定服务。

E:BlogsrcAIDLTest>adb shell
mido:/ # ps -A | grep com.sqchen
u0_a140      26015   745 5239648  68328 SyS_epoll_wait 7269051c40 S com.sqchen.aidltest
u0_a140      26125   745 5217176  39604 SyS_epoll_wait 7269051c40 S com.sqchen.aidltest:remote

binderDied和onServiceDisconnected的区别:

1、binderDied早于onServiceDisconnected被调用(参考:linkToDeath机制了解和使用)

2、binderDied在客户端的Binder线程池被调用,不能在这个方法中访问UI,而onServiceDisconnected在客户端的UI线程被调用;

你可能感兴趣的:(#,知识体系,android)