IPC机制<二>AIDL

来源:互联网 发布:excel表格数据连接 编辑:程序博客网 时间:2024/05/02 05:06

上次讲到IPC机制的几种通信方式,但是没有讲完,还剩下几种方式没有讲,今天继续。

在上次讲到Service的时候,说过AIDL,但是那种只是非常简单的方式,今天将会详细的讲述AIDL。

一、AIDL 中的数据类型

aidl支持的数据类型:

  1. 基本数据类型(int,long,char,boolean,double等)
  2. String和CharSequence
  3. List:只支持ArrayList,里面的每个元素都必须能够被AIDL支持。
  4. Map:只支持HashMap,里面的每个元素都必须能够被AIDL支持
  5. Parcelable:所有实现了Parcelable接口的对象。
  6. AIDL:所有的AIDL接口本身也可以在AIDL中使用。

注意:

  1. 自定义的Parcelable对象和AIDL对象必须要显示import进来,不管他们是否在同一个包中。

    package cn.demo.zx_aidl_learn;import cn.demo.zx_aidl_learn.domain.Book;interface IService {    List<Book>getBookList();    void addBook(in Book b);}
  2. 如果AIDL文件中用到了自定义的Parcelable对象,那么必须新建一个和它同名的AIDL文件,并在其中声明为parcelable类型。

  3. 在AIDL中处理基本数据类型,其他类型的参数必须标上方向:in,out或者inout。in表示输入型参数,out表示输出型参数,inout表示输入输出型参数。
  4. AIDL接口中只支持方法不支持声明静态常量。
  5. AIDL的包结构在服务端和客户端要保持一致,否则运行会出错,这是因为客户端需要反序列化服务端中和AIDL接口相关的所有类,如果类的完整路径不一样的话,就无法成功反序列化。

案例:

服务端:

public class BookManagerService extends Service {    /**     * 使用CopyOnWriteArrayList的原因:     * 因为CopyOnWriteArrayList支持并发读/写,AIDL方法是在服务端的Binder的线程池中执行的,     * 因此当多个客户端同时连接的时候,会存在多个线程同时访问的情形,所以我们要在AIDL方法中     * 处理线程同步,而我们这里直接使用CopyOnWriteArrayList来进行自动的线程同步。     *     * AIDL中能够使用的List只有ArrayList,但是这里为什么还使用它呢?     * 因为AIDL中所支持的是抽象的List,而List只是一个接口,因此虽然服务端返回的是CopyOnWriteArrayList,     * 但是在Binder中会按照List的规范去访问数据并最终返回一个新的ArrayList传递给客户端。还有就是类似的是     * ConcurrentHashMap.     */    CopyOnWriteArrayList<Book>mBookList = new CopyOnWriteArrayList<>();    public BookManagerService() {    }    @Override    public IBinder onBind(Intent intent) {        return mBinder;    }    @Override    public void onCreate() {        super.onCreate();        mBookList.add(new Book(1,"android"));        mBookList.add(new Book(2,"ios学习"));    }    private Binder mBinder = new IService.Stub(){        @Override        public List<Book> getBookList() throws RemoteException {            return mBookList;        }        @Override        public void addBook(Book b) throws RemoteException {            mBookList.add(b);        }    };}

客户端:

public class MainActivity extends AppCompatActivity {    private IService mService = null;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        Intent intent = new Intent(this,BookManagerService.class);        startService(intent);        bindService(intent,conn,BIND_AUTO_CREATE);    }    public void findBook(View v){    /**     * 注意:服务端的方法有可能是耗时的方法,那么下面的调用就有可能早能ANR异常,需要注意。     */        try {            List<Book> bookList = mService.getBookList();            if(bookList!=null){                for(Book b : bookList){                    System.out.println("书籍::"+b.toString());                }            }        } catch (RemoteException e) {            e.printStackTrace();        }    }    private ServiceConnection conn = new ServiceConnection() {        @Override        public void onServiceConnected(ComponentName name, IBinder service) {            mService = IService.Stub.asInterface(service);        }        @Override        public void onServiceDisconnected(ComponentName name) {        }    };}

清单文件AndroidMenifest:

