Android 中的 IPC 方式(三) AIDL

本文深入解析了使用AIDL实现进程间通信的流程,包括服务端和客户端的实现步骤,以及如何使用AIDL接口进行跨进程方法调用。同时,文章详细阐述了如何在AIDL中实现权限验证,包括在onBind和onTransact方法中的验证方式。此外,还介绍了如何使用RemoteCallbackList解决跨进程listener的注册与解除问题。

上一篇文章我们介绍了 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 中的方法了。

1. AIDL 接口的创建

首先看 AIDL 接口的创建,如下所示,我们创建了一个后缀名为 AIDL 的文件,在里面声明了一个接口和两个接口方法。

// IBookManager.aidl
package com.demo.text.demotext.aidl;

import com.demo.text.demotext.aidl.Book;

interface IBookManager {

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

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

● 基本数据类型(int、long、char、double、boolean 等);

● String  和 CharSeqence;

● List:只支持 ArrayList,里面每个元素都必须能被 AIDL 支持;

● Map:只支持 HashMap,里面每个元素都必须能被 AIDL 支持,包括 key 和 value;

● Parcelable:所有实现了 Parcelable 接口的对象;

● AIDL:所有 AIDL 接口本身也可以在 AIDL 文件中使用。

以上六中类型就是 AIDL 所支持的所有类型,其中自定义的 Parcelable 对象和 AIDL 对象必须显示的 import 进来。

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

// Book.aidl
package com.demo.text.demotext.aidl;

parcelable Book;

我们需要注意,AIDL 中每个实现了 Parcelable 的类都需要按照上面的方式去创建响应的 AIDL 文件并声明那个类为 Parcelable。除此之外,AIDL 中除了基本数据类型,其他类型的参数必须标上方向:in、out 或者 inout,in 表示输入型参数,数据只能由客户端流向服务端,表现为服务端将会接收到一个那个对象的完整数据,但是客户端的那个对象不会因为服务端对传参的修改而发生变动;out 表示输出型参数,数据只能由服务端流向客户端,表现为服务端将会接收到那个对象的空对象,但是在服务端对接收到的空对象有任何修改之后客户端将会同步变动;inout 表示输入输出型参数,数据可在服务端与客户端之间双向流通,表现为服务端将会接收到客户端传来对象的完整信息,并且客户端将会同步服务端对该对象的任何变动。我们根据实际需求去指定参数类型,不能一概用 out 或者 inout,因为这在底层实现是有开销的。最后,AIDL 接口中只支持方法,不支持声明静态常量,这一点区别于传统的接口。

为了方便 AIDL开发,建议把所有的和 AIDL 相关的类和文件全部放入同一个包中,这样做的好处是,当客户端是另一个应用时,我们可以直接把整个包复制到客户端工程中,对于本利来说,我要要把 com.demo.text.demotext.aidl 这个包和包中的文件原封不动的复制到客户端中。如果 AIDL 相关的文件位于不同的包中时,那么就需要把这些包一一复制到客户端工程中,这样操作起来比较麻烦而且容易出错。需要注意的是,AIDL 的包结构在服务端和客户端要保持一致,否则运行会出错,这是因为客户端要反序列化服务端中和 AIDL 接口相关的所有类,如果类的完整路径不一样的话,就无法成功反序列化,程序也就无法整成运行。

1. 远程服务端 Service 的实现

上面我们讲述了如何定义 AIDL 接口,接下来我们就需要实现这个接口了,我们先创建一个 Service,称为 BookManagerService,代码如下:

public class BookManagerSerrvice extends Service {

    private static final String TAG = "BMS";
    private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();

    private Binder binder = 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, "Andrroid"));
        mBookList.add(new Book(2, "IOS"));
    }

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

上面是一个服务端典型的 Service 实例,首先在 onCreate 中添加两本图书的信息,然后创建一个 Binde 对象并在 onBind 中返回它,这个对象继承自 IBookManager.Stub 并实现了它内部的 AIDL 方法。这个过程在前面就介绍过了,这里就不多说了。这里主要看 getBookList 和 addBook 这两个 AIDL 方法的实现,实现过程也不叫简单,注意这里采用了CopyOnWriteArrayList,这个 CopyOnWriteArrayList 支持并发读/写。在前面我们提到,AIDL 方法在服务端的 Binder 线程池中执行,因此多个客户端同时连接的时候,会存在多线程同时访问的情况,所以我们要在 AIDL 方法中处理线程同步,而我们这里直接采用 CopyOnWriteArrayList 来进行自动的线程同步。

