美文网首页
IPC之AIDL进阶用法

IPC之AIDL进阶用法

作者: 一线游骑兵 | 来源:发表于2018-09-11 20:19 被阅读9次

    接上篇笔记 AIDL入门用法

    主要包括下边四点:

    1. 跨进程的接口回调【观察者】
    2. 线程问题
    3. 断开重连问题
    4. 权限校验问题

    一:跨进程的观察者
    Step1. 首先定义一个通知客户端的回调接口
    // OnNewBookInsertListener.aidl
    package com.zhu.aidldemo;
    
    import com.zhu.aidldemo.Book;
    
    interface OnNewBookInsertListener {
       void onNewBookInsert(in Book book);
    }
    

    因为在AIDL文件中不支持接口,所以定义成aidl文件。

    Step2. 增加IBookManager.aidl方法
    // IBookManager.aidl
    package com.zhu.aidldemo;
    import com.zhu.aidldemo.Book;
    import com.zhu.aidldemo.OnNewBookInsertListener;
    
    interface IBookManager {
       void addBook(in Book book);
       List<Book> getBookList();
       void registerBookInsertListener(OnNewBookInsertListener listener);
       void unregisterBookInsertListener(OnNewBookInsertListener listener);
    }
    

    新增注册与注销观察者的方法。然后Make Project.

    Step3. 在服务端增加相关逻辑代码。
    public class BookManagerService extends Service {
        private final static String TAG = "server";
    
        private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();
        private CopyOnWriteArrayList<OnNewBookInsertListener> mListenerList = new CopyOnWriteArrayList<>();
        private AtomicBoolean mIsServiceDestoryed = new AtomicBoolean(false);
    
        @Override
        public void onCreate() {
            super.onCreate();
            mBookList.add(new Book(1, "人类简史"));
            mBookList.add(new Book(2, "三体"));
            new Thread(new BookAutoInsertWorker()).start(); //每个5s自动向bookList中插入一条数据
        }
    
        private Binder mBinder = new IBookManager.Stub() {
            @Override
            public void addBook(Book book) throws RemoteException {
                Log.d(TAG, "receive a request from client, add book to list...");
                mBookList.add(book);
            }
    
            @Override
            public List<Book> getBookList() throws RemoteException {
                Log.d(TAG, "receive a request from client, query book list...");
                return mBookList;
            }
    
            @Override
            public void registerBookInsertListener(OnNewBookInsertListener listener) throws RemoteException {
                if (!mListenerList.contains(listener)) {
                    Log.d(TAG, "reguister listener to list..." + listener.toString());
                    mListenerList.add(listener);
                } else {
                    Log.d(TAG, "listener already exists...");
                }
                Log.d(TAG, "listenerList size: " + mListenerList.size());
            }
    
            @Override
            public void unregisterBookInsertListener(OnNewBookInsertListener listener) throws RemoteException {
                if (mListenerList.contains(listener)) {
                    Log.d(TAG, "remove listener from list..." + listener.toString());
                    mListenerList.remove(listener);
                } else {
                    Log.d(TAG, "listener not exists...");
                }
                Log.d(TAG, "listenerList size: " + mListenerList.size());
            }
        };
    
        private class BookAutoInsertWorker implements Runnable {
    
            @Override
            public void run() {
                while (!mIsServiceDestoryed.get()) {
                    try {
                        Thread.sleep(5000);
                        int bookId = mBookList.size() + 1;
                        Book newBook = new Book(bookId, "New book#" + bookId);
                        insertBookAndNotifyUser(newBook);
                    } catch (Exception e) {
                        Log.e(TAG, e.getMessage());
                    }
                }
            }
        }
    
        private void insertBookAndNotifyUser(Book newBook) {
            try {
                mBookList.add(newBook);
                Log.d(TAG, "add a new book: " + newBook.toString() + " , size: " + mBookList.size());
                for (int i = 0; i < mListenerList.size(); i++) {
                    OnNewBookInsertListener onNewBookInsertListener = mListenerList.get(i);
                    Log.d(TAG, "notify user > " + onNewBookInsertListener.toString());
                    onNewBookInsertListener.onNewBookInsert(newBook);
                }
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    
    
        @Nullable
        @Override
        public IBinder onBind(Intent intent) {
            return mBinder;
        }
    
        @Override
        public void onDestroy() {
            super.onDestroy();
            mIsServiceDestoryed.set(true);
        }
    }
    

    新增逻辑如下:

    • 定义了一个观察者集合:mListenerList,用来记录注册的观察者
    • 在mBinder中实现了新增的两个方法。注册观察者与注销观察者
    • 开启了一个线程每5s向mBookList中新增一条数据并通知所有的观察者
    Step4. 客户端注册并监听相关方法。
    class MainActivity : AppCompatActivity() {
        val TAG = "client"
    
        val MSG_NEW_BOOK_INSERTED = 1
        private var mHandler = object : Handler() {
            override fun handleMessage(msg: Message?) {
                super.handleMessage(msg)
                when (msg?.what) {
                    MSG_NEW_BOOK_INSERTED -> {
                        Log.d(TAG, "receive a notify from server: " + msg.obj.toString())
                    }
                    else -> super.handleMessage(msg)
                }
            }
        }
    
        private var onNewBookInsertListener = object : OnNewBookInsertListener.Stub() {
    
            override fun onNewBookInsert(book: Book?) {
                mHandler.obtainMessage(MSG_NEW_BOOK_INSERTED, book).sendToTarget()
            }
        }
    
        private var mBookManager: IBookManager? = null
        private val serviceConnection = object : ServiceConnection {
            override fun onServiceConnected(name: ComponentName, service: IBinder) {
                var bookManager = IBookManager.Stub.asInterface(service)
                mBookManager = bookManager
                try {
                    var bookList = bookManager.bookList
                    Log.d(TAG, "init book list on server: " + bookList.toString())
                    bookManager.addBook(Book(3, "未来简史"))
                    Log.d(TAG, "insert a book from client...")
                    Log.d(TAG, "book list on server after insert : " + bookManager.bookList.toString())
                    bookManager.registerBookInsertListener(onNewBookInsertListener)
                } catch (e: Exception) {
                    println(e.message)
                }
            }
    
            override fun onServiceDisconnected(name: ComponentName) {
                Log.d(TAG, "onServiceDisconnected...")
                mBookManager = null
            }
        }
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            var intent = Intent(this, BookManagerService::class.java)
            bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
        }
    
        override fun onDestroy() {
            super.onDestroy()
            if (mBookManager != null && mBookManager?.asBinder()?.isBinderAlive!!) {
                try {
                    Log.d(TAG, "unregister listener....")
                    mBookManager?.unregisterBookInsertListener(onNewBookInsertListener)
                } catch (e: Exception) {
                    Log.e(TAG, e.message)
                }
    
            }
            if (mHandler != null) {
                mHandler.removeCallbacksAndMessages(null)
            }
            unbindService(serviceConnection)
        }
    }
    

    新增逻辑如下:

    1. 获取服务端初始图书集合
    2. 调用服务端方法新增一条数据
    3. 注册成为一个观察者
    4. 当收到新增图书通知时打印相关信息
    5. 页面退出时注销观察者
    结果:

    客户端日志:


    image.png

    服务端日志:


    image.png
    存在的问题:

    有日志可以观察到,在客户端页面退出时,调用服务端的注销观察者方法,并没有注销成功,而且客户端在退出时,依然能再次受到一条通知。猜测每次传入过去的onNewBookInsertListener都是一个新的对象。
    对象是不能跨进程传输的,对象的跨进程传输本质上是一中反序列化,因此Binder回把客户端传过来的对象重新转化并生成一个新的对象。因此无法像单进程一样操作list集合。

    解决的方法:

    使用RemoteCallbackList。对BookManagerService中的mListenerList做如下修改:

    1. CopyOnWriteArrayList改为RemoteCallbackList
        private RemoteCallbackList<OnNewBookInsertListener> mListenerList = new RemoteCallbackList<>();
    
    1. 适配注册观察者与反注册观察者方法:
            @Override
            public void registerBookInsertListener(OnNewBookInsertListener listener) throws RemoteException {
                boolean result = mListenerList.register(listener);
                if (result) {
                    Log.d(TAG, "reguister listener to list success..." + listener.toString());
                } else {
                    Log.d(TAG, "register listener failed...");
                }
                int count = mListenerList.beginBroadcast();
                Log.d(TAG,"listener size : "+count);
                mListenerList.finishBroadcast();
            }
    
            @Override
            public void unregisterBookInsertListener(OnNewBookInsertListener listener) throws RemoteException {
                boolean result = mListenerList.unregister(listener);
                if (result) {
                    Log.d(TAG, "unregister listener success..." + listener.toString());
                } else {
                    Log.d(TAG, "unregister listener failed...");
                }
                int count = mListenerList.beginBroadcast();
                Log.d(TAG,"listener size : "+count);
                mListenerList.finishBroadcast();
            }
    
    1. 修改通知客户端的方法:
         private void insertBookAndNotifyUser(Book newBook) {
            try {
                mBookList.add(newBook);
                final int N = mListenerList.beginBroadcast();
                Log.d(TAG, "add a new book: " + newBook.toString() + " , size: " + mBookList.size());
                for (int i = 0; i < N; i++) {
                    OnNewBookInsertListener onNewBookInsertListener = mListenerList.getBroadcastItem(i);
                    if (onNewBookInsertListener != null) {
                        Log.d(TAG, "notify user > " + onNewBookInsertListener.toString());
                        onNewBookInsertListener.onNewBookInsert(newBook);
                    }
                }
                mListenerList.finishBroadcast();
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    
    1. 结果:
      客户端日志:


      image.png

    服务端日志:


    image.png
    1. RemoteCallbackList说明:
    • 虽说多次跨进程传输客户端的对象会在服务端生成不同的对象,但新生的对象都有一个共同点,即他们底层的Binder对象是同一个。当客户端解注册时,我们只需遍历服务端所有的listener,找出和解注册listener具有相同Binder对象的服务端listener并删除即可。同时,客户端进程终止后,它能自动移除客户端所注册的listener。另外,RemoteCallbackList内部自动实现了线程同步的功能,所以使用时不需做额外的线程同步工作。
    • 我们无法像操作正常List一样操作它,它并不是List,操作它时beginBroadcast()finishBroadcast()必须配对使用。

    二:线程问题
    1. IPC时服务端的耗时操作导致的ANR

    由于客户端的onServiceConnected方法实在UI线程中执行的,所有当在该方法内部调用服务端比较耗时的方法时,容易导致客户端ANR,因此在该方法中最好开启线程调用服务端的方法。
    打印结果如下:

    image.png
    当在服务端的addBook 以及 getBookList方法中睡眠6s后客户端多次调用回发生ANR:
    image.png image.png

    解决方法:在onServiceConnected方法开启新的线程并在线程内部调用服务端方法,如需操作UI,可通过Handler或runOnUiThread等方法实现。
    另外,由于服务端的方法本身就运行在服务端的Binder线程池中,所以服务端方法本身就可以执行大量耗时操作,因此不需再开启新线程

    2. IPC时客户端耗时操作导致的问题

    同样,如果再服务端调用客户端的方法时,如果客户端的方法中进行了耗时的操作,比如在该demo中,当服务端新增一条数据时,会通过onNewBookInsertListener.onNewBookInsert(newBook);回调客户端的方法。该种情况请确保客户端的回调不是在UI线程中接受即可。否则也会造成客户端的ANR。

    image.png
    该方法运行在客户端的Binder线程池中,因此不能在该方法中进行UI操作。
    三:服务意外断开重连问题

    当服务端进程意外停止,Binder会意外死亡,此时需要客户端重新进行连接服务。

    1. 方法一:该Binder设置DeathRecipient监听

    首先实现IBinder.DeathRecipient接口,并重写binderDied方法。

    image.png
    在接受到binder终止后重新绑定远程服务
    之后将该回调设置给binder:
      var bookManager = IBookManager.Stub.asInterface(service)
      mBookManager = bookManager
      mBookManager?.asBinder()?.linkToDeath(this@MainActivity, 0)
    

    测试: 使用 adb shell kill pid 杀死远程服务进程【adb shell ps】,然后客户端打印log如下:查看进程信息

    image.png
    四:IPC权限校验

    为了防止任何人都可以连接远程服务,因此有时需要在客户端连接时进行权限校验。

    方法1:在onBind中进行验证

    首先在客户端的清单文件中声明调用权限:

        <permission
            android:name="com.zhu.aidldemo.permission.ACCESS_BOOK_MANAGE_SERVICE"
            android:protectionLevel="normal" />
    
        <uses-permission android:name="com.zhu.aidldemo.permission.ACCESS_BOOK_SERVICE" />
    

    然后在服务端的onBind方法中进行校验:

        @Nullable
        @Override
        public IBinder onBind(Intent intent) {
            int check = checkCallingOrSelfPermission("com.zhu.aidldemo.permission.ACCESS_BOOK_SERVICE");
            if (check == PackageManager.PERMISSION_DENIED) {
                Log.d(TAG, "permission denied....");
                return null;
            }
            Log.d(TAG, "permission granted....");
            return mBinder;
        }
    

    结果:


    image.png

    反例【更改onBind中的权限内容】:


    image.png
    方法2:在onTransact方法中进行权限校验

    重写服务端创建Binder中的onTransact方法:

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

    双重校验:先校验是否有权限,然后校验用户uid的包名。

    测试结果:

    正例:


    image.png

    反例【修改服务端校验的包名】:


    image.png

    完。

    相关文章

      网友评论

          本文标题:IPC之AIDL进阶用法

          本文链接:https://www.haomeiwen.com/subject/zcfpgftx.html