使用AIDL实现IPC

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/CodingEnding/article/details/71244274

前言

实现Android中的IPC(进程间通信)有多种方式,而AIDL则是其中功能最强大的一种方式。通过AIDL,我们不仅可以在进程间传递数据,还可以实现RPC(远程方法调用)。很多时候,一些业务需求只能使用AIDL这种方式进行实现,因此掌握AIDL的使用至关重要。本文就简单讲解一下如何使用AIDL实现IPC

基础概念

支持的数据类型

首先,我们要知道AIDL支持哪些数据类型,如下所示:

  • 基本数据类型
  • StringCharSequence
  • 实现Parcelable接口的对象
  • List,准确来说是ArrayList,并要求包含的元素也是AIDL支持的数据类型
  • Map,准确来说是HashMap,并要求键、值都是AIDL支持的数据类型
  • 其它AIDL接口对应的对象

AIDL主要就支持这6种数据。

AIDL接口的创建

知道了AIDL支持那些数据类型,接下来就以AndroidStudio为例讲解AIDL接口如何创建。我们需要先创建一个aidl文件,文件的后缀名是.aidl。在这个文件中,我们需要书写提供给客户端的接口。需要注意的是,在这个接口中只能有方法,不能提供静态常量。此时我们需要执行Build->Clean ProjectAndroidStudio会借助这个aidl文件自动生成对应的Java文件。实际上,我们后续使用的就是这个自动生成的Java文件中的类。以下是一个简单的aidl文件:

//IBookManager.aidl
package com.example.ipcdemo.aidl;
import com.example.ipcdemo.aidl.Book;

interface IBookManager {
    List<Book> getBookList();
    void addBook(in Book book);
}

可以看到,在这个aidl文件中,我们声明了一个AIDL接口,并声明了两个方法。需要注意的是,如果在接口中使用了自定义的类对象,那么必须将这个类显式import进来。此外,如果将自定义的类对象作为参数,那么必须指明它是什么类型的参数,可以选择in、out、inout三者中的一种。比如本例中的Book类,需要显式地导入,并且指明其为in类型,即输入型参数。

import com.example.ipcdemo.aidl.Book;
void addBook(in Book book);

Book类实现了Parcelable接口,其结构如下:

public class Book implements Parcelable {
    private int bookId;
    private String name;

    public Book(int bookId, String name) {
        this.bookId=bookId;
        this.name = name;
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(bookId);
        dest.writeString(name);
    }

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

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

    //用于反序列化Book的私有构造方法
    private Book(Parcel source){
        bookId=source.readInt();
        name=source.readString();
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "bookId:"+bookId+" bookName:"+name;
    }
}

ParcelableAIDL支持的数据类型之一,用于实现对象的序列化。关于对象序列化的具体内容,可以参考这一篇博客:

Android中的序列化方式

需要注意的是,在AIDL中使用的自定义类,必须为它们创建对应的aidl文件,在这个文件中声明它是Parcelable对象。比如本例中的Book类,需要为其创建Book.aidl文件。文件内容如下:

// Book.aidl
package com.example.ipcdemo.aidl;
parcelable Book;

到这里,一个基本的AIDL接口就写好了。然后,我们需要执行AndroidStudioBuild->Clean ProjectIDE会自动帮我们生成对应的Java文件。这个自动生成的文件名字就叫IBookManager.java,和AIDL文件的名字保持一致。在这个文件中有一个静态内部类,名字叫Stub,它继承了Binder类,并实现了IBookManager接口。实际上,我们后续使用的都是这个Stub类的实例。关于这一过程的细节,可以参考这篇博客:

深入理解AIDL(先挖个坑,有空就填)

小提示:

关于aidl的文件结构,这里给出一个建议。通过AndroidStudioNew->AIDL方式创建aidl文件后,IDE会自动为我们生成一个aidl文件夹,它和java文件夹是同级的。在这个文件夹中,存在和Java文件夹相同的主包结构。因此,我们可以在这个文件夹下再创建一个名为aidl的文件夹,然后将所有aidl文件都放到这个文件夹中。此外,我们还需要在Java文件夹中也创建一个名为aidl的文件夹,然后将所有AIDL接口会使用到的自定义类都放到这个文件夹中。这样,aidl文件java文件的结构就保持一致了。同时,这也方便了将aidl移植到其他应用中。直接将这两个包全部复制过去即可,减少了出错的可能。在上面的例子中,最终的文件结构如下:

aidl文件结构

基本流程

服务器端

首先,需要提供一个Service,用于监听客户端的绑定请求。其次,我们需要创建自己的AIDL接口,并在Service中实例化这个接口对应的Stub对象。最后,在ServiceonBind方法中,我们需要将这个AIDL实例对象作为Binder返回。

客户端

首先,我们需要绑定远程服务端,并将绑定后返回的IBinder对象转化为AIDL接口。通过这个对象,我们就可以跨进程调用服务端的方法了。

一个典型的例子

服务器端:

public class ServerService extends Service {
    private static final String TAG="ServerService";
    private CopyOnWriteArrayList<Book> bookList=new CopyOnWriteArrayList<>();

    private IBinder binder=new IBookManager.Stub(){
        @Override
        public List<Book> getBookList() throws RemoteException {
            return bookList;
        }

        @Override
        public void addBook(Book book) throws RemoteException {
            bookList.add(book);
        }
    };

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

    @Override
    public void onCreate() {
        super.onCreate();
        //初始化数据
        bookList.add(new Book(1,"小王子"));
        bookList.add(new Book(2,"长尾理论"));
    }
}

可以看到,我们在Service中实例化了一个AIDL接口对应的对象,在这里实际上是通过IBookManager.Stub来进行实例化的。IBookManagerIBookManager.Stub都是IDE根据aidl文件自动生成的。在匿名内部类中,我们重写了接口中的两个方法。在onBind方法中,我们将这个AIDL对象作为Binder返回。

此外,我们实例化了一个CopyOnWriteArrayList对象,用于存储Book数据。这里之所以不使用ArrayList,是因为AIDL中的方法运行在服务器端的Binder线程池中,多个客户端可以并发请求,这就带来了线程同步的问题。使用CopyOnWriteArrayList,支持并发读,就解决了线程同步的问题。但是上文提到过,AIDL只支持ArrayList,那为什么这里可以使用CopyOnWriteArrayList呢?原因在于,虽然这里使用的是CopyOnWriteArrayList,但是AIDL会按照List的规则去读取数据,最后返回给客户端的就是一个ArrayList。同样的,我们也可以使用ConcurrentHashMap,道理是一样的。

之后,我们为这个Service指定私有进程名称,让它运行在一个独立的进程中。

<service
    android:name=".ServerService"
    android:process=":remote">
</service>

客户端:

public class ClientActivity extends AppCompatActivity {
    private static final String TAG="ClientActivity";

    private IBookManager bookManager;

    private ServiceConnection serviceConnection=new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            bookManager=IBookManager.Stub.asInterface(service);
            try {
                List<Book> bookList=bookManager.getBookList();
                Log.i(TAG,bookList.toString());
                bookManager.addBook(new Book(3,"野火集"));
                List<Book> newBookList=bookManager.getBookList();
                Log.i(TAG,"添加新的书:"+newBookList.toString());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_client);

        Intent intent=new Intent(this,ServerService.class);
        bindService(intent,serviceConnection,BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(serviceConnection);//解绑
    }
}

可以看到,首先要绑定远程服务端。在onServiceConnnected方法中,我们将返回的IBinder对象转化为对应的AIDL接口。通过这个接口,我们就可以调用服务端的方法了。在onDestroy方法中,我们则进行了解绑操作。

private IBookManager bookManager;
bookManager=IBookManager.Stub.asInterface(service);

需要注意的是,服务器端的方法运行在Binder线程池中,我们在客户端调用服务器端方法后,客户端的线程就被阻塞了,直到服务器端的方法执行完毕才会恢复。如果服务器端的方法中执行了耗时操作,而客户端是在UI线程发起的请求,应用就可能出现ANR问题。因此,正确的做法是在客户端通过异步请求的方式调用服务器端的方法

运行结果:

.../com.example.ipcdemo I/ClientActivity: [bookId:1 bookName:小王子, bookId:2 bookName:长尾理论]
.../com.example.ipcdemo I/ClientActivity: 添加新的书:[bookId:1 bookName:王子, bookId:2 bookName:长尾理论, bookId:3 bookName:野火集]

实现观察者模式

上面提供了一个典型的AIDL例子,通过这种方式我们可以向服务器端添加Book,也可以查询Book列表。但是这都是客户端的主动操作,很多时候我们需要服务端主动提醒客户端Book列表是否改变。考虑这种场景,我们希望IBookManager在添加新的Book后,就主动通知每个客户端,而不需要客户端自己去查询数据是否改变。显然,这就构成了一个典型的观察者模式IBookManager成了发布者,而客户端们则成了订阅者。关于这个模式的详细讲解可以参考这篇博客:

观察者模式(先挖个坑,有空就填)

为了实现观察者模式,我们需要提供一个新的AIDL接口,之所以不选用普通接口是因为AIDL不支持普通接口。每个客户端都向IBookManger注册自己的接口对象,IBookManager则会将每个客户端对应的接口对象保存下来。当Book列表数据改变后,IBookManager就遍历自己的接口列表,依次执行这些接口中的回调方法,这也就起到了通知客户端的作用。具体实现如下,新增的主要代码前面都添加了//Add注释:

新增的AIDL接口:

// IOnBookAddListener.aidl
package com.example.ipcdemo.aidl;
import com.example.ipcdemo.aidl.Book;

interface IOnBookAddListener {
    void onBookAdd(in Book book);
}

新增的AIDL接口很简单,就是用来监听IBookManagerBook列表数据是否改变的监听器。

修改后的原AIDL接口

// IBookManager.aidl
package com.example.ipcdemo.aidl;
import com.example.ipcdemo.aidl.Book;

//Add
import com.example.ipcdemo.aidl.IOnBookAddListener;

interface IBookManager {
    List<Book> getBookList();
    void addBook(in Book book);

    //Add
    void registerListener(IOnBookAddListener listener);
    void unRegisterListener(IOnBookAddListener listener);
}

可以看到,修改后的IBookManager接口中新增了两个方法,一个用于注册监听器,一个用于取消注册。由于这两个方法的参数是AIDL接口,因此不需要指定参数的输入/输出类型(in、out、inout)。但是需要注意的是,新增的AIDL接口依旧需要显式地导入进来。

import com.example.ipcdemo.aidl.IOnBookAddListener;