<application    android:allowBackup="true"    android:icon="@mipmap/ic_launcher"    android:label="@string/app_name"    android:supportsRtl="true"    android:theme="@style/AppTheme">    <activity android:name=".MainActivity">        <intent-filter>            <action android:name="android.intent.action.MAIN"/>            <category android:name="android.intent.category.LAUNCHER"/>        </intent-filter>    </activity>    <service        android:name=".BookManagerService"        android:enabled="true"        android:exported="true"        android:process=":romote">    </service></application>

上述案例只是讲述客户端从服务端获取图书信息,如果是在服务端每添加一本书籍就向客户端发送消息,那么怎么实现呢?

这里就会用到观察者模式。

二、AIDL中的观察者模式

创建一个aidl接口,每个用户都需要实现这个接口并且向图书馆申请新书的提醒功能。

新创建的aidl文件:

import cn.demo.zx_aidl_learn.domain.Book;interface IOnNewBookArrivedListener {    void onNewBookArrived(in Book newBook);}import cn.demo.zx_aidl_learn.domain.Book;import cn.demo.zx_aidl_learn.IOnNewBookArrivedListener;interface IService {    List<Book>getBookList();    void addBook(in Book b);    void registerListener(in IOnNewBookArrivedListener listener);    void unregisterListener(in IOnNewBookArrivedListener listener);}

在MainActivity中创建一个IOnNewBookArrivedListener对象,然后通过注册的方式将IOnNewBookArrivedListener对象传递给服务端:

private ServiceConnection conn = new ServiceConnection() {    @Override    public void onServiceConnected(ComponentName name, IBinder service) {        mService = IService.Stub.asInterface(service);        try {            /**             * 注册对新书的监控             */            mService.registerListener(listener);        } catch (RemoteException e) {            e.printStackTrace();        }    }    @Override    public void onServiceDisconnected(ComponentName name) {    }};private IOnNewBookArrivedListener listener = new IOnNewBookArrivedListener.Stub() {    @Override    public void onNewBookArrived(Book newBook) throws RemoteException {        System.out.println("新到的图书::"+newBook.toString());        mHandler.obtainMessage(0,newBook).sendToTarget();    }};

服务端通过获取传入的IOnNewBookArrivedListener对象来调用客户端中的方法来通知客户端:

/** * 发送新书已到的消息到客户端 * 怎样通知呢? * 通过客户端在主持监听的时候传入的IOnNewBookArrivedListener对象来通知客户端 * @param newBook */private void sendNewBookArrivedToClient(Book newBook){    mBookList.add(newBook);    for(int i=0;i<mListenerList.size();i++){        IOnNewBookArrivedListener listener = mListenerList.get(i);        try {            listener.onNewBookArrived(newBook);        } catch (RemoteException e) {            e.printStackTrace();        }    }}

详细功能实现请查看源码

三、取消注册功能

当我们实现了注册功能之后,怎样取消注册功能呢?

@Overrideprotected void onDestroy() {    super.onDestroy();    if(mService!=null || mService.asBinder().isBinderAlive()){        try {            mService.unregisterListener(listener);        } catch (RemoteException e) {            e.printStackTrace();        }    }    unbindService(conn);}

我们通过上述方法无法取消注册。这是为什么呢?

这是因为在客户端传入的IOnNewBookArrivedListener对象是同一个对象,但是在服务端却又变成了另外的一个对象了。因为对象无法跨进程通信,服务端接收的IOnNewBookArrivedListener对象是通过序列化之后得到对象,所以无法取消注册,这种情况怎么处理呢?

这里我们就会用到RemoteCallbackList类,它是系统专门提供的用于删除跨进程listener的接口,它是一个泛型,支持管理任意的AIDL接口,因为它继承了IInterface接口,aidl接口都继承IInterface。

它的工作原理非常简单,就是在它的内部有一个专门用于保存所有的AIDL回调的Map集合:

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

其中Callback中封装了真正的远程listener,当客户端注册listener的时候,它会把这个listener的信息存入mCallback中,其中key和value分别通过下面的方式获得:

private final class Callback implements IBinder.DeathRecipient。。。。public boolean register(E callback, Object cookie) {    synchronized (mCallbacks) {        if (mKilled) {            return false;        }        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;        }    }}

到这里就应该知道,虽然在客户端传入的同一对象在服务端会生成了不同的对象,但是它的底层的Binder对象却没有变化,利用这个特性就可以实现上面的功能了。当客户端取消注册的时候,我们会便利所有的listener接口,找到与客户端传入的listener具有相同Binder对象的服务端listener,然后把它删除即可。

