与Service实现双向通信(二)

  1. 与Service实现双向通信(一)
  2. 与Service实现双向通信(二)
  3. 与Service实现双向通信(三)

这是系列文章《与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

Read more

Android 上的低功耗蓝牙实践

这是我在 Droidcon Beijing 2016 和 GDG Devfest 2016 上做的分享,以下是正文: Slide 01 我今天分享的主题是 Android 上低功耗蓝牙的实践。这个主题比较小众。我在过去的一年多的时间里,主要是在做低功耗蓝牙相关的开发。接触过程中发现,BLE 的开发和通常的 Android APP 的开发有点不一样,这里需要访问硬件资源,而且涉及到一些协议相关的内容,而且这方面的资料也比较少。今天我从 Android 开发者的角度,来分享一下低功耗蓝牙开发实践。 Slide 02 今天分享的内容,主要包含如下几个部分:首先对蓝牙和低功耗蓝牙做一个简单的介绍;然后介绍 Android 上对低功耗蓝牙的支持;再介绍一下在 Android 平台上可以开发哪些低功耗蓝牙应用;然后是,开发过程中,可以帮助我们调试的工具;最后,总结一下所谓的 “最佳实践”,低功耗蓝牙开发的一些小经验。 Slide

By Race604

React Native 触摸事件处理详解

触控是移动设备的核心功能,也移动应用交互的基础,Android 和 iOS 各自都有完善的触摸事件处理机制。React Native(以下简称 RN)提供了一套统一的处理方式,能够方便的处理界面中组件的触摸事件、用户手势等。本文尝试介绍 RN 中触摸事件处理。 1. RN 基本触摸组件 RN 的组件除了 Text,其他组件默认是不支持点击事件,也不能响应基本触摸事件,所以 RN 中提供了几个直接处理响应事件的组件,基本上能够满大部分的点击处理需求TouchableHighlight, TouchableNativeFeedback, TouchableOpacity 和 TouchableWithoutFeedback。因为这几个组件的功能和使用方法基本类似,只是 Touch 的反馈效果不一样,所以一般我们用 Touchable** 代替。Touchable** 有如下几个回调方法: * onPressIn:点击开始; * onPressOut:点击结束或者离开; * onPress:单击事件回调; * onLongPress:长按事件回调。 它们的基本使用方法如下,

By Race604

React Native 中 ScrollView 性能探究

1 基本使用 ScrollView 是 React Native(后面简称:RN) 中最常见的组件之一。理解 ScrollView 的原理,有利于写出高性能的 RN 应用。 ScrollView 的基本使用也非常简单,如下: <ScrollView> <Child1 /> <Child2 /> ... </ScrollView> 它和 View 组件一样,可以包含一个或者多个子组件。对子组件的布局可以是垂直或者水平的,通过属性 horizontal=true/false 来控制。甚至还默认支持“下拉”刷新操作。另外还有一个特别赞的特性,超出屏幕的 View 会自动被移除,从而节省资源和提高绘制效率。我们来看如下一个例子: class

By Race604

30 天入门 Android 开发, Google 与你一起圆梦

经常会有朋友让我推荐 Android 开发入门的教程或者视频,我一直是推荐看官方的教程。大部分人或者觉得比较迷茫,或者觉得坚持不下去。这次推荐这个《30 天入门 Android 开发》是 Google 亲自发起的免费教学,以学习小组方式,大家可以一起学习和交流。一个好的开始,是成功的一半。让 Google 工程师带领你一起进入多彩的 Android 开发大门。点击这里 报名。 Android 设备已经随处可见,你想尝试一下在 Android 设备上的开发和创新吗?快来跟随 Google 的步伐,一起学习 Android 入门课吧! Google Study Jams 活动介绍 Study Jams 是一个学习 Google 在线课程的活动。该活动由学员自主发起课程学习小组,带领小组成员入门 Android 开发,最终将

By Race604