前面我们提到 AIDL 中能够使用的 List 只有 ArrayList,但是我们这里却是用了 CopyOnWriteArrayList(它不是继承 ArrayList),为什么能够正常工作呢?这是因为 AIDL 中所支持的是抽象的 List,而 List 只是一个接口,因此虽然服务端返回的是 CopyOnWriteArrayList ,但是在 Binder 中会按照 List 的规范去访问数据并最终形成一个新的 ArrayList 传递给客户端。所以,我们在服务端采用 CopyOnWriteArrayList 是完全可以的。和此类似的还有 ConcurrentHashMap。

2.客户端的实现

客户端的实现就比较简单了,首先绑定远程服务,绑定成功后,将服务端的 Binder 转换成 AIDL 接口,然后通过这个接口就可以去调用服务端的远程方法了,代码如下:

        @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        bindService(new Intent(this, BookManagerService.class), mConnection, Context.BIND_AUTO_CREATE);
    }

    ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            try {
                Log.i(TAG, "query book list, list types:" + bookManager.getBookList().getClass().getCanonicalName());
                Log.i(TAG, "query book list:" + bookManager.getBookList().toString());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };


    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(mConnection);
    }

绑定成功后,会通过 bookManager 去调用 getBookList 的方法,然后打印出所获取的图书信息。需要注意的是,服务端的方法可能需要很久才能执行完毕,这个时候下面的代码可能会导致 ANR,这一点是值得注意的。后面会介绍这种情况。运行之后,log 如下:

09-13 15:24:56.269 16271-16271/com.demo.text.demotext I/MainActivity: query book list, list types:java.util.ArrayList
09-13 15:24:56.270 16271-16271/com.demo.text.demotext I/MainActivity: query book list:[[bookId:1, bookName:Andrroid], [bookId:2, bookName:IOS]]

可以发现,虽然我们在服务端返回的是 CopyOnWriteArrayList类型,但是客户端收到的仍然是 ArrayList 类型,这也证实了我们在前面做的分析。第二行 log 表明客户端成功得到了服务端的图书信息。

我们接着在调用一下另外一个方法 addBook,在客户端给服务端添加一本书,然后在获取一次。堪称秀是否能正常工作。还是上面的代码,客户端在连接后,修改如下:

    ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            try {
                Log.i(TAG, "query book list, list types:" + bookManager.getBookList().getClass().getCanonicalName());
                Log.i(TAG, "query book list:" + bookManager.getBookList().toString());
                bookManager.addBook(new Book(3, "java"));
                Log.i(TAG, "query book list:" + bookManager.getBookList().toString());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

然后我们看下 log,很显然,我们成功添加了一本书。

09-13 15:33:22.103 17441-17441/com.demo.text.demotext I/MainActivity: query book list:[[bookId:1, bookName:Andrroid], [bookId:2, bookName:IOS], [bookId:3, bookName:java]]

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

// IOnNewBookArrivedListener.aidl
package com.demo.text.demotext.aidl;

import com.demo.text.demotext.aidl.Book;

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

除了新建一个 AIDL 接口,我们还需要在原来的接口中添加两个新方法,一个用来监听,一个用来解除监听,代码如下:

// IBookManager.aidl
package com.demo.text.demotext.aidl;

import com.demo.text.demotext.aidl.Book;
import com.demo.text.demotext.aidl.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 就像图库中添加一本新书并通知感兴趣的用户,注意,我们这里使用的是 AtomicBoolean,而不是使用的boolean,至于为什么,读者可以自行百度,这里就不解释了。代码如下:

ublic class BookManagerService extends Service {

    private AtomicBoolean mIsServiceDestoryed = new AtomicBoolean(true);
    private static final String TAG = "BMS";
    private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();
    private CopyOnWriteArrayList<IOnNewBookArrivedListener> mListenerList = new CopyOnWriteArrayList<>();


    private Binder binder = 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 registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
            if(!mListenerList.contains(listener)) {
                mListenerList.add(listener);
            } else {
                Log.i(TAG, "already exists.");
            }
            Log.i(TAG, "registerListener size:" + mListenerList.size());
        }

        @Override
        public void unRegisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
            if(mListenerList.contains(listener)) {
                mListenerList.remove(listener);
                Log.i(TAG, "unregister listener succeed.");
            } else {
                Log.i(TAG, "not found,can not unregister");
            }
            Log.i(TAG, "unregisterListener current size:" + mListenerList.size());
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        mBookList.add(new Book(1, "Andrroid"));
        mBookList.add(new Book(2, "IOS"));
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (mIsServiceDestoryed.get()) {
                        Thread.sleep(5000);
                        onNewBookArrived(new Book(mBookList.size() + 1, "newBookName#" + (mBookList.size() + 1)));
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

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

    private void onNewBookArrived(Book book){
        mBookList.add(book);
        for (int i = 0; i < mListenerList.size(); i++) {
            try {
                mListenerList.get(i).onNewBookArrived(book);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        mIsServiceDestoryed.set(false);
    }
}

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

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        bindService(new Intent(this, BookManagerService.class), mConnection, Context.BIND_AUTO_CREATE);
    }

    private IBookManager mIBookManager;

    ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            mIBookManager = bookManager;
            try {
                Log.i(TAG, "query book list, list types:" + bookManager.getBookList().getClass().getCanonicalName());
                Log.i(TAG, "query book list:" + bookManager.getBookList().toString());
                bookManager.addBook(new Book(3, "java"));
                Log.i(TAG, "query book list:" + bookManager.getBookList().toString());
                bookManager.registerListener(mOnNewBookArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mOnNewBookArrivedListener = null;
        }
    };

    private IOnNewBookArrivedListener mOnNewBookArrivedListener = new IOnNewBookArrivedListener.Stub() {
        @Override
        public void onNewBookArrived(Book newBook) throws RemoteException {
            Message msg = new Message();
            msg.what = 1;
            msg.obj = newBook;
            handler.sendMessage(msg);
        }
    };

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if(msg.what == 1) {
                Log.i(TAG, "receive new book:" + msg.obj);
            }
        }
    };

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if(mIBookManager != null && mIBookManager.asBinder().isBinderAlive()) {
            Log.i(TAG, "uunregister listener:" + mOnNewBookArrivedListener);
            try {
                mIBookManager.unRegisterListener(mOnNewBookArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        unbindService(mConnection);
    }
}

运行程序,我们来看下结果,如下所示:

09-13 16:33:14.459 25712-25712/com.demo.text.demotext I/MainActivity: query book list, list types:java.util.ArrayList
09-13 16:33:14.459 25712-25712/com.demo.text.demotext I/MainActivity: query book list:[[bookId:1, bookName:Andrroid], [bookId:2, bookName:IOS]]
09-13 16:33:14.460 25712-25712/com.demo.text.demotext I/MainActivity: query book list:[[bookId:1, bookName:Andrroid], [bookId:2, bookName:IOS], [bookId:3, bookName:java]]
09-13 16:33:19.464 25712-25712/com.demo.text.demotext I/MainActivity: receive new book:[bookId:4, bookName:newBookName#4]
09-13 16:33:24.462 25712-25712/com.demo.text.demotext I/MainActivity: receive new book:[bookId:5, bookName:newBookName#5]
09-13 16:33:29.465 25712-25712/com.demo.text.demotext I/MainActivity: receive new book:[bookId:6, bookName:newBookName#6]

如果你以为到这里 AIDL 的介绍就结束了,那你就错了,之前说了,AIDL 远不止这么简单,目前还有一些难点使我们没有涉及到的,接下来我们继续研究。

从上面的代码可以看出,当 MainActivity 关闭时,我们会在 onDestory 中去解除已经注册服务端的 listener,这就相当于我们不想再接收图书馆的新书提醒了,所以我们可以随时结束这个提醒,按 Back 按键退出 MainActivity,下面看下 log:

09-13 16:33:33.457 25712-25712/com.demo.text.demotext I/MainActivity: uunregister listener:com.demo.text.demotext.MainActivity$5@df2ed17
09-13 16:33:33.457 25728-25740/com.demo.text.demotext:remote I/BMS: not found,can not unregister
09-13 16:33:33.457 25728-25740/com.demo.text.demotext:remote I/BMS: unregisterListener current size:1

从上面的 log 可以看出,程序没有像我们所预期的那样执行。在解除注册的过程中,服务端镜无法找到我们之前注册的那个 listener,在客户端我们注册和解除注册时,明明传递的是同一个 listener 啊!最终,服务端由于无法找到要接触的 listener 而宣告解除注册失败!这当然不是我们想要的结果,但仔细想想,好像这种方式确实无法解除注册。其实,这是必然的,这种解除注册的处理方式在日常开发中时常用到的,但是放到多进程中却无法奏效,因为 Binder 会把客户端传递过来的对象重新转换成一个新的对象。虽然我们在注册和解除注册过程中使用的是同一个客户端对象,但是通过 Binder 传递到服务器后,却产生了两个全新的对象。别忘了对象是不能跨进程直接传输的,对象的跨进程传输本质上都是反序列化的过程,这就是为什么 AIDL 中的自定义对象都必须要实现 Parcelable 接口的原因。不过没关系,接下来我们来解决这个问题。

RemoteCallbackList 是系统专门提供的用于删除跨进程 listener 的接口,RemoteCallBackList 是一个泛型,支持管理任意的 AIDL 接口,这点从它的声明就可以看出,因为所有的 AIDL 接口都继承自 IInterface 接口,

public class RemoteCallbackList<E extends IInterface> 

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

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

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

        IBinder key = listener.asBinder();
        Callback value = new Callback(listener, cookie);

到这里,我们应该明白了,虽然说多次跨进程传输客户端的同一个对象会在服务端生成不同的对象,但是这些新生成的对象有一个共同点,那就是它们底层的 Binder 对象是同一个,利用这个特性,就可以实现上面我们无法实现的功能。当客户端接触注册的时候,我们只要遍历服务端所有的 listener,找出那个和解注册 listener 具有相同 Binder 对象的服务端 listener 把它删除即可,这就是 RemoteCallbackList 为我们所做的事情。同时,RemoteCallbackList 还有一个很有用的功能,就是当客户端进程终止后,它能够自动移除客户端所注册的 listener。另外,RemoteCallbackList 内部自动实现了线程同步功能,所以我们使用它来注册和解除注册时,不需要做额外的线程同步工作。

RemoteCallbackList  使用起来很简单,我们要对 BookManagerService 做一些修改,首先创建一个 RemoteCallbackList  对象来替代 CopyOnWriteArrayList,如下所示:

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

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

        @Override
        public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
            mListenerList.register(listener);
            Log.i(TAG, "registerListener size:" + mListenerList.beginBroadcast());
            mListenerList.finishBroadcast();
        }

        @Override
        public void unRegisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
            boolean success = mListenerList.unregister(listener);
            if (success) {
                Log.i(TAG, "unregister success.");
            } else {
                Log.i(TAG, "not found, can not unregister.");
            }
            Log.i(TAG, "unregisterListener current size:" + mListenerList.beginBroadcast());
            mListenerList.finishBroadcast();
        }

接着修改 onNewBookArrived 方法,当有新书时,我们要通知所有已经注册的 listener,如下所示:

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

BookManagerService 的修改已经完毕了,我们接下来运行一下程序,看看是否完成我们想要的功能:

09-13 18:02:26.201 6696-6707/com.demo.text.demotext:remote I/BMS: unregister success.
09-13 18:02:26.201 6696-6707/com.demo.text.demotext:remote I/BMS: unregisterListener current size:0

使用 RemoteCallbackList 有一点需要注意,我们无法像操作 List 一样去操作它,尽管它的名字也带一个 List,但是它并不是一个 List。遍历 RemoteCallbackList ,必须要按照下面的方式进行,其中 beginBroadcast 和 finishBroadcast 必须要配对使用,哪怕我们仅仅是像获取 RemoteCallbackList 中的元素个数,这是必须注意的地方,否则将会报错:java.lang.IllegalStateException: beginBroadcast() called while already in a broadcast。

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

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

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

另外,由于客户端的 IOnNewBookArrivedListener 中的 onNewBookArrived 方法运行在客户端的 Binder 线程池中,所以不能在它里面去访问 UI 相关的内容,如果要访问 UI,请使用 Handler 切换到 UI 线程。

为了程序的健壮性,我们还需要做一件事,Binder 是可能意外死亡的,这往往是由于服务端进程意外停止了,这时我们需要重新连接服务。有两种方法:

(1)是给 Binder 设置 DeathRecipient 监听,当 Binder 死亡时,我们会收到 binderDied 方法的回调,在 binderDied 方法中我们可以重新连接远程服务,具体在前面已经说过了。请查看 Binder 概念详解

(2)在 onServiceDisconnected 中重新远程服务。

上述两种方法我们可以随意采用一种来使用,它们的区别在于:onServiceDisconnected 在客户端的 UI 线程中被回调,而 binderDied 在客户端的 Binder 线程池中被回调。也就是说,在 binderDied 方法中我们不能访问 UI。这就是它们的区别。

2. 使用 AIDL 进行权限验证

经过上面的分析,我们已经对 AIDL 有了一个系统性的认识,但是还差最有后一步,如何在 AIDL 中使用权限验证功能,默认情况下,我们的远程服务任何客户端都可以连接,但这应该不是我们愿意看到的,所以必须给服务加入权限验证功能。权限验证失败则服务调用府服务中的方法,在 AIDL 中进行权限验证,这里介绍两助攻方法。

第一种方法,我们可以在 onBind 中进行验证,验证不通过直接返回 null,这样验证失败的客户端直接无法绑定服务。至于验证方式有很多种,比如使用 permission 验证,使用这种验证方式,我们需要现在 AndroidManifest 中声明权限,比如:

    <permission
        android:name="com.demo.text.demotext.permission.ACCESS_BOOK_SERVICE"
        android:protectionLevel="normal" />

关于 permission 的定义这里就不做详细解释了,毕竟这里主要介绍的是 AIDL,定义了权限以后,就可以在 BookManagerService 的 onBind 方法中进行验证了。代码如下:

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        int check = checkCallingOrSelfPermission("com.demo.text.demotext.permission.ACCESS_BOOK_SERVICE");
        if(check == PackageManager.PERMISSION_DENIED) {
            return null;
        }
        return binder;
    }

一个应用来绑定我们的服务时,会验证这个应用的权限,如果他没有使用这个权限,onBind 方法则会直接返回 null,最终结果就是这个应用无法绑定到我们的服务, 这样就达到了权限验证的效果,这种方法同样适用于 Messenger 中。

如果我们自己内部的应用想绑定到我们的服务中,只需要在它的 AndroidManifest.xml 文件中采用如下方式使用 permission 即可。

    <uses-permission android:name="com.demo.text.demotext.permission.ACCESS_BOOK_SERVICE"/>

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

        @Override
        public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            int check = checkCallingOrSelfPermission("com.demo.text.demotext.permission.ACCESS_BOOK_SERVICE");
            if (check == PackageManager.PERMISSION_DENIED) {
                return false;
            }
            String packageName = null;
            String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
            if (null != packages && packages.length > 0) {
                //循环判断,偷个懒
                packageName = packages[0];
            }
            if (!packageName.startsWith("com.demo")) {
                return false;
            }
            return super.onTransact(code, data, reply, flags);
        }

上面介绍了两种 AIDL 中常用的权限验证方法,但是肯定还有其他的方法做权限验证,比如在 Service 指定 android:permission 属性等,这里就不一一进行介绍了。

代码转载自:https://pan.quark.cn/s/f87b8041184b Language: 中文 欢迎来到戈戈圈! 当你点开这个存储库的时候,你会看到戈戈圈的图标↓ 本图片均在知识共享 署名-相同方式共享 3.0(CC BY-SA 3.0)许可协议下提供,如有授权遵照授权协议使用。 那么恭喜你,当你看到这个图标的时候,就代表着你已经正式成为了一名戈团子啦! 欢迎你来到这个充满爱与希望的大家庭! 「与大家创造更多快乐,与人们一起改变世界。 」 戈戈圈是一个在中国海南省诞生的创作企划,由王戈wg的妹妹于2018年7月14日正式公开。 戈戈圈的创作类型广泛,囊括插画、小说、音乐等各种作品类型。 戈戈圈的目前成员: Contributors 此外,支持戈戈圈及本企划的成员被称为“戈团子”。 “戈团子”一词最初来源于2015年出生的名叫“团子”的大熊猫,也因为一种由糯米包裹着馅料蒸熟而成的食品也名为“团子”,不仅有团圆之意,也蕴涵着团结友爱的象征意义大家的美好期盼,因此我们最终于2021年初决定命名戈戈圈的粉丝为“戈团子”。 如果你对戈戈圈有兴趣的话,欢迎加入我们吧(σ≧︎▽︎≦︎)σ! 由于王戈wg此前投稿的相关视频并未详细说明本企划的信息,且相关视频的表述极其模糊,我们特此创建这个存储库,以文字的形式向大家介绍戈戈圈。 戈戈圈自2018年7月14日成立至今,一直以来都秉持着包容开放、谐友善的原则。 我们深知自己的责任使命,始终尊重社会道德习俗,严格遵循国家法律法规,为维护社会稳定公共利益做出了积极的贡献。 因此,我们不允许任何人或组织以“戈戈圈”的名义在网络平台或现实中发布不当言论,同时我们也坚决反对过度宣传戈戈圈的行为,包括但不限于与戈戈圈无关的任何...
内容概要:本文详细介绍了一个基于YOLOv8的血细胞智能检测系统全流程开发指南,涵盖从环境搭建、数据准备、模型训练与验证到UI交互系统开发的完整实践过程。项目利用YOLOv8高精度、高速度的优势,实现对白细胞、红细胞血小板的自动识别与分类,准确率超过93%,单张图像检测仅需0.3秒。通过公开或自建血细胞数据集,结合LabelImg标注工具Streamlit开发可视化界面,构建了具备图像上传、实时检测、结果统计与异常提示功能的智能系统,并提供了论文撰写与成果展示建议,强化其在医疗场景中的应用价值。; 适合人群:具备一定Python编程与深度学习基础,从事计算机视觉、医疗AI相关研究或项目开发的高校学生、科研人员及工程技术人员,尤其适合需要完成毕业设计或医疗智能化项目实践的开发者。; 使用场景及目标:①应用于医院或检验机构辅助医生进行血涂片快速筛查,提升检测效率与一致性;②作为深度学习在医疗影像领域落地的教学案例,掌握YOLOv8在实际项目中的训练、优化与部署流程;③用于学术论文写作与项目成果展示,理解技术与临床需求的结合方式。; 阅读建议:建议按照“数据→模型→系统→应用”顺序逐步实践,重点理解数据标注规范、模型参数设置与UI集成逻辑,同时结合临床需求不断优化系统功能,如增加报告导出、多类别细粒度分类等扩展模块。
基于蒙特卡洛,copula函数,fuzzy-kmeans获取6个典型场景进行随机优化多类型电动汽车采用分时电价调度,考虑上级电网出力、峰谷差惩罚费用、风光调度、电动汽车负荷调度费用网损费用内容概要:本文围绕多类型电动汽车在分时电价机制下的优化调度展开研究,采用蒙特卡洛模拟、Copula函数模糊K-means聚类方法获取6个典型场景,并在此基础上进行随机优化。模型综合考虑了上级电网出力、峰谷差惩罚费用、风光可再生能源调度、电动汽车负荷调度成本以及电网网损费用等多个关键因素,旨在实现电力系统运行的经济性与稳定性。通过Matlab代码实现相关算法,验证所提方法的有效性与实用性。; 适合人群:具备一定电力系统基础知识Matlab编程能力的研究生、科研人员及从事新能源、智能电网、电动汽车调度相关工作的工程技术人员。; 使用场景及目标:①用于研究大规模电动汽车接入电网后的负荷调控策略;②支持含风光等可再生能源的综合能源系统优化调度;③为制定合理的分时电价政策及降低电网峰谷差提供技术支撑;④适用于学术研究、论文复现与实际项目仿真验证。; 阅读建议:建议读者结合文中涉及的概率建模、聚类分析与优化算法部分,动手运行并调试Matlab代码,深入理解场景生成与随机优化的实现流程,同时可扩展至更多元化的应用场景如V2G、储能协同调度等。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值