Android中的IPC方式 - AIDL(二)

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

本章重点总结:
使用AIDL
如果有大量的并发请求,使用Messenger就不太适合,同时如果需要跨进程调用服务端的方法,Messenger就无法做到了。这时我们可以使用AIDL。
流程如下:
1. 服务端需要创建Service来监听客户端请求,然后创建一个AIDL文件,将暴露给客户端的接口在AIDL文件中声明,最后在Service中实现这个AIDL接口即可。
2. 客户端首先绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。
注意事项:
1. AIDL支持的数据类型:
i. 基本数据类型、String、CharSequence
ii. List:只支持ArrayList,里面的每个元素必须被AIDL支持
iii. Map:只支持HashMap,里面的每个元素必须被AIDL支持
iv. Parcelable
v. 所有的AIDL接口本身也可以在AIDL文件中使用
2. 自定义的Parcelable对象和AIDL对象,不管它们与当前的AIDL文件是否位于同一个包,都必须显式import进来。
3. 如果AIDL文件中使用了自定义的Parcelable对象,就必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型。
package com.ryg.chapter_2.aidl;
parcelable Book
4. AIDL接口中的参数除了基本类型以外都必须表明方向in/out。AIDL接口文件中只支持方法,不支持声明静态常量。建议把所有和AIDL相关的类和文件放在同一个包中,方便管理。
void addBook(in Book book);
5. AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时连接时,管理数据的集合直接采用 CopyOnWriteArrayList 来进行自动线程同步。类似的还有 ConcurrentHashMap 。
6. 因为客户端的listener和服务端的listener不是同一个对象,所以 RecmoteCallbackList 是系统专门提供用于删除跨进程listener的接口,支持管理任意的AIDL接口,因为所有AIDL接口都继承自 IInterface 接口。
public class RemoteCallbackList
它内部通过一个Map接口来保存所有的AIDL回调,这个Map的key是 IBinder 类型,value是 Callback 类型。当客户端解除注册时,遍历服务端所有listener,找到和客户端listener具有相同Binder对象的服务端listenr并把它删掉。
7. 客户端RPC的时候线程会被挂起,由于被调用的方法运行在服务端的Binder线程池中,可能很耗时,不能在主线程中去调用服务端的方法。
使用ContentProvider
1. ContentProvider是四大组件之一,其底层实现和Messenger一样是Binder。ContentProvider天生就是用来进程间通信,只需要实现一个自定义或者系统预设置的ContentProvider,通过ContentResolver的query、update、insert和delete方法即可。
2. 创建ContentProvider,只需继承ContentProvider实现 onCreate 、 query 、 update 、 insert 、 getType 六个抽象方法即可。除了 onCreate 由系统回调并运行在主线程,其他五个方法都由外界调用并运行在Binder线程池中。
使用Sock
Socket可以实现计算机网络中的两个进程间的通信,当然也可以在本地实现进程间的通信。服务端Service监听本地端口,客户端连接指定的端口,建立连接成功后,拿到 Socket 对象就可以向服务端发送消息或者接受服务端发送的消息。
Binder连接池
AIDL是一种最常用的IPC方式,是日常开发中涉及IPC时的首选。前面提到AIDL的流程是 客户端在Service的onBind方法中拿到继承AIDL的Stub对象,然后客户端就可以通过这个Stub对象进行RPC。
那么如果项目庞大,有多个业务模块都需要使用AIDL进行IPC,随着AIDL数量的增加,我们不能无限制地增加Service,我们需要把所有AIDL放在同一个Service中去管理。
流程是:
1. 服务端只有一个Service,我们应该把所有AIDL放在一个Service中去管理,不同业务模块
之间是不能有耦合的
2. 服务端提供一个 queryBinder 接口,这个借口能够根据业务模块的特征来返回响应的
Binder对象给客户端
3. 不同的业务模块拿到所需的Binder对象就可以进行RPC了
选用合适的IPC方式
这里写图片描述

1 使用AIDL