服务端的代码:

RemoteCallbackList<IOnNewBookArrivedListener>mListenerList = new RemoteCallbackList<>();@Override    public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {//        if(!mListenerList.contains(listener)){//           mListenerList.add(listener);//        }        mListenerList.register(listener);    }    @Override    public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {//        if(mListenerList.contains(listener)){//            mListenerList.remove(listener);//        }        mListenerList.unregister(listener);    }/** * 发送新书已到的消息到客户端 * 怎样通知呢? * 通过客户端在主持监听的时候传入的IOnNewBookArrivedListener对象来通知客户端 * @param newBook */private void sendNewBookArrivedToClient(Book newBook){    mBookList.add(newBook);//        for(int i=0;i<mListenerList.size();i++){    //            IOnNewBookArrivedListener listener = mListenerList.get(i);    //            try {    //                listener.onNewBookArrived(newBook);    //            } catch (RemoteException e) {    //                e.printStackTrace();    //            }    //        }    int N = mListenerList.beginBroadcast();    for(int i=0;i<N;i++){        IOnNewBookArrivedListener listener = mListenerList.getBroadcastItem(i);        try {            listener.onNewBookArrived(newBook);        } catch (RemoteException e) {            e.printStackTrace();        }    }    mListenerList.finishBroadcast();}

四、设置死亡代理

关于死亡代理,以前讲过,这里在说明一下,当客户端在与服务端进行跨进程通信的时候,服务端因为异常而被销毁,但是服务端却不知道,那么怎样解决这个问题呢,这里就讲到了重新连接服务的两种方法,第一种就是死亡代理:

private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {    @Override    public void binderDied() {        mService.asBinder().unlinkToDeath(mDeathRecipient,0);        mService = null;        //重新连接        bindService(intent,conn,BIND_AUTO_CREATE);    }};private ServiceConnection conn = new ServiceConnection() {    @Override    public void onServiceConnected(ComponentName name, IBinder service) {        mService = IService.Stub.asInterface(service);        try {            service.linkToDeath(mDeathRecipient,0);        } catch (RemoteException e) {            e.printStackTrace();        }        try {            /**             * 注册对新书的监控             */            mService.registerListener(listener);        } catch (RemoteException e) {            e.printStackTrace();        }    }    @Override    public void onServiceDisconnected(ComponentName name) {    }};

还有一种方法就是在onServiceDisconnected中重新连接绑定服务。

五、权限验证

我们不想让所有的客户端访问我们的服务端,我们希望具有某些权限的客户端才能访问,怎样实现?

第一种方法,在onBind中进行验证。验证不通过就返回null,造成无法连接服务端。这种验证很多,这里介绍其中一种permission权限验证,在服务端定义一个权限。

AndroidMenifest:

<permission android:name="cn.demo.zx_aidl_learn.YANZHENG"/>

服务端:

@Overridepublic IBinder onBind(Intent intent) {    int check = checkCallingOrSelfPermission("cn.demo.zx_aidl_learn.YANZHENG");    if(check== PackageManager.PERMISSION_DENIED){        return null;    }    return mBinder;}

如果想要访问这个服务端的话,那么客户端就需要设置权限:

<uses-permission android:name="cn.demo.zx_aidl_learn.YANZHENG"/>

另一种方法,这种方法就是在服务端的onTransact方法中进行权限验证,如果验证失败返回false。验证的方法也是很多,可以通过permission验证,也可以通过包名验证。permission验证的方法与上述onBind方法中差不多,唯一的区别就是返回false。包名验证就通过getCallinngUidgetCallingPid获取客户端的uid和pid从而获取包名,进行验证。

String[] packageNames = getPackageManager().getPacagesForUid(getCallingUid());if(!packageNames[0].startsWith("cn.demo")){    return false;}

注意

当客户端访问服务端的方法时候,客户端会等待跨进程通信,如果服务端中的方法是一个耗时的方法的话,那么客户端就需要在子线程中去访问,如果在UI线程中,容易造成ANR异常。服务端访问客户端的方法也是一样的。

源码下载:

https://github.com/zhangxun1900/my_ipc_learn/

0 0
原创粉丝点击