修改后的服务器端:

 public class ServerService extends Service {
    private static final String TAG="ServerService";
    private CopyOnWriteArrayList<Book> bookList=new CopyOnWriteArrayList<>();

    //Add:存储监听器的数据结构
    private RemoteCallbackList<IOnBookAddListener> listenerList=new RemoteCallbackList<>();

    private IBinder binder=new IBookManager.Stub(){
        @Override
        public List<Book> getBookList() throws RemoteException {
            return bookList;
        }
        @Override
        public void addBook(Book book) throws RemoteException {
            bookList.add(book);
        }

        //Add
        @Override
        public void registerListener(IOnBookAddListener listener) throws RemoteException {
            listenerList.register(listener);
        }
        @Override
        public void unRegisterListener(IOnBookAddListener listener) throws RemoteException {
            listenerList.unregister(listener);
        }
    };

    //Add:添加书籍
    public void addBook(Book book) throws RemoteException {
        bookList.add(book);
        int listenerNum=listenerList.beginBroadcast();
        for(int i=0;i<listenerNum;i++){
            IOnBookAddListener listener=listenerList.getBroadcastItem(i);
            listener.onBookAdd(book);
        }
        listenerList.finishBroadcast();
    }

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

    @Override
    public void onCreate() {
        super.onCreate();
        //初始化数据
        bookList.add(new Book(1,"小王子"));
        bookList.add(new Book(2,"长尾理论"));

        //Add
        new Thread(){
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    addBook(new Book(4,"创新者的窘境"));
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }
}

可以看到,我们在客户端创建了一个RemoteCallbackList对象来保存客户端的监听器。

private RemoteCallbackList<IOnBookAddListener> listenerList=new RemoteCallbackList<>();

RemoteCallbackListAIDL中专门用来存储AIDL接口以及删除跨进程listener的数据结构。之所以不使用ArrayList存储AIDL接口,是因为listener对象在通过跨进程传输时,实际上经过了一个序列化和反序列化的过程。这意味着在服务器端得到的listener对象只是和客户端的listener对象内容相同,两者本质上依旧是不同的对象。因此在客户端进行解注册操作时,服务器端会因为找不到与客户端使用的listener相同的对象而解注册失败。而RemoteCallbackList内部进行了特殊处理,让我们得以正常地移除客户端的listener。关于RemoteCallbackList的原理解析,可以参考下文的RemoteCallbackList解析

IBookManager.Stub中,我们实现了新增的两个方法,分别用于注册listener和取消注册listener。此外,我们新增了一个addBook方法。这个方法用于向IBookManager中添加一个Book对象。在这个方法中,我们逐次调用了每个listener的回调方法,这就起到了通知客户端的作用。需要注意RemoteCallbackList的使用方式,它和ArrayList的使用方式完全不同。

进行遍历前,先要调用beginBroadcast方法,这个方法会返回RemoteCallbackList中的元素数量。然后在for循环中,通过getBroadcastItem方法逐次获取存储的listener。最后,还要调用finishBroadcast方法,这才算是构成了一个完整的遍历过程。

int listenerNum=listenerList.beginBroadcast();
IOnBookAddListener listener=listenerList.getBroadcastItem(i);
listenerList.finishBroadcast();

最后,我们在ServiceonCreate方法中开启了一个匿名的线程。这个线程的作用是在3秒后向IBookManager中添加一个Book对象,主要用于测试是否成功实现了观察者模式。

修改后的客户端:

public class ClientActivity extends AppCompatActivity {
    private static final String TAG="ClientActivity";
    private static final int MSG_ON_BOOK_ADD=1;

    private IBookManager bookManager;

    //Add
    private Handler handler=new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case MSG_ON_BOOK_ADD:
                    Log.i(TAG,msg.obj.toString());
                    break;
                default:
                    break;
            }
        }
    };

    //Add
    private IOnBookAddListener listener=new IOnBookAddListener.Stub(){
        @Override
        public void onBookAdd(Book book) throws RemoteException {
            Message message=new Message();
            message.what=MSG_ON_BOOK_ADD;
            message.obj=book;
            handler.sendMessage(message);
        }
    };

    private ServiceConnection serviceConnection=new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            bookManager=IBookManager.Stub.asInterface(service);
            try {
                List<Book> bookList=bookManager.getBookList();
                Log.i(TAG,bookList.toString());
                bookManager.addBook(new Book(3,"野火集"));
                List<Book> newBookList=bookManager.getBookList();
                Log.i(TAG,"添加新的书:"+newBookList.toString());

                //Add:注册监听器
                bookManager.registerListener(listener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_client);

        Intent intent=new Intent(this,ServerService.class);
        bindService(intent,serviceConnection,BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(serviceConnection);//解绑

        //Add:取消注册监听器
        try {
            bookManager.unRegisterListener(listener);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
}

可以看到,我们通过实例化IOnBookAddListener.Stub获得了一个AIDL接口对象,并在onServiceConnected方法中进行了注册。在onDestroy方法中,我们又将listenerIBookManager中取消注册。

bookManager.registerListener(listener);
bookManager.unRegisterListener(listener);

需要注意的是,IOnBookAddListener中的回调方法是在客户端的Binder线程池中执行的,因此不能在这个方法里直接操作客户端UI。在本例中,我们提供了一个Handler对象,并在IOnBookAddListener的回调方法中通过Handler发送Message,借助Handler切换到UI线程执行后续操作。

运行结果:

05-05 01:31:43.707 15028-15028/com.example.ipcdemo I/ClientActivity: [bookId:1 bookName:小王子, bookId:2 bookName:长尾理论]
05-05 01:31:43.707 15028-15028/com.example.ipcdemo I/ClientActivity: 添加新的书:[bookId:1 bookName:小王子, bookId:2 bookName:长尾理论, bookId:3 bookName:野火集]
05-05 01:31:46.667 15028-15028/com.example.ipcdemo I/ClientActivity: bookId:4 bookName:创新者的窘境

RemoteCallBackList解析

RemoteCallbackList的内部维持着一个Map结构,具体来说是一个ArrayMap,如下所示:

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

可以看到,这个MapkeyIBinder对象,value则是Callback对象。我们来看一下keyvalue是怎么获得的:

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

这样一来,RemoteCallbackList的工作原理就很清晰了。虽然经过序列化和反序列化后,客户端注册时和解除注册时的listener在服务器端并不是同一个对象,但是它们底层的Binder却是同一个对象。通过这个Binder对象,就可以从RemoteCallbackList内部的Map中获得Callback对象,也就间接获得了listener对象。在Map中移除这个Callback对象,就达到了取消注册listener的作用。

最佳实践

重连机制

我们要知道,在实际使用中Binder是有可能死亡的,一般是由于服务器端进程的突然停止造成的。为了避免这种情况的发生,我们应该为Binder设置重连机制。一旦Binder重新死亡,就需要重新绑定服务器端。下面介绍两种方法。

一种方法是为Binder对象设置死亡代理,具体代码如下所示:

private IBinder.DeathRecipient deathRecipient=new IBinder.DeathRecipient() {
    @Override
    public void binderDied() {
        if(bookManager!=null){
            return;
        }
        bookManager.asBinder().unlinkToDeath(deathRecipient,0);
        bookManager=null;
        //重新绑定服务
        Intent intent=new Intent(ClientActivity.this,ServerService.class);
        bindService(intent,serviceConnection,BIND_AUTO_CREATE);
    }
};

private ServiceConnection serviceConnection=new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        bookManager=IBookManager.Stub.asInterface(service);
        try {
            //设置死亡代理
            service.linkToDeath(deathRecipient,0);
            .........
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
    .......
};

可以看到,我们先创建了一个IBinder.DeathRecipient对象,并在它的回调方法binderDied中进行了重连操作。此外,我们在ServiceConnectiononServiceConnected方法中为IBookManager设置了这个死亡代理linkToDeath方法的第二个参数是一个标识,一般传入0即可。

service.linkToDeath(deathRecipient,0);

需要注意的是,在对这种方式进行测试时,未能获得理想的效果,暂时先留个坑在这里。

另一种方法是在ServiceConnectiononServiceDisconnected方法中执行重连操作,具体代码如下所示:

 private ServiceConnection serviceConnection=new ServiceConnection() {
    ......
    @Override
    public void onServiceDisconnected(ComponentName name) {
        //重连操作
        Intent intent=new Intent(ClientActivity.this,ServerService.class);
        bindService(intent,serviceConnection,BIND_AUTO_CREATE);
    }
};

以上两种方式的区别在于,第一种方式的binderDied方法在Binder线程池中调用,而第二种方式的onServiceConnnected方法则在UI线程中调用。

权限验证

作为服务器端,自然需要具备权限验证功能,否则任何应用都可以调用我们的远程服务,这显然不是我们想要的。一般来说,在ServiceonBind方法中进行权限验证就可以了。接下来介绍一种简单的验证方式,即通过permission进行权限验证。

首先,我们需要在服务端自定义权限,如下所示:

<permission
    android:name="com.example.ipcdemo.CONNECT_TO_SERVICE"
    android:protectionLevel="normal">
</permission>

可以看到,我们将permission的级别设置为normal。关于permission的详细解析,可以看一看这篇博客:

详解Android权限机制

在服务器端的onBind方法中,我们先进行权限判断。如果客户端拥有相应权限,我们才返回Binder对象,否则返回null。具体代码如下所示:

@Override
public IBinder onBind(Intent intent) {
    int permissionEnd=checkCallingOrSelfPermission("com.example.ipcdemo.CONNECT_TO_SERVICE");
    if(permissionEnd==PackageManager.PERMISSION_DENIED){
        return null;
    }
    return binder;
}

需要注意的是,如果ServiceonBind方法返回null,则ServiceConnectiononServiceConnected方法将不会被调用。

需要绑定服务器端的客户端,只要在自己的AndroidManifest文件中声明对应的权限即可,如下所示:

<uses-permission android:name="com.example.ipcdemo.CONNECT_TO_SERVICE"></uses-permission>

Binder线程池

通过前面的例子,可以发现我们的AIDL是建立在一个Service基础上的。如果仅仅采用这种模式,当我们的业务需求较多时,就需要单独创建很多个Service。对于一个大型项目而言,这显然是不现实的。因此,需要引入线程池的概念。即通过一个统一的Service向外提供AIDL服务,并针对不同的业务请求返回不同的Binder,这就解决了Service数量过多的问题。关于线程池的具体使用,请参考这篇博客:

Binder线程池(按照惯例,先挖坑,慢慢填)

小结

通过上文的讲解,我们基本就了解AIDL的常规使用了。此外,我们还需要记住,在客户端调用服务器端的方法时,这些方法运行在服务器端的Binder线程池中,客户端的线程则会陷入阻塞状态。因此,如果是在客户端的UI线程中进行远程调用,就要注意使用异步的方式,避免出现ANR。而服务器端的方法,本来就运行在线程池中,所以我们应该避免在服务器端的方法中再开启线程。同样的,从服务器端远程调用客户端中的回调方法时,这些方法也运行在客户端的Binder线程池中。

项目demo下载地址

下面给出上述例子的demo下载地址:IPCDemo

猜你喜欢

转载自blog.csdn.net/CodingEnding/article/details/71244274