写点什么

手把手讲解 IPC 框架,成为一名合格 Android 架构师

用户头像
Android架构
关注
发布于: 2021 年 11 月 03 日

}


private ServiceConnection connection = new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {iUserInfo = IUserInfo.Stub.asInterface(service);resultView.setText("连接成功");Log.d(TAG, "connection:" + "连接成功");}


@Overridepublic void onServiceDisconnected(ComponentName name) {iUserInfo = null;resultView.setText("连接 已经断开");Log.d(TAG, "connection:" + "已经断开");}};


@Overrideprotected void onDestroy() {super.onDestroy();unbindService(connection);}}


很容易发现,服务端的代码量尚可,不是很复杂,但是客户端这边,要处理 connection,要手动去绑定以及解绑Service,所有参与通信的javabean还必须实现序列化接口parcelableDemo中只有一个客户端,还不是很明显,但是如果有N个客户端Activity都需要与service发生通信,意味着每一个Activity都必须写类似的代码. 不但累赘,而且丑陋.

前方高能

不使用 RPC 框架时,CS 两端的代码的结构,已经有了大致的印象,下面是 使用IPC框架时 客户端、服务端 的核心代码

客户端

之前的bindService呢?没了。客户端使用此框架来进行 进程通信,不用去关心AIDL怎么写了,不用关注bindService,ServiceConnection,省了很多事。

服务端
对比 使用框架前后,我们的核心代码的变化

有什么变化?显而易见,极大缩减了客户端的编码量,而且,一劳永逸,除非需求大改,不然这个框架,一次编写,终身使用。除此之外,使用框架还可以极大地节省客户端代码量,减少人为编码时产生的可能疏漏(比如忘记释放连接造成泄漏等). 试想一下,如果你是一个团队leader,团队成员的水平很有可能参差不齐,那么如何保证项目开发中 出错概率最小 ------- 使用框架, 用框架来简化团队成员的编码量编码难度,让他们傻瓜式地写代码.



三、Demo 展示

github 地址https://github.com/18598925736/MyRpc


以上 Demo,模拟的场景是:服务端:开启一个登录服务,启动服务之后,保存一个可以登录的用户名和密码客户端 1RPC调用登录服务,用户名和密码 和服务端的一样,可以登录成功客户端 2RPC调用登录服务,用户名和密码 和服务端的不一样,登录失败

Demo工程代码结构图


客户端和服务端必须同时依赖框架层 module implementation project(":ipc")



四、框架核心思想讲解

我们不使用IPC框架时,有两件事非常恶心:


1. 随着业务的扩展,我们需要频繁(因为要新增业务接口)改动AIDL文件,而且AIDL修改起来没有任何代码提示,只有到了编译之后,编译器才会告诉我哪里错了,而且 直接引用到的JavaBean还必须手动再声明一次。实在是不想在这个上面浪费时间。2. 所有客户端Activity,只要想进行进程间binder通信,就不可避免要去手动bindService,随后去处理 Binder连接,重写ServiceConnection,还要在适当的时候释放连接,这种业务不相关而且重复性很大的代码,要尽量少写。


IPC 框架将会着重解决这两个问题。下面开始讲解核心设计思想


注:1.搭建框架牵涉的知识面会很广,我不能每个细节都讲得很细致,一些基础部分一笔带过的,如有疑问,希望能留言讨论。2.设计思路都是环环相扣的,阅读时最好是从上往下依次理解.

框架思想四部曲:

1)业务注册

上文说到,直接使用AIDL通信,当业务扩展时,我们需要对AIDL文件进行改动,而改起来比较费劲,且容易出错。怎么办?利用 业务注册的方式,将 业务类class对象,保存到服务端 内存中。进入 Demo 代码 Registry.java


public class Ipc {


/**


  • @param business*/public static void register(Class<?> business) {//注册是一个单独过程,所以单独提取出来,放在一个类里面去 Registry.getInstance().register(business);//注册机是一个单例,启动服务端,// 就会存在一个注册机对象,唯一,不会随着服务的绑定解绑而受影响}...省略无关代码}


/**


  • 业务注册机*/public class Registry {...省略不关键代码


/**


  • 业务表/private ConcurrentHashMap<String, Class<?>> mBusinessMap= new ConcurrentHashMap<>();/*

  • 业务方法表, 二维 map,key 为 serviceId 字符串值,value 为 一个方法 map - key,方法名;value*/private ConcurrentHashMap<String, ConcurrentHashMap<String, Method>> mMethodMap= new ConcurrentHashMap<>();


/**


  • 业务类的实例,要反射执行方法,如果不是静态方法的话,还是需要一个实例的,所以在这里把实例也保存起来*/private ConcurrentHashMap<String, Object> mObjectMap = new ConcurrentHashMap<>();


/**


  • 业务注册

  • 将业务 class 的 class 和 method 对象都保存起来,以便后面反射执行需要的 method*/public void register(Class<?> business) {//这里有个设计,使用注解,标记所使用的业务类是属于哪一个业务 ID,在本类中,ID 唯一 ServiceId serviceId = business.getAnnotation(ServiceId.class);//获取那个类头上的注解 if (serviceId == null) {throw new RuntimeException("业务类必须使用 ServiceId 注解");}String value = serviceId.value();mBusinessMap.put(value, business);//把业务类的 class 对象用 value 作为 key,保存到 map 中


//然后要保存这个 business 类的所有 method 对象 ConcurrentHashMap<String, Method> tempMethodMap = mMethodMap.get(value);//先看看方法表中是否已经存在整个业务对应的方法表 if (tempMethodMap == null) {tempMethodMap = new ConcurrentHashMap<>();//不存在,则 newmMethodMap.put(value, tempMethodMap);// 并且将它存进去}for (Method method : business.getMethods()) {String methodName = method.getName();Class<?>[] parameterTypes = method.getParameterTypes();String methodMapKey = getMethodMapKeyWithClzArr(methodName, parameterTypes);tempMethodMap.put(methodMapKey, method);}...省略不关键代码}


...省略不关键代码


/**


  • 如何寻找到一个 Method?

  • 参照上面的构建过程,

  • @param serviceId

  • @param methodName

  • @param paras

  • @return*/public Method findMethod(String serviceId, String methodName, Object[] paras) {ConcurrentHashMap<String, Method> map = mMethodMap.get(serviceId);String methodMapKey = getMethodMapKeyWithObjArr(methodName, paras); //同样的方式,构建一个 StringBuilderreturn map.get(methodMapKey);}


/**


  • 放入一个实例

  • @param serviceId

  • @param object*/public void putObject(String serviceId, Object object) {mObjectMap.put(serviceId, object);}


/**


  • 取出一个实例

  • @param serviceId*/public Object getObject(String serviceId) {return mObjectMap.get(serviceId);}}


/**


  • 自定义注解,用于注册业务类的*/@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface ServiceId {String value();}


我利用一个单例的Registry类,将当前这个业务class对象,拆解出每一个Method,保存到map集合中。而,保存这些Class,Method,则是为了 反射执行指定的业务Method做准备。此处有几个精妙设计:1, 利用自定义注解 @ServiceId 对业务接口和实现类,都形成约束,这样业务实现类就有了进行唯一性约束,因为在Registry类中,一个ServiceId只针对一种业务,如果用Registry类注册一个没有@ServiceId注解的业务类,就会抛出异常。


2, 利用注解@ServiceIdvalue作为 key,保存所有的业务实现类的Class , 以及该Class的所有publicMethodmap集合中,通过日志打印,很容易看出当前服务端有哪些 业务类,业务类有哪些可供外界调用的方法。(·这里需要注意,保存方法时,必须连同方法的参数类型一起作为key,因为存在同名方法重载的情况·)当你运行Demo,启动服务端的时候,过滤一下日志,就能看到:

3 ,如果再发生 业务扩展的情况,我们只需要直接改动加了@ServiceId注解的业务类即可,并没有其他多余的动作。如果我在IUserBusiness接口中,增加一个logout方法,并且在实现类中去实现它。那么,再次启动服务端app,上图的日志中就会多出一个logout方法.

4,提供一个Map集合,专门用来保存每一个ServiceId对应的Object,并提供getObjectputObject方法,以便反射执行Method时所需。

OK,一切准备万全。业务类的每个部分基本上都保存到了服务端进程内存中,反射执行Method,随时可以取用。

2)自定义通信协议

跨进程通信,我们本质上还是使用Binder AIDL这一套,所以AIDL代码还是要写的,但是,是写在框架层中,一旦确定了通信协议,那这一套AIDL就不会随着业务的变动去改动它,因为它是框架层代码,不会随意去动。 要定自己的通信协议,其实没那么复杂。想一想,通信,无非就是客户端向服务端发送消息,并且取得回应的过程,那么,核心方法就确定为 send

入参是Request,返回值是Response,有没有觉得很像HTTP协议。request和response都是我们自定义的,注意,要参与跨进程通信的javaBean,必须实现Parcelable接口,它们的属性类型也必须实现Parcelable接口。


Request中的重要元素包括:serviceId 客户端告诉服务端要调用哪一个业务methodName 要调用哪一个方法parameters 调这个方法要传什么参数这3个元素,足矣涵盖客户端的任何行为。但是,由于我的业务实现类定义 为了单例,所以它有一个静态的getInstance方法。静态方法和普通方法的反射调用不太一样,所以,加上一个type属性,加以区分。


public class Request implements Parcelable {private int type;/**


  • 创建业务类实例,并且保存到注册表中/public final static int TYPE_CREATE_INSTANCE = 0;/*

  • 执行普通业务方法*/public final static int TYPE_BUSINESS_METHOD = 1;


public int getType() {return type;}private String serviceId; //客户端告诉服务端要调用哪一个业务 private String methodName;//要调用哪一个方法 private Parameter[] parameters;//调这个方法要传什么参数...省略无关代码}


Response中的重要元素有:result 字符串类型,用json字符串表示接口执行的结果isSuccesstrue,接口执行成功,false 执行失败


public class Response implements Parcelable {private String result;//结果 json 串 private boolean isSuccess;//是否成功}


最后,Request 引用的 Parameter 类:type 表示,参数类型(如果是 String 类型,那么这个值就是 java.long.String)value 表示,参数值,Gson 序列化之后得到的字符串


public class Parameter implements Parcelable {private String value;//参数值序列化之后的 jsonprivate String type;//参数类型 obj.getClass}


为什么设计这么一个 Parameter?为什么不直接使用 Object?因为,Request 中需要 客户端给的参数列表,可是如果直接使用客户端给的 Object[] ,你并不能保证数组中的所有参数都实现了Parcelable,一旦有没有实现的,通信就会失败(binder AIDL通信,所有参与通信的对象,都必须实现Parcelable,这是基础),所以,直接用gson将 Object[] 转化成 Parameter[],再传给 Request,是不错的选择,当需要反射执行的时候,再把 Parameter[] 反序列化成为 Object[] 即可。


OK,通信协议的 3 个类讲解完了,那么下一步应该是把这个协议使用起来。

3)bin
《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


der 连接封装参照Demo源码,这一个步骤中的两个核心类:IpcService , Channel


先说 IpcService.java

它就是一个 extends android.app.Service 的一个普通Service,它在服务端启动,然后与客户端发生通信。它必须在服务端appmanifest文件中注册。同时,当客户端与它连接成功时,它必须返回一个Binder对象,所以我们要做两件事:1 服务端的manifest中对它进行注册

ps: 这里肯定有人注意到了,上面service注册时,其实使用了多个IpcService的内部静态子类,设计多个内部子类的意义是,考虑到服务端存在个 业务接口的存在,让每一个业务接口的实现类 都由一个专门的IpcService服务区负责通信。举个例子:上图中存在两个 IpcService的子类,我让IpcService0 负责 用户业务UserBusiness,让IpcService1 负责 DownloadBusiness, 当 客户端需要使用UserBusiness时,就连接到IpcService0,当需要使用 DownloadBusiness时,就连接到IpcService1.但是这个并不是硬性规定,而只是良好的编程习惯,一个业务接口A,对应一个IpcService子类A,客户端要访问业务接口A,就直接和IpcService子类A通信即可。同理,一个业务接口B,对应一个IpcService子类B,客户端要访问业务接口B,就直接和IpcService子类B通信即可。(我是这么理解的,如有异议,欢迎留言)


2 重写onBind方法,返回一个 Binder 对象:我们要明确返回的这个 Binder 对象的作用是什么。它是给客户端去使用的,客户端用它来调用远程方法用的,所以,我们前面两个大步骤准备的 注册机Registry,和通信协议 request,response,就是在这里大显身手了 .


public IBinder onBind(Intent intent) {return new IIpcService.Stub() {//返回一个 binder 对象,让客户端可以 binder 对象来调用服务端的方法 @Overridepublic Response send(Request request) throws RemoteException {//当客户端调用了 send 之后//IPC 框架层应该要 反射执行服务端业务类的指定方法,并且视情况返回不同的回应//客户端会告诉框架,我要执行哪个类的哪个方法,我传什么参数 String serviceId = request.getServiceId();String methodName = request.getMethodName();Object[] paramObjs = restoreParams(request.getParameters());//所有准备就绪,可以开始反射调用了?//先获取 MethodMethod method = Registry.getInstance().findMethod(serviceId, methodName, paramObjs);switch (request.getType()) {case Request.TYPE_CREATE_INSTANCE:try {Object instance = method.invoke(null, paramObjs);Registry.getInstance().putObject(serviceId, instance);return new Response("业务类对象生成成功", true);} catch (Exception e) {e.printStackTrace();return new Response("业务类对象生成失败", false);}case Request.TYPE_BUSINESS_METHOD:Object o = Registry.getInstance().getObject(serviceId);if (o != null) {try {Log.d(TAG, "1:methodName:" + method.getName());for (int i = 0; i < paramObjs.length; i++) {Log.d(TAG, "1:paramObjs " + paramObjs[i]);}Object res = method.invoke(o, paramObjs);Log.d(TAG, "2");return new Response(gson.toJson(res), true);} catch (Exception e) {return new Response("业务方法执行失败" + e.getMessage(), false);}}Log.d(TAG, "3");break;}return null;}};}


这里有一些细节需要总结一下:1 从request中拿到的 参数列表是Parameter[]类型的,而我们反射执行某个方法,要的是Object[] ,那怎么办?反序列化咯,先前是用gson去序列化的,这里同样使用gson去反序列化, 我定义了一个名为:restoreParams的方法去反序列化成Object[].2 之前在request中,定义了一个type,用来区分静态的getInstance方法,和 普通的业务method,这里要根据request中的type值,区分对待。getInstance方法,会得到一个业务实现类的Object,我们利用RegistryputObject把它保存起来。 而,普通method,再从Registry中将刚才业务实现类的Object取出来,反射执行method3 静态getInstance的执行结果,不需要告知客户端,所以没有返回Response对象,而 普通Method,则有可能存在返回值,所以必须将返回值gson序列化之后,封装到Response中,return出去。


再来讲 Channel类:


之前抱怨过,不喜欢重复写 bindService,ServiceConnection,unbindService。但是其实还是要写的,写在IPC框架层,只写一次就够了。


public class Channel {String TAG = "ChannelTag";private static final Channel ourInstance = new Channel();


/**


  • 考虑到多重连接的情况,把获取到的 binder 对象保存到 map 中,每一个服务一个 binder*/private ConcurrentHashMap<Class<? extends IpcService>, IIpcService> binders = new ConcurrentHashMap<>();


public static Channel getInstance() {return ourInstance;}


private Channel() {}


/**


  • 考虑 app 内外的调用,因为外部的调用需要传入包名*/public void bind(Context context, String packageName, Class<? extends IpcService> service) {Intent intent;if (!TextUtils.isEmpty(packageName)) {intent = new Intent();Log.d(TAG, "bind:" + packageName + "-" + service.getName());intent.setClassName(packageName, service.getName());} else {intent = new Intent(context, service);}Log.d(TAG, "bind:" + service);context.bindService(intent, new IpcConnection(service), Context.BIND_AUTO_CREATE);}


private class IpcConnection implements ServiceConnection {


private final Class<? extends IpcService> mService;


public IpcConnection(Class<? extends IpcService> service) {this.mService = service;}


@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {IIpcService binder = IIpcService.Stub.asInterface(service);binders.put(mService, binder);//给不同的客户端进程预留不同的 binder 对象 Log.d(TAG, "onServiceConnected:" + mService + ";bindersSize=" + binders.size());}


@Overridepublic void onServiceDisconnected(ComponentName name) {binders.remove(mService);Log.d(TAG, "onServiceDisconnected:" + mService + ";bindersSize=" + binders.size());}


}


public Response send(int type, Class<? extends IpcService> service, String serviceId, String methodName, Object[] params) {Response response;Request request = new Request(type, serviceId, methodName, makeParams(params));Log.d(TAG, ";bindersSize=" + binders.size());IIpcService iIpcService = binders.get(service);try {response = iIpcService.send(request);Log.d(TAG, "1 " + response.isSuccess() + "-" + response.getResult());} catch (RemoteException e) {e.printStackTrace();response = new Response(null, false);Log.d(TAG, "2");} catch (NullPointerException e) {response = new Response("没有找到 binder", false);Log.d(TAG, "3");}return response;}...省略不关键代码}


上面的代码是 Channel 类代码,两个关键:1 bindService+ServiceConnection 供客户端调用,绑定服务,并且将连接成功之后的 binder 保存起来

2 提供一个send方法,传入request,且 返回response,使用serviceId对应的binder 完成通信。

4)动态代理实现RPC

终于到了最后一步,前面 3 个步骤,为进程间通信做好了所有的准备工作,只差最后一步了------ 客户端调用服务。重申一下 RPC 的定义:让客户端像 使用本地方法一样 调用远程过程


像 使用本地方法一样?我们平时是怎么使用本地方法的呢?


A a = new A();a.xxx();

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
手把手讲解IPC框架,成为一名合格Android架构师