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、跨进程传输接口

跨进程传递对象

image.png

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

以之前的获取学生信息为例:


image.png

如上图所示,客户端通过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、我们就会想到学生成绩发生变化了,服务端就主动通知我们就好啦。

如下图所示:


image.png

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

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
先来看看客户端表现:

tt0.top-671071.gif

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

再来看服务端表现:

tt0.top-354096.gif

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

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

注意事项

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

image.png

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

1、Android各种Context的前世今生
2、Android DecorView 一窥全貌(上)
3、Android DecorView 一窥全貌(下)
4、Window/WindowManager 不可不知之事
5、View Measure/Layout/Draw 真明白了
6、Android事件分发全套服务
7、Android invalidate/postInvalidate/requestLayout 彻底厘清
8、Android Window 如何确定大小/onMeasure()多次执行原因
9、Android事件驱动Handler-Message-Looper解析
10、Android 键盘一招搞定
11、Android 各种坐标彻底明了
12、Android Activity/Window/View 的background
13、Android IPC 之Service 还可以这么理解
14、Android IPC 之Binder基础
15、Android IPC 之Binder应用
16、Android IPC 之AIDL应用(上)
17、Android IPC 之AIDL应用(下)
18、Android IPC 之Messenger 原理及应用
19、Android IPC 之获取服务(IBinder)
20、Android 存储基础
21、Android 10、11 存储完全适配(上)
22、Android 10、11 存储完全适配(下)
23、Java 并发系列不再疑惑

推荐阅读更多精彩内容