Android IPC 之服务端回调

前言

IPC 系列文章:
建议按顺序阅读。

Android IPC 之Service 还可以这么理解
Android IPC 之Binder基础
Android IPC 之Binder应用
Android IPC 之AIDL应用(上)
Android IPC 之AIDL应用(下)
Android IPC 之Messenger 原理及应用
Android IPC 之服务端回调
Android IPC 之获取服务(IBinder)

前面几篇文章详细分析了AIDL的使用,包括数据在客户端和服务端的传输,本篇将分析AIDL 回调的使用。
通过本篇文章,你将了解到:

1、跨进程传输接口
2、AIDL 回调的使用
3、回调在四大组件里的应用

1、跨进程传输接口

跨进程传递对象

Android IPC 之服务端回调

基本数据类型,如int、short 、String 等不用做任何处理可通过Binder直接传送。而复杂数据类型,如自定义的类,需要实现Parcelable 接口才能通过Binder传送。

以之前的获取学生信息为例:
Android IPC 之服务端回调

如上图所示,客户端通过IPC 从服务端获取学生信息,学生信息封装在Student类里:

public class Student implements Parcelable {
    private String name;
    private int age;
    private float score;
    ...
}

学生信息包括姓名、年龄、分数三个字段。
我们定义AIDL接口如下:

interface IStudentInfo {
    //主动获取
    Student getStudentInfo();
}

客户端通过调用 getStudentInfo() 方法即可获取从服务端返回的学生信息。

跨进程传递接口

客户端想要获取学生信息,需要主动调用 getStudentInfo() 方法。考虑一种场景:

1、学生每一门考试,分数都在变化,客户端需要一直轮询去调用getStudentInfo() 方法才能获取最新的成绩。我们知道轮询是效率比较低的做法,要尽量避免。
2、我们就会想到学生成绩发生变化了,服务端就主动通知我们就好啦。

如下图所示:
Android IPC 之服务端回调

现在的问题重点是:服务端如何主动通知客户端。
依据以往的经验,有两种方式可以实现:

1、客户端通过绑定服务端的Service,进而与服务端通信,那么可以换种思路,客户端也可以定义Service,而后服务端通过绑定客户端,进而调用客户端的接口,主动给客户端传递消息。
2、客户端绑定了服务端的Service,两者之间就能够通信。实际上服务端传递了Binder给客户端,客户端拿到Binder之后就可以进行通信了,这就说明了Binder对象本身能够跨进程传输。
于是改造之前的接口:
客户端调用服务端接口的时候将自己生成的Binder传递给服务端,那么服务端发生变化的时候就可以通过这个Binder来通知客户端了。

通过比对1、2两种方式:
第一种方式过于复杂,对于客户端、服务端的角色容易搞混。
第二种方式符合我们认知的"回调",也就是说跨进程的回调和同一个进程里的回调理解上是一致的。

2、AIDL 回调的使用

服务端声明回调接口

定义AIDL 回调接口:

import com.fish.ipcserver.Student;
interface RemoteCallback {
    //回调
    oneway void onCallback(in Student student);
}

Student 为学生信息类,该对象支持跨进程传输。
in 表示数据流方向,表示该Student 对象传递给客户端。
oneway 表示调用onCallback(xx) 方法的线程立即返回,不阻塞等待方法调用结果。

服务端暴露注册回调接口方法

服务端定义了回调接口,客户端需要给服务端传递接口的实现。因此服务端还需要将注册回调的接口暴露给客户端。
定义AIDL 文件如下:

import com.fish.ipcserver.Student;
import com.fish.ipcserver.RemoteCallback;

interface IStudentInfo {
    //主动获取
    Student getStudentInfo();
    //注册回调
    oneway void register(in RemoteCallback callback);
}

至此,服务端提供了两个方法:

1、getStudentInfo() 客户端调用此方法主动获取学生信息。
2、register(xx) 客户端调用此方法注册回调实例。

服务端编写回调逻辑

public class StudentService extends Service {

    private Student student;
    private RemoteCallback remoteCallback;
    private MyStudent myStudent;

    @Override
    public void onCreate() {
        super.onCreate();
        student = new Student();
        student.setAge(19);
        student.setName("小明");
        myStudent = new MyStudent();
    }

    class MyStudent extends IStudentInfo.Stub {
        @Override
        public Student getStudentInfo() throws RemoteException {
            return student;
        }

        @Override
        public void register(RemoteCallback callback) throws RemoteException {
            //客户端注册的回调实例保存到成员变量 remoteCallback
            remoteCallback = callback;
        }

