赞
踩
作者:享学课堂终身VIP周周
简书ID:波澜步惊
转载请声明出处!
上一期《手把手讲解IPC框架》享学课堂周周同学分享了概念QA以及前置技能、传统方式IPC通信写法与使用IPC框架进行RPC通信的对比以及Demo展示三个部分。这一期他将继续为大家带来手把手讲解IPC框架分享。
一、概念QA以及前置技能
二、传统方式IPC通信写法与使用IPC框架进行RPC通信的对比
三、Demo展示
四、框架核心思想讲解
五、写在最后的话
接上一篇:明星学员作品:手把手讲解IPC框架(1)
我们不使用IPC框架时,有两件事非常恶心:
1. 随着业务的扩展,我们需要频繁(因为要新增业务接口)改动
AIDL
文件,而且AIDL
修改起来没有任何代码提示,只有到了编译之后,编译器才会告诉我哪里错了,而且 直接引用到的JavaBean
还必须手动再声明一次。实在是不想在这个上面浪费时间。2. 所有客户端
Activity
,只要想进行进程间binder
通信,就不可避免要去手动bindService
,随后去处理Binder
连接,重写ServiceConnection
,还要在适当的时候释放连接,这种业务不相关而且重复性很大的代码,要尽量少写。
IPC框架将会着重解决这两个问题。下面开始讲解核心设计思想
注:1.搭建框架牵涉的知识面会很广,我不能每个细节都讲得很细致,一些基础部分一笔带过的,如有疑问,希望能留言讨论。
2.设计思路都是环环相扣的,阅读时最好是从上往下依次理解.
上文说到,直接使用
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<>(); //不存在,则new mMethodMap.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); //同样的方式,构建一个StringBuilder return 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
集合中。而,保存这些Clas
s,Method
,则是为了 反射执行指定的业务Method
做准备。此处有几个精妙设计:1、利用自定义注解
@ServiceId
对业务接口和实现类,都形成约束,这样业务实现类就有了进行唯一性约束,因为在Registry
类中,一个ServiceId
只针对一种业务,如果用Registry
类注册一个没有@ServiceId
注解的业务类,就会抛出异常。2、利用注解
@ServiceId
的value
作为key
,保存所有的业务实现类的Class
, 以及该Class
的所有public
的Method
到map
集合中,通过日志打印,很容易看出当前服务端有哪些 业务类,业务类有哪些可供外界调用的方法。(·这里需要注意,保存方法时,必须连同方法的参数类型一起作为key
,因为存在同名方法重载的情况·) 当你运行Demo
,启动服务端的时候,过滤一下日志,就能看到:3 、如果再发生业务扩展的情况,我们只需要直接改动加了
@ServiceId
注解的业务类即可,并没有其他多余的动作。如果我在IUserBusiness
接口中,增加一个logout
方法,并且在实现类中去实现它。那么,再次启动服务端
app,上图的日志中就会多出一个logout
方法.4、提供一个
Map
集合,专门用来保存每一个ServiceId
对应的Object
,并提供getObject
和putObject
方法,以便反射执行Method
时所需。OK,一切准备万全。业务类的每个部分基本上都保存到了服务端进程的内存中,反射执行
Method
,随时可以取用。
跨进程通信,我们本质上还是使用
BinderAIDL
这一套,所以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
字符串表示接口执行的结果isSuccess
为true
,接口执行成功,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;
//参数值序列化之后的json
private String type;
//参数类型 obj.getClass
}
为什么设计这么一个Parameter?为什么不直接使用Object?因为,Request 中需要客户端给的参数列表,可是如果直接使用客户端给的Object[] ,你并不能保证数组中的所有参数都实现了 Parcelable
,一旦有没有实现的,通信就会失败( binder
AIDL
通信,所有参与通信的对象,都必须实现 Parcelable
,这是基础),所以,直接用 gson
将Object[] 转化成Parameter[],再传给Request,是不错的选择,当需要反射执行的时候,再把Parameter[] 反序列化成为 Object[] 即可。
OK,通信协议的3个类讲解完了,那么下一步应该是把这个协议使用起来。
参照 Demo
源码,这一个步骤中的两个核心类:IpcService
, Channel
先说
IpcService.java
它就是一个
extendsandroid.app.Service
的一个普通Service
,它在服务端启动,然后与客户端发生通信。它必须在服务端app
的manifest
文件中注册。同时,当客户端与它连接成功时,它必须返回一个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对象来调用服务端的方法 @Override public Response send(Request request) throws RemoteException { //当客户端调用了send之后 //IPC框架层应该要 反射执行服务端业务类的指定方法,并且视情况返回不同的回应 //客户端会告诉框架,我要执行哪个类的哪个方法,我传什么参数 String serviceId = request.getServiceId(); String methodName = request.getMethodName(); Object[] paramObjs = restoreParams(request.getParameters()); //所有准备就绪,可以开始反射调用了? //先获取Method Method 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
,我们利用Registry
的putObject
把它保存起来。而,普通method
,再从Registry
中将刚才业务实现类的Object
取出来,反射执行method
3、静态
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; } @Override public 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()); } @Override public 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
完成通信。
终于到了最后一步,前面3个步骤,为进程间通信做好了所有的准备工作,只差最后一步了------ 客户端调用服务。重申一下RPC的定义:让客户端像使用本地方法一样调用远程过程。
像 使用本地方法一样?我们平时是怎么使用本地方法的呢?
A a = new A();
a.xxx();
类似上面这样。但是我们的客户端和服务端是两个隔离的进程,内存并不能共享,也就是说服务端存在的类对象,不能直接被客户端使用,那怎么办?泛型+动态代理!我们需要构建一个处在客户端进程内的业务代理类对象,它可以执行和服务端的业务类一样的方法,但是它确实不是服务端进程的那个对象,如何实现这种效果?
public class Ipc { ...省略无关代码 /** * @param service * @param classType * @param getInstanceMethodName * @param params * @param <T> 泛型, * @return */ public static <T> T getInstanceWithName(Class<? extends IpcService> service, Class<T> classType, String getInstanceMethodName, Object... params) { //这里之前不是创建了一个binder么,用binder去调用远程方法,在服务端创建业务类对象并保存起来 if (!classType.isInterface()) { throw new RuntimeException("getInstanceWithName方法 此处必须传接口的class"); } ServiceId serviceId = classType.getAnnotation(ServiceId.class); if (serviceId == null) { throw new RuntimeException("接口没有使用指定ServiceId注解"); } Response response = Channel.getInstance().send(Request.TYPE_CREATE_INSTANCE, service, serviceId.value(), getInstanceMethodName, params); if (response.isSuccess()) { //如果服务端的业务类对象创建成功,那么我们就构建一个代理对象,实现RPC return (T) Proxy.newProxyInstance( classType.getClassLoader(), new Class[]{ classType } , new IpcInvocationHandler(service, serviceId.value())); } return null; } }
上面的 getInstanceWithName
,会返回一个动态代理的 业务类对象(处在客户端进程), 它的行为 和 真正的业务类(服务端进程)一模一样。这个方法有 4
个参数 @paramservice
要访问哪一个远程service,因为不同的service会返回不同的Binder @paramclassType
要访问哪一个业务类,注意,这里的业务类完全是客户端自己定义的,包名不必和服务端一样,但是一定要有一个和服务端对应类一样的注解。注解相同,框架就会认为你在访问相同的业务。@paramgetInstanceMethodName
我们的业务类都是设计成单例的,但是并不是所有获取单例对象的方法都叫做getInstance,我们框架要允许其他的方法名 @paramparams
参数列表,类型为 Object[]
。
重中之重,实现RPC的最后一个步骤,如图:
如果服务端的单例对象创建成功,那么说明 服务端的注册表中已经存在了一个业务实现类的对象,进而,我可以通过binder通信来 使用这个对象 执行我要的业务方法,并且拿到方法返回值,最后 把返回值反序列化成为
Object
,作为动态代理业务类的方法的执行结果。
关键代码 IpcInvocationHandler
:
/** * RPC调用 执行远程过程的回调 */ public class IpcInvocationHandler implements InvocationHandler { private Class<? extends IpcService> service; private String serviceId; private static Gson gson = new Gson(); IpcInvocationHandler(Class<? extends IpcService> service, String serviceId) { this.service = service; this.serviceId = serviceId; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //当,调用代理接口的方法时,就会执行到这里,执行真正的过程 //而你真正的过程是远程通信 Log.d("IpcInvocationHandler", "类:" + serviceId + " 方法名" + method.getName()); for (int i = 0; i < args.length; i++) { Log.d("IpcInvocationHandler", "参数:" + args.getClass().getName() + "/" + args[i].toString()); } Response response = Channel.getInstance().send(Request.TYPE_BUSINESS_METHOD, service, serviceId, method.getName(), args); if (response.isSuccess()) { //如果此时执行的方法有返回值 Class<?> returnType = method.getReturnType(); if (returnType != void.class && returnType != Void.class) { //既然有返回值,那就必须将序列化的返回值 反序列化成对象 String resStr = response.getResult(); return gson.fromJson(resStr, returnType); } } return null; } }
ok,收工之前总结一下,最后 RPC
的实现,借助了 Proxy
动态代理+ Binder
通信。用动态代理产生一个本进程中的对象,然后在重写 invoke
时,使用 binder
通信执行服务端过程拿到返回值。这个设计确实精妙。
本案例提供的两个Demo,都只是作为演示效果作用的,代码不够精致,请各位不要在意这些细节。
此框架并非本人原创,课题内容来自享学课堂Lance老师,本文只做学习交流之用,转载请务必注明出处,谢谢合作。
第二个Demo(IPC通信框架实现RPC),我的原装代码中只实现了服务端1个服务,2个客户端同时调用,但是这个框架是支持服务端多个服务,多个客户端同时调用的,所以,可以尝试在我的代码基础上扩展出服务端N个业务接口和实现类,多个客户端混合调用的场景。应该不会有bug。
建议读者去尝试扩展一下服务端和客户端的代码,因为这样可以最直观地感受到框架的给我们开发带来的便利。
生活不止眼前的苟且…还要学会用大局观思考…
框架思想,如果我们能够理解,甚至创造自己的框架,那么我们就已经脱离了低级趣味,在走向进阶了。然而,进阶之路漫漫长。我昨天看了高手的一篇文章,或者一个视频,感觉学了点干货,那我想要吸收知识为己所用,就不能真的把知识当成干货储存起来,我要想办法找点水把干货咽下去,消化吸收,才是我自己的东西。
无论是哪家公司,都很重视基础,大厂更加重视技术的深度和广度,面试是一个双向选择的过程,不要抱着畏惧的心态去面试,不利于自己的发挥。同时看中的应该不止薪资,还要看你是不是真的喜欢这家公司,是不是能真的得到锻炼。
针对以上面试技术点,我在这里也做一些资料分享,希望能更好的帮助到大家。
框架思想,如果我们能够理解,甚至创造自己的框架,那么我们就已经脱离了低级趣味,在走向进阶了。然而,进阶之路漫漫长。我昨天看了高手的一篇文章,或者一个视频,感觉学了点干货,那我想要吸收知识为己所用,就不能真的把知识当成干货储存起来,我要想办法找点水把干货咽下去,消化吸收,才是我自己的东西。
无论是哪家公司,都很重视基础,大厂更加重视技术的深度和广度,面试是一个双向选择的过程,不要抱着畏惧的心态去面试,不利于自己的发挥。同时看中的应该不止薪资,还要看你是不是真的喜欢这家公司,是不是能真的得到锻炼。
针对以上面试技术点,我在这里也做一些资料分享,希望能更好的帮助到大家。
[外链图片转存中…(img-RAd3GmW3-1623556590819)]
[外链图片转存中…(img-QwMUfnKl-1623556590820)]
[外链图片转存中…(img-KhXTLl7q-1623556590821)]
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。