上一节我们介绍了使用Messenger来进行进程间通信的方法,可以发现,Messenger是以串行的方式处理客户端发来的消息,如果大量的消息同时发送到服务端,服务端仍然只能一个个处理,如果有大量的并发请求,那么用Messenger就不太合适了。同时,Messenger的作用主要是为了传递消息,很多时候我们可能需要跨进程调用服务端的方法,这种情形用Messenger就无法做到了,但是我们可以使用AIDL来实现跨进程的方法调用。AIDL也是Messenger的底层实现,因此Messenger本质上也是AIDL,只不过系统为我们做了封装从而方便上层的调用而已。在上一节中,我们介绍了Binder的概念,大家对Binder也有了一定的了解,在Binder的基础上我们可以更加容易地理解AIDL。这里先介绍使用AIDL来进行进程间通信的流程,分为服务端和客户端两个方面。
1. 服务端
服务端首先要创建一个Service用来监听客户端的连接请求,然后创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明,最后在Service中实现这个AIDL接口即可。
2.客户端
客户端所要做事情就稍微简单一些,首先需要绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。
3. AIDL接口的创建
首先看AIDL接口的创建,如下所示,我们创建了一个后缀为AIDL的文件,在里面声明了一个接口和两个接口方法。

package com.nextvpu.myapplication;

import com.nextvpu.myapplication.Book;
import com.nextvpu.myapplication.IOnNewBookArrivedListener;

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

在AIDL文件中,并不是所有的数据类型都是可以使用的,那么到底AIDL文件支持哪些数据类型呢?如下所示。

  • 基本数据类型(int、long、char、boolean、double等);
  • String和CharSequence;
  • List:只支持ArrayList,里面每个元素都必须能够被AIDL支持;
  • Map:只支持HashMap,里面的每个元素都必须被AIDL支持,包括key和value;
  • Parcelable:所有实现了Parcelable接口的对象;
  • AIDL:所有的AIDL接口本身也可以在AIDL文件中使用。

另外一个需要注意的地方是,如果AIDL文件中用到了自定义的Parcelable对象,那么必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型。在上面的IBookManager.aidl中,我们用到了Book这个类,所以,我们必须要创建Book.aidl,然后在里面添加如下内容:

package com.nextvpu.myapplication;

parcelable Book;

我们需要注意,AIDL中每个实现了Parcelable接口的类都需要按照上面那种方式去创建相应的AIDL文件并声明那个类为parcelable。除此之外,AIDL中除了基本数据类型,其他类型的参数必须标上方向:in、out或者inout,in表示输入型参数,out表示输出型参数,inout表示输入输出型参数,至于它们具体的区别,这个就不说了。我们要根据实际需要去指定参数类型,不能一概使用out或者inout,因为这在底层实现是有开销的。最后,AIDL接口中只支持方法,不支持声明静态常量,这一点区别于传统的接口。
为了方便AIDL的开发,建议把所有和AIDL相关的类和文件全部放入同一个包中,这样做的好处是,当客户端是另外一个应用时,我们可以直接把整个包复制到客户端工程中,对于本例来说,就是要把com.ryg.chapter_2.aidl这个包和包中的文件原封不动地复制到客户端中。如果AIDL相关的文件位于不同的包中时,那么就需要把这些包一一复制到客户端工程中,这样操作起来比较麻烦而且也容易出错。需要注意的是,AIDL的包结构在服务端和客户端要保持一致,否则运行会出错,这是因为客户端需要反序列化服务端中和AIDL接口相关的所有类,如果类的完整路径不一样的话,就无法成功反序列化,程序也就无法正常运行。为了方便演示,本章的所有示例都是在同一个工程中进行的,但是读者要理解,一个工程和两个工程的多进程本质是一样的,两个工程的情况,除了需要复制AIDL接口所相关的包到客户端,其他完全一样,读者可以自行试验。
4. 远程服务端Service的实上面讲述了如何定义AIDL接口,接下来我们就需要实现这个接口了。我们先创建一个Service,称为BookManagerService,代码如下:

public class BookManagerService extends Service {
private static final String TAG = "BMS";
private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArray￾List<Book>();
private Binder mBinder = new IBookManager.Stub() {
@Override
public List<Book> getBookList() throws RemoteException {
return mBookList;
}
@Override
public void addBook(Book book) throws RemoteException {
mBookList.add(book);
}
};
@Override
public void onCreate() {
super.onCreate();
mBookList.add(new Book(1,"Android"));
mBookList.add(new Book(2,"Ios"));
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}

上面是一个服务端Service的典型实现,首先在onCreate中初始化添加了两本图书的信息,然后创建了一个Binder对象并在onBind中返回它,这个对象继承自IBookManager.Stub并实现了它内部的AIDL方法,这个过程在Binder那一节已经介绍过了,这里就不多说了。这里主要看getBookList和addBook这两个AIDL方法的实现,实现过程也比较简单,注意这里采用了CopyOnWriteArrayList,这个CopyOnWriteArrayList支持并发读/写。在前面我们提到,AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时连接
的时候,会存在多个线程同时访问的情形,所以我们要在AIDL方法中处理线程同步,而我们这里直接使用CopyOnWriteArrayList来进行自动的线程同步。
前面我们提到,AIDL中能够使用的List只有ArrayList,但是我们这里却使用了CopyOnWriteArrayList(注意它不是继承自ArrayList),为什么能够正常工作呢?这是因为AIDL中所支持的是抽象的List,而List只是一个接口,因此虽然服务端返回的是CopyOnWriteArrayList,但是在Binder中会按照List的规范去访问数据并最终形成一个新的ArrayList传递给客户端。所以,我们在服务端采用CopyOnWriteArrayList是完全可以的。和此类似的还有ConcurrentHashMap,读者可以体会一下这种转换情形。然后我们需要在XML中注册这个Service,如下所示。注意BookManagerService是运行在独立进程中的,它和客户端的Activity不在同一个进程中,这样就构成了进程间通信的场景。
5. 客户端的实现
客户端的实现就比较简单了,首先要绑定远程服务,绑定成功后将服务端返回的Binder对象转换成AIDL接口,然后就可以通过这个接口去调用服务端的远程方法了,代码如下所示。

public class BookManagerActivity extends Activity {
private static final String TAG = "BookManagerActivity";
private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className,IBinder
service) {
IBookManager bookManager = IBookManager.Stub.asInterface
(service);
try {
List<Book> list = bookManager.getBookList();
Log.i(TAG,"query book list,list type:" + list.getClass().
getCanonicalName());
Log.i(TAG,"query book list:" + list.toString());
} catch (RemoteException e) {
e.printStackTrace();
}
}
public void onServiceDisconnected(ComponentName className) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_book_manager);
Intent intent = new Intent(this,BookManagerService.class);
bindService(intent,mConnection,Context.BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
unbindService(mConnection);
super.onDestroy();
}
}

绑定成功以后,会通过bookManager去调用getBookList方法,然后打印出所获取的图书信息。需要注意的是,服务端的方法有可能需要很久才能执行完毕,这个时候下面的代码就会导致ANR,这一点是需要注意的,后面会再介绍这种情况,之所以先这么写是为了让读者更好地了解AIDL的实现步骤。
可以发现,虽然我们在服务端返回的是CopyOnWriteArrayList类型,但是客户端收到的仍然是ArrayList类型,这也证实了我们在前面所做的分析。第二行log表明客户端成功地得到了服务端的图书列表信息。这就是一次完完整整的使用AIDL进行IPC的过程,到这里相信读者对AIDL应该有了一个整体的认识了,但是还没完,AIDL的复杂性远不止这些,下面继续介绍AIDL中常见的一些难点。我们接着再调用一下另外一个接口addBook,我们在客户端给服务端添加一本书,然后再获取一次,看程序是否能够正常工作。还是上面的代码,客户端在服务连接后,在onServiceConnected中做如下改动:

public void onServiceConnected(ComponentName className,IBinder service) {
IBookManager bookManager = IBookManager.Stub.asInterface(service);
try {
List<Book> list = bookManager.getBookList();
Log.i(TAG,"query book list:" + list.toString());
Book newBook = new Book(3,"Android开发艺术探索");
bookManager.addBook(newBook);
Log.i(TAG,"add book:" + newBook);
List<Book> newList = bookManager.getBookList();
Log.i(TAG,"query book list:" + newList.toString());
} catch (RemoteException e) {
e.printStackTrace();
}
}

现在我们考虑一种情况,假设有一种需求:用户不想时不时地去查询图书列表了,太累了,于是,他去问图书馆,“当有新书时能不能把书的信息告诉我呢?”。大家应该明白了,这就是一种典型的观察者模式,每个感兴趣的用户都观察新书,当新书到的时候,图书馆就通知每一个对这本书感兴趣的用户,这种模式在实际开发中用得很多,下面我们就来模拟这种情形。首先,我们需要提供一个AIDL接口,每个用户都需要实现这个接口并且向图书馆申请新书的提醒功能,当然用户也可以随时取消这种提醒。之所以选择AIDL接口而不是普通接口,是因为AIDL中无法使用普通接口。这里我们创建一个
IOnNewBookArrivedListener.aidl文件,我们所期望的情况是:当服务端有新书到来时,就会通知每一个已经申请提醒功能的用户。从程序上来说就是调用所有IOnNewBookArrivedListener对象中的onNewBookArrived方法,并把新书的对象通过参数传递给客户端,内容如下所示。

package com.nextvpu.myapplication;

import com.nextvpu.myapplication.Book;

interface IOnNewBookArrivedListener {
    void onNewBookArrived(in Book newBook);
}

除了要新加一个AIDL接口,还需要在原有的接口中添加两个新方法,代码如下所示。

package com.nextvpu.myapplication;

 import com.nextvpu.myapplication.Book;
 import com.nextvpu.myapplication.IOnNewBookArrivedListener;

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

接着,服务端中Service的实现也要稍微修改一下,主要是Service中
IBookManager.Stub的实现,因为我们在IBookManager新加了两个方法,所以在IBookManager.Stub中也要实现这两个方法。同时,在BookManagerService中还开启了一个线程,每隔5s就向书库中增加一本新书并通知所有感兴趣的用户,整个代码如下所示。

package com.nextvpu.myapplication;

import android.app.Service;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import android.util.Log;

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

public class BookManagerService extends Service {

    private static final String TAG = "BMS";
    private AtomicBoolean mIsServiceDestoryed = new AtomicBoolean(false);

    private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();

    private RemoteCallbackList<IOnNewBookArrivedListener> mListenerList = new RemoteCallbackList<>();

    private Binder mBinder = new IBookManager.Stub() {
        @Override
        public List<Book> getBookList() throws RemoteException {
            SystemClock.sleep(5000);
            return mBookList;
        }

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

        @Override
        public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            int check = checkCallingOrSelfPermission("com.nextvpu.myapplication.permission.ACCESS_BOOK_SERVICE");
            Log.e("xyz"," check ===  "+check);
            if (check == PackageManager.PERMISSION_DENIED){
                return false;
            }
            String packageName = null;
            String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
            if (packages!=null && packages.length>0){
                packageName = packages[0];
            }
            Log.e("xyz","onTransact: " + packageName);

            if (!packageName.startsWith("com.nextvpu")){
                return false;
            }
            return super.onTransact(code, data, reply, flags);
        }

        @Override
        public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
            mListenerList.register(listener);

            final  int N = mListenerList.beginBroadcast();
            mListenerList.finishBroadcast();
            Log.e("xyz","registerListener, current size:" + N);
        }

        @Override
        public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
            boolean success = mListenerList.unregister(listener);

            if (success){
                Log.e("xyz"," unregister success. ");
            }else{
                Log.e("xyz", "not found, can not unregister.");
            }
            final  int N = mListenerList.beginBroadcast();
            mListenerList.finishBroadcast();
            Log.e("xyz","registerListener, current size:" + N);

        }
    };




    @Override
    public IBinder onBind(Intent intent) {
        int check = checkCallingOrSelfPermission("com.nextvpu.myapplication.permission.ACCESS_BOOK_SERVICE");
        Log.e("xyz", "onbind check=" + check);
        if (check == PackageManager.PERMISSION_DENIED){
            return null;
        }
        return mBinder;
    }


    @Override
    public void onCreate() {
        super.onCreate();
        mBookList.add(new Book(1,"Android"));
        mBookList.add(new Book(2,"Ios"));
        new Thread(new ServiceWorker()).start();
    }

    private class ServiceWorker implements Runnable{

        @Override
        public void run() {
            while (!mIsServiceDestoryed.get()){
                try {
                    Thread.sleep(5000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }

                int bookId = mBookList.size()+1;
                Book newBook = new Book(bookId,"new book#"+bookId);

                try {
                    onNewBookArrived(newBook);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private void onNewBookArrived(Book book) throws RemoteException{
        mBookList.add(book);

        final int N = mListenerList.beginBroadcast();
        for (int i = 0; i < N ; i++) {
            IOnNewBookArrivedListener l = mListenerList.getBroadcastItem(i);
            if (l!=null){
                try {
                    l.onNewBookArrived(book);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
        mListenerList.finishBroadcast();
    }

}

最后,我们还需要修改一下客户端的代码,主要有两方面:首先客户端要注册IOnNewBookArrivedListener到远程服务端,这样当有新书时服务端才能通知当前客户端,同时我们要在Activity退出时解除这个注册;另一方面,当有新书时,服务端会回调客户端的IOnNewBookArrivedListener对象中的onNewBookArrived方法,但是这个方法是在客户端的Binder线程池中执行的,因此,为了便于进行UI操作,我们需要有一个Handler可以将其切换到客户端的主线程中去执行,这个原理在Binder中已经做了分析,这里就不多说了。客户端的代码修改如下:

package com.nextvpu.myapplication;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.View;
import android.widget.Toast;

import java.util.List;

public class BookManagerActivity extends Activity {

    private static final int MESSAGE_NEW_BOOK_ARRIVED = 1;

    private IBookManager mRemoteBookManager;

    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case MESSAGE_NEW_BOOK_ARRIVED:
                    Log.e("xyz", "receive new book :" + msg.obj);
                    break;
            }
            super.handleMessage(msg);
        }
    };

    private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
        @Override
        public void binderDied() {
            Log.e("xyz", "binder died. tname:" + Thread.currentThread().getName());
            if (mRemoteBookManager == null)
                return;
            mRemoteBookManager.asBinder().unlinkToDeath(mDeathRecipient,0);
            mRemoteBookManager = null;
        }
    };

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            mRemoteBookManager = bookManager;

            try {
                mRemoteBookManager.asBinder().linkToDeath(mDeathRecipient,0);
                List<Book> list = bookManager.getBookList();
                Log.e("xyz", "query book list, list type:"
                        + list.getClass().getCanonicalName());
                Log.e("xyz", "query book list:" + list.toString());
                Book newBook = new Book(3,"Android进阶");
                bookManager.addBook(newBook);
                Log.i("xyz", "add book:" + newBook);
                List<Book> newList = bookManager.getBookList();
                Log.e("xyz", "query book list:" + newList.toString());
                bookManager.registerListener(mOnNewBookArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
                mRemoteBookManager = null;
            Log.e("xyz", "onServiceDisconnected. tname:" + Thread.currentThread().getName());
        }
    };

    private IOnNewBookArrivedListener mOnNewBookArrivedListener = new IOnNewBookArrivedListener.Stub() {
        @Override
        public void onNewBookArrived(Book newBook) throws RemoteException {
            mHandler.obtainMessage(MESSAGE_NEW_BOOK_ARRIVED,newBook).sendToTarget();
        }

    };


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_book_manager);
        Intent intent = new Intent(this,BookManagerService.class);
        bindService(intent,mConnection, Context.BIND_AUTO_CREATE);
    }

    public void onButton1Click(View view){
        Toast.makeText(this, "click button1", Toast.LENGTH_SHORT).show();
        new Thread(new Runnable() {
            @Override
            public void run() {
                if (mRemoteBookManager!=null){
                    try {
                        List<Book> newList = mRemoteBookManager.getBookList();
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

    @Override
    protected void onDestroy() {
        if (mRemoteBookManager!=null && mRemoteBookManager.asBinder().isBinderAlive()){
            Log.e("xyz", "unregister listener:" + mOnNewBookArrivedListener);
            try {
                mRemoteBookManager.unregisterListener(mOnNewBookArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        unbindService(mConnection);
        super.onDestroy();
    }
}

果你以为到这里AIDL的介绍就结束了,那你就错了,之前就说过,AIDL远不止这么简单,目前还有一些难点是我们还没有涉及的,接下来将继续为读者介绍。从上面的代码可以看出,当BookManagerActivity关闭时,我们会在onDestroy中去解除已经注册到服务端的listener,这就相当于我们不想再接收图书馆的新书提醒了,所以我们可以随时取消这个提醒服务。按back键退出BookManagerActivity,下面是打印出的log。

09-06 10:34:05.344 15264-15264/? E/xyz: onbind check=0
09-06 10:34:10.397 15264-15264/com.nextvpu.myapplication E/xyz: query book list, list type:java.util.concurrent.CopyOnWriteArrayList
09-06 10:34:10.399 15264-15264/com.nextvpu.myapplication E/xyz: query book list:[[bookId:1, bookName:Android], [bookId:2, bookName:Ios], [bookId:3, bookName:new book#3]]
09-06 10:34:10.399 15264-15264/com.nextvpu.myapplication I/xyz: add book:[bookId:3, bookName:Android进阶]
09-06 10:34:15.400 15264-15264/com.nextvpu.myapplication E/xyz: query book list:[[bookId:1, bookName:Android], [bookId:2, bookName:Ios], [bookId:3, bookName:new book#3], [bookId:3, bookName:Android进阶], [bookId:5, bookName:new book#5]]

如果你以为到这里AIDL的介绍就结束了,那你就错了,之前就说过,AIDL远不止这么简单,目前还有一些难点是我们还没有涉及的,接下来将继续为读者介绍。
从上面的代码可以看出,当BookManagerActivity关闭时,我们会在onDestroy中去解除已经注册到服务端的listener,这就相当于我们不想再接收图书馆的新书提醒了,所以我们可以随时取消这个提醒服务。按back键退出BookManagerActivity,下面是打印出的log。

I/BookManagerActivity(5642): unregister listener:com.ryg.chapter_2.aidl.BookManagerActivity$3@405284c8D/BMS(5650): not found,can not unregister.D/BMS(5650): unregisterListener,current size

从上面的log可以看出,程序没有像我们所预期的那样执行。在解注册的过程中,服务端竟然无法找到我们之前注册的那个listener,在客户端我们注册和解注册时明明传递的是同一个listener啊!最终,服务端由于无法找到要解除的listener而宣告解注册失败!这当然不是我们想要的结果,但是仔细想想,好像这种方式的确无法完成解注册。其实,这是必然的,这种解注册的处理方式在日常开发过程中时常使用到,但是放到多进程中却无法奏效,因为Binder会把客户端传递过来的对象重新转化并生成一个新的对象。虽然我们在注册和解注册过程中使用的是同一个客户端对象,但是通过Binder传递到服务端后,却会
产生两个全新的对象。别忘了对象是不能跨进程直接传输的,对象的跨进程传输本质上都是反序列化的过程,这就是为什么AIDL中的自定义对象都必须要实现Parcelable接口的原因。那么到底我们该怎么做才能实现解注册功能呢?答案是使用RemoteCallbackList,这看起来很抽象,不过没关系,请看接下来的详细分析。
RemoteCallbackList是系统专门提供的用于删除跨进程listener的接口。RemoteCallbackList是一个泛型,支持管理任意的AIDL接口,这点从它的声明就可以看出,因为所有的AIDL接口都继承自IInterface接口,读者还有印象吗?

public class RemoteCallbackList<E extends IInterfac>

它的工作原理很简单,在它的内部有一个Map结构专门用来保存所有的AIDL回调,这个Map的key是IBinder类型,value是Callback类型,如下所示。

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

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

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

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

IBinder key= listener.asBinder()Callback alue neCallback(listener,cooki

到这里,读者应该都明白了,虽然说多次跨进程传输客户端的同一个对象会在服务端生成不同的对象,但是这些新生成的对象有一个共同点,那就是它们底层的Binder对象是同一个,利用这个特性,就可以实现上面我们无法实现的功能。当客户端解注册的时候,我们只要遍历服务端所有的listener,找出那个和解注册listener具有相同Binder对象的服务端listener并把它删掉即可,这就是RemoteCallbackList为我们做的事情。同时RemoteCallbackList还有一个很有用的功能,那就是当客户端进程终止后,它能够自动移除客户端所注册的listener。另外,RemoteCallbackList内部自动实现了线程同步的功能,所以我们使用它来注册和解注册时,不需要做额外的线程同步工作。由此可见,RemoteCallbackList的确是个很有价值的类,下面就演示如何使用它来完成解注册。
RemoteCallbackList使用起来很简单,我们要对BookManagerService做一些修改,首先要创建一个RemoteCallbackList对象来替代之前的CopyOnWriteArrayList,如下所示。

private RemoteCallbackList<IOnNewBookArrivedListener> mListenerList newRemoteCallbackList<IOnNewBookArrivedListener>(

然后修改registerListener和unregisterListener这两个接口的实现,如下所示。

@Override
public void registerListener(IOnNewBookArrivedListener listener)
throws RemoteException {
mListenerList.register(listener);
}
@Override
public void unregisterListener(IOnNewBookArrivedListener listener)
throws RemoteException {
mListenerList.unregister(listener);
};

怎么样?是不是用起来很简单,接着要修改onNewBookArrived方法,当有新书时,我们就要通知所有已注册的listener,如下所示。

private void onNewBookArrived(Book book) throws RemoteException {
mBookList.add(book);
final int N = mListenerList.beginBroadcast();
for (int i = 0; i < N; i++) {
IOnNewBookArrivedListener l = mListenerList.getBroadcastItem(i);
if (l != null) {
try {
l.onNewBookArrived(book);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
mListenerList.finishBroadcast()
}

BookManagerService的修改已经完毕了,为了方便我们验证程序的功能,我们还需要添加一些log,在注册和解注册后我们分别打印出所有listener的数量。如果程序正常工作的话,那么注册之后listener总数量是1,解注册之后总数量应该是0,我们再次运行一下程序,看是否如此。从下面的log来看,很显然,使用RemoteCallbackList的确可以完成跨进程的解注册功能。

I/BookManagerActivity(8419): register listener:com.ryg.chapter_2.aidl.
BookManagerActivity$3@40537610
D/BMS(8427): registerListener,current size:1
I/BookManagerActivity(8419): unregister listener:com.ryg.chapter_2.aidl.
BookManagerActivity$3@40537610
D/BMS(8427): unregister success.
D/BMS(8427): unregisterListener,current size:0

使用RemoteCallbackList,有一点需要注意,我们无法像操作List一样去操作它,尽管它的名字中也带个List,但是它并不是一个List。遍历RemoteCallbackList,必须要按照下面的方式进行,其beginBroadcast和beginBroadcast必须要配对使用,哪怕我们仅仅是想要获取RemoteCallbackList中的元素个数,这是必须要注意的地方。

final int N = mListenerList.beginBroadcast();
for (int i = 0; i < N; i++) {
IOnNewBookArrivedListener l = mListenerList.getBroadcastItem(i);
if (l != null) {
//TODO handle l
}
}
mListenerList.finishBroadcast();

到这里,AIDL的基本使用方法已经介绍完了,但是有几点还需要再次说明一下。我们知道,客户端调用远程服务的方法,被调用的方法运行在服务端的Binder线程池中,同时客户端线程会被挂起,这个时候如果服务端方法执行比较耗时,就会导致客户端线程长时间地阻塞在这里,而如果这个客户端线程是UI线程的话,就会导致客户端ANR,这当然不是我们想要看到的。因此,如果我们明确知道某个远程方法是耗时的,那么就要避免在客户端的UI线程中去访问远程方法。由于客户端的onServiceConnected和onServiceDisconnected方法都运行在UI线程中,所以也不可以在它们里面直接调用服务端的耗时方法,这点要尤其注意。另外,由于服务端的方法本身就运行在服务端的Binder线程池中,所以服务端方法本身就可以执行大量耗时操作,这个时候切记不要在服务端方法中开线程去进行异步任务,除非你明确知道自己在干什么,否则不建议这么做。下面我们稍微改造一下服务端的getBookList方法,我们假定这个方法是耗时的,那么服务端可以这么实现:

@Override
public List<Book> getBookList() throws RemoteException {
SystemClock.sleep(5000);
return mBookList;
}

然后在客户端中放一个按钮,单击它的时候就会调用服务端的getBookList方法,可以预知,连续单击几次,客户端就ANR了。
避免出现上述这种ANR其实很简单,我们只需要把调用放在非UI线程即可,如下所示。

public void onButton1Click(View view) {
Toast.makeText(this,"click button1",Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
if (mRemoteBookManager != null) {
try {
List<Book> newList = mRemoteBookManager.getBookList();
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
}).start();
}

同理,当远程服务端需要调用客户端的listener中的方法时,被调用的方法也运行在Binder线程池中,只不过是客户端的线程池。所以,我们同样不可以在服务端中调用客户端的耗时方法。比如针对BookManagerService的onNewBookArrived方法,如下所示。在它
内部调用了客户端的IOnNewBookArrivedListener中的onNewBookArrived方法,如果客户端的这个onNewBookArrived方法比较耗时的话,那么请确保BookManagerService中的onNewBookArrived运行在非UI线程中,否则将导致服务端无法响应。

private void onNewBookArrived(Book book) throws RemoteException {
mBookList.add(book);
Log.d(TAG,"onNewBookArrived,notify listeners:" + mListenerList.
size());
for (int i = 0; i < mListenerList.size(); i++) {
IOnNewBookArrivedListener listener = mListenerList.get(i);
Log.d(TAG,"onNewBookArrived,notify listener:" + listener);
listener.onNewBookArrived(book);
}
}

为了程序的健壮性,我们还需要做一件事。Binder是可能意外死亡的,这往往是由于
服务端进程意外停止了,这时我们需要重新连接服务。有两种方法,第一种方法是给
Binder设置DeathRecipient监听,当Binder死亡时,我们会收到binderDied方法的回调,在binderDied方法中我们可以重连远程服务,具体方法在Binder那一节已经介绍过了,这里就不再详细描述了。另一种方法是在onServiceDisconnected中重连远程服务。这两种方法
我们可以随便选择一种来使用,它们的区别在于:onServiceDisconnected在客户端的UI线程中被回调,而binderDied在客户端的Binder线程池中被回调。也就是说,在binderDied方法中我们不能访问UI,这就是它们的区别。下面验证一下二者之间的区别,首先我们通过DDMS杀死服务端进程,接着在这两个方法中打印出当前线程的名称,如下所示:

D/BookManagerActivity(13652): onServiceDisconnected. tname:main
D/BookManagerActivity(13652): binder died. tname:Binder Thread #2

从上面的log和图2-8我们可以看到,onServiceDisconnected运行在main线程中,即UI线程,而binderDied运行在“Binder Thread#2”这个线程中,很显然,它是Binder线程池中的一个线程。
到此为止,我们已经对AIDL有了一个系统性的认识,但是还差最后一步:如何在AIDL中使用权限验证功能。默认情况下,我们的远程服务任何人都可以连接,但这应该不是我们愿意看到的,所以我们必须给服务加入权限验证功能,权限验证失败则无法调用服务中的方法。在AIDL中进行权限验证,这里介绍两种常用的方法。
第一种方法,我们可以在onBind中进行验证,验证不通过就直接返回null,这样验证
失败的客户端直接无法绑定服务,至于验证方式可以有多种,比如使用permission验证。
使用这种验证方式,我们要先在AndroidMenifest中声明所需的权限,比如:


    <permission
        android:name="com.nextvpu.myapplication.permission.ACCESS_BOOK_SERVICE"
        android:protectionLevel="normal" />

关于permission的定义方式请读者查看相关资料,这里就不详细展开了,毕竟本节的主要内容是介绍AIDL。定义了权限以后,就可以在BookManagerService的onBind方法中做权限验证了,如下所示。

 @Override
    public IBinder onBind(Intent intent) {
        int check = checkCallingOrSelfPermission("com.nextvpu.myapplication.permission.ACCESS_BOOK_SERVICE");
        Log.e("xyz", "onbind check=" + check);
        if (check == PackageManager.PERMISSION_DENIED){
            return null;
        }
        return mBinder;
    }

一个应用来绑定我们的服务时,会验证这个应用的权限,如果它没有使用这个权限,onBind方法就会直接返回null,最终结果是这个应用无法绑定到我们的服务,这样就达到了权限验证的效果,这种方法同样适用于Messenger中,读者可以自行扩展。如果我们自己内部的应用想绑定到我们的服务中,只需要在它的AndroidMenifest文件中采用如下方式使用permission即可。

<permission
        android:name="com.nextvpu.myapplication.permission.ACCESS_BOOK_SERVICE"
        android:protectionLevel="normal" />

第二种方法,我们可以在服务端的onTransact方法中进行权限验证,如果验证失败就直接返回false,这样服务端就不会终止执行AIDL中的方法从而达到保护服务端的效果。至于具体的验证方式有很多,可以采用permission验证,具体实现方式和第一种方法一样。还可以采用Uid和Pid来做验证,通过getCallingUid和getCallingPid可以拿到客户端所属应用的Uid和Pid,通过这两个参数我们可以做一些验证工作,比如验证包名。在下面的代码中,既验证了permission,又验证了包名。一个应用如果想远程调用服务中的方法,首先要使用我们的自定义限“com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE”,
其次包名必须以“com.ryg”开始,否则调用服务端的方法会失败。


        @Override
        public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            int check = checkCallingOrSelfPermission("com.nextvpu.myapplication.permission.ACCESS_BOOK_SERVICE");
            Log.e("xyz"," check ===  "+check);
            if (check == PackageManager.PERMISSION_DENIED){
                return false;
            }
            String packageName = null;
            String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
            if (packages!=null && packages.length>0){
                packageName = packages[0];
            }
            Log.e("xyz","onTransact: " + packageName);

            if (!packageName.startsWith("com.nextvpu")){
                return false;
            }
            return super.onTransact(code, data, reply, flags);
        }

上面介绍了两种AIDL中常用的权限验证方法,但是肯定还有其他方法可以做权限验证,比如为Service指定android:permission属性等,这里就不一一进行介绍了。到这里为止,本节的内容就全部结束了,读者应该对AIDL的使用过程有很深入的理解了

自己根据任大神写的单独aidl的例子,可以参考参考:

Demo下载地址

猜你喜欢

转载自blog.csdn.net/qq_20967339/article/details/82429994