        public void changeScore() {
            //学生成绩发生改变
            student.setScore((float)(Math.random() * 100));
            try {
                if (remoteCallback != null)
                    //调用回调实例方法,将变化后的学生信息传递给客户端
                    remoteCallback.onCallback(student);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        //将Stub 返回给客户端
        return myStudent.asBinder();
    }
}

可以看出,声明了IStudentInfo 实例。
小结上面的逻辑:

1、服务端声明了Stub(桩,实际上是Binder实例),并将Stub返回给客户端。
2、客户端收到Stub(实际上是BinderProxy),然后转换为IStudentInfo 接口。而该接口里声明了两个方法,分别是getStudentInfo()和register(xx)。
3、客户端调用register(RemoteCallback) 将回调注册(传递)给服务端。
4、服务端发生变化的时候通过RemoteCallback 通知客户端数据已经发生改变。

客户端编写调用逻辑

分三步:
(1)、客户端绑定服务端Service。
(2)、建立连接后客户端将IBinder 转化为IStudentInfo 接口,并注册回调。
(3)、客户端处理回调内容。

来看看代码实现:

(1)绑定服务

        //参数1:运行远程服务的包名
        //参数2:远程服务全限定类名
        ComponentName componentName = new ComponentName("com.fish.ipcserver", "com.fish.ipcserver.StudentService");
        Intent intent = new Intent();
        intent.setComponent(componentName);
        //绑定远程服务
        v.getContext().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);

(2)IBinder 转换为IStudentInfo 接口

    ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            isConnected = true;
            //转为对应接口
            iStudentInfo = IStudentInfo.Stub.asInterface(service);
            try {
                //注册回调
                iStudentInfo.register(remoteCallback);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            isConnected = false;
        }
    };

(3)客户端处理回调

    //声明回调
    RemoteCallback remoteCallback = new RemoteCallback.Stub() {
        @Override
        public void onCallback(Student student) throws RemoteException {
            Log.d("fish", "call back student:" + student);
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(IPCActivity.this, "client receive change:" + student.toString(), Toast.LENGTH_SHORT).show();
                }
            });
        }
    };

此处收到服务端的回调后,仅仅Toast 学生信息。

测试效果

为了更贴近实际应用效果,客户端、服务端分别跑在不同的App里。
客户端的应用名为:AndroidDemo
服务端的应用名为:IPCServer
先来看看客户端表现:
Android IPC 之服务端回调

步骤如下:
1、当点击按钮时,客户端判断没有连接上服务端,于是开始连接。
2、连接成功后,开始注册服务端接口。
3、再次点击按钮时,通过getStudentInfo()方法主动获取学生信息。

再来看服务端表现:
Android IPC 之服务端回调
步骤如下:
1、服务端收到客户端绑定请求。
2、服务端收到客户端注册的回调接口。
3、服务端点击按钮改变学生分数,并通过回调接口通知客户端。
4、客户端收到后弹出Toast。

通过以上两个测试效果可以看出,客户端不仅能够主动调用服务端方法,同时也可以通过回调监听服务端的变化。

注意事项

1、自定义类型Student.java 与Student.aidl 需要在同一个包名下。
2、客户端与服务端定义的aidl 文件需要在同一个包名下。通常来说,一般先定义服务端aidl 接口,最后将这些aidl文件拷贝到客户端相同包名下。
3、bindService Intent 需要指定ComponentName。

Android IPC 之服务端回调

3、回调在四大组件里的应用

以ContentProvider 为例:
想要获取相册数据,可以通过ContentProvider获取,而相册是公共的存储图片区域,其它App都可以往里面插入数据或者删除数据。
而系统也提供了监听相册变化的回调:

Handler handler = new Handler(Looper.getMainLooper());
    ContentObserver contentObserver = new ContentObserver(handler) {
        @Override
        public void onChange(boolean selfChange) {
            //数据变化回调
            super.onChange(selfChange);
        }
    };
    getContentResolver().registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, contentObserver);

如上,通过registerContentObserver(xx)向系统(服务端)注册了回调接口,当有数据变化的时候服务端会调用onChange(xx)通知客户端。

不仅ContentProvider 运用到了回调,Service、Activity、Broadcast也用到了。

理解了进程间的回调原理及其使用,对理解四大组件的通信帮助很大。

下篇将重点分析四大组件的框架。

本文基于Android 10.0
完整代码演示 若是有帮助,给github 点个赞呗~

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android/Java

上一篇:进程间通信IPC操作记录


下一篇:Android IPC 之获取服务(IBinder)