IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    与Service实现双向通信(中)

    liuchi1993发表于 2017-03-08 23:11:45
    love 0

    这是系列文章《与Service实现双向通信》的第二篇。有了上一篇文章作为基础,本文实现一个稍微复杂一点的场景:

    Service端实现一个控制中心(例如一个多人游戏),客户端可以随时加入,或者退出,每个客户端都可以获取当前参与进来的成员列表。

    根据需求,在上一篇文章的代码的基础上,我们可以很容易申明如下接口:

    // IRemoteService.aidl
    package com.race604.servicelib;
    
    interface IRemoteService {  
        ...
    
        void join(String userName);
        void leave(String userName);
        List<String> getParticipators();
    }

    Service的实现也很简单,大致如下:

    // RemoteService.java
    package com.race604.remoteservice;
    
    import ...
    
    public class RemoteService extends Service {  
      private List<String> mClients = new ArrayList<>();
    
      private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
        @Override
        public void join(String name) throws RemoteException {
            mClients.add(name);
        }
    
        @Override
        public void leave(String name) throws RemoteException {
            mClients.remove(name);
        }
    
        @Override
        public List<String> getParticipators() throws RemoteException {
            return mClients;
        }
      };
    ...

    这里的实现非常简单,看起来也没有问题。

    客户端的实现我这里就不写了。我们期望Client调用join()和leave()成对出现。在离开的时候,注意调用leave()。

    但是,考虑一个情况:客户端意外退出。例如客户端因为错误应用Crash,或者被Kill掉了。没有机会调用到leave()。这样Service中的mClients中的还保持这个客户端的信息,得不到释放。这里还是一个简单的例子,但是如果Service中如果为Client申请了一些资源,客户端意外退出以后,Service中资源得不到释放,会造成资源浪费。

    幸运的是,Binder有可以让对端的进程得到意外退出通知的机制:Link-To-Death。我这里以我们这里Service被通知Client意外退出的情况为例,实现的方法如下:

    1. Client传递一个Binder对象给Service,此Binder对象与Client的进程关联;
    2. 在Sevice中接受到这个Binder对象,并且使用binder.linkToDeath(),注册一个DeathRecipient回调;
    3. 实现DeathRecipient。当Client意外退出的时候,DeathRecipient.binderDied()将被回调,我们可以在这里释放相关的资源。

    具体实现如下: 修改AIDL的定义如下:

    // IRemoteService.aidl
    package com.race604.servicelib;
    
    interface IRemoteService {  
        ...
    
        void join(IBinder token, String name);
        void leave(IBinder token);
        List<String> getParticipators();
    }

    注意到这里接口中传入了一个IBinder对象token,此就是客户端的唯一标示。

    接下来重点看一下Service的实现。我们首先定义个类来保存Client的信息,如下:

    private final class Client implements IBinder.DeathRecipient {  
        public final IBinder mToken;
        public final String mName;
    
        public Client(IBinder token, String name) {
            mToken = token;
            mName = name;
        }
    
        @Override
        public void binderDied() {
            // 客户端死掉,执行此回调
            int index = mClients.indexOf(this);
            if (index < 0) {
                return;
            }
    
            Log.d(TAG, "client died: " + mName);
            mClients.remove(this);
        }
    }

    这里为了方便,因为每个IBinder都需要注册一个IBinder.DeathRecipient回调,我们就直接让Client实现此接口。

    Service中保存客户端的信息也做如下修改:

    private List<Client> mClients = new ArrayList<>();
    
    // 通过IBinder查找Client
    private int findClient(IBinder token) {  
        for (int i = 0; i < mClients.size(); i++) {
            if (mClients.get(i).mToken == token) {
                return i;
            }
        }
        return -1;
    }

    然后修改join()的实现如下:

    @Override
    public void join(IBinder token, String name) throws RemoteException {  
        int idx = findClient(token);
        if (idx >= 0) {
            Log.d(TAG, "already joined");
            return;
        }
    
        Client client = new Client(token, name);
        // 注册客户端死掉的通知
        token.linkToDeath(client, 0);
        mClients.add(client);
    }

    注意到这里的token.linkToDeath(client, 0);,表示的含义就是与token(IBinder对象)关联的客户端,如果意外退出,就会回调client.binderDied()方法。

    同理leave()的实现如下:

    @Override
    public void leave(IBinder token) throws RemoteException {  
        int idx = findClient(token);
        if (idx < 0) {
            Log.d(TAG, "already left");
            return;
        }
    
        Client client = mClients.get(idx);
        mClients.remove(client);
    
        // 取消注册
        client.mToken.unlinkToDeath(client, 0);
    }

    当调用leave的时候,释放相关资源,取消IBinder.DeathRecipient回调,即client.mToken.unlinkToDeath(client, 0);。

    客户端调用就比较简单了,主要代码如下:

    package com.race604.client;
    
    import ...
    
    public class MainActivity extends ActionBarActivity {  
        ...
        private IBinder mToken = new Binder();
        private boolean mIsJoin = false;
    
        private void toggleJoin() {
            if (!isServiceReady()) {
                return;
            }
    
            try {
                if (!mIsJoin) {
                    String name = "Client:" + mRand.nextInt(10);
                    mService.join(mToken, name);
                    mJoinBtn.setText(R.string.leave);
                    mIsJoin = true;
                } else {
                    mService.leave(mToken);
                    mJoinBtn.setText(R.string.join);
                    mIsJoin = false;
                }
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    }

    至此,核心代码都实现了。完整的代码请参考这个Commit。

    我们做如下测试,在客户端join()后,然后在最近任务列表中,删除client应用,我们看到service端打印信息:

    02-04 14:01:23.627: D/RemoteService(29969): client died: Client:6

    可见,我们Kill掉客户端,回调到了这里:

    private final class Client implements IBinder.DeathRecipient {  
        ...
        @Override
        public void binderDied() {
            // 客户端死掉,执行此回调
            ...
            Log.d(TAG, "client died: " + mName);
            ...
        }
    }

    参考文章:

    1. Binders & Death Recipients

     本系列:

    • 与Service实现双向通信(上)
    • 与Service实现双向通信(中)

    相关文章

    • 与Service实现双向通信(上)
    • 经典论文翻译导读之《Dremel: Interactive Analysis of WebScale Datasets》
    • 在测试中使用匹配器
    • notify和notifyAll的一段代码分析
    • 怎样编写高质量的Java代码
    • Java虚拟机类加载机制——案例分析
    • 应对Memcached缓存失效,导致高并发查询DB的几种思路
    • 渐析java的浅拷贝和深拷贝
    • [JAVA · 初级]:容器类
    • 从MyEclipse到IntelliJ IDEA


沪ICP备19023445号-2号
友情链接