Android程序员进阶必知架构源码:手把手讲解IPC框架(2)

做者:享学课堂终身VIP周周java

转载请声明出处!android

上一期《手把手讲解IPC框架》享学课堂周周同窗分享了概念QA以及前置技能、传统方式IPC通讯写法与使用IPC框架进行RPC通讯的对比以及Demo展现三个部分。这一期他将继续为你们带来手把手讲解IPC框架分享。编程

正文大纲

1、概念QA以及前置技能json

2、传统方式IPC通讯写法使用IPC框架进行RPC通讯的对比数组

3、Demo展现bash

4、框架核心思想讲解app

5、写在最后的话框架

接上一篇:明星学员做品:手把手讲解IPC框架(1)ide

4、框架核心思想讲解

咱们不使用IPC框架时,有两件事很是恶心:post

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<>();
			//不存在,则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集合中。而,保存这些 Class, Method,则是为了 反射执行指定的业务 Method作准备。此处有几个精妙设计:

一、利用自定义注解 @ServiceId 对业务接口和实现类,都造成约束,这样业务实现类就有了进行惟一性约束,由于在 Registry类中,一个 ServiceId只针对一种业务,若是用 Registry类注册一个没有 @ServiceId注解的业务类,就会抛出异常。

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

3 、若是再发生业务扩展的状况,咱们只须要直接改动加了 @ServiceId注解的业务类便可,并无其余多余的动做。若是我在 IUserBusiness接口中,增长一个 logout方法,而且在实现类中去实现它。那么,再次启动 服务端app,上图的日志中就会多出一个 logout方法.

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

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

2)自定义通讯协议

跨进程通讯,咱们本质上仍是使用 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字符串表示接口执行的结果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;
	//参数值序列化以后的json
	private String type;
	//参数类型 obj.getClass
}
复制代码

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

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

3)binder链接封装

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

先说 IpcService.java

它就是一个 extendsandroid.app.Service 的一个普通 Service,它在服务端启动,而后与客户端发生通讯。它必须在服务端 appmanifest文件中注册。同时,当客户端与它链接成功时,它必须返回一个 Binder对象,因此咱们要作两件事:

一、服务端的 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通讯便可。(我是这么理解的,若有异议,欢迎留言)

二、重写 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;
		}
	}
	;
}
复制代码

这里有一些细节须要总结一下: 一、从 request中拿到的 参数列表是 Parameter[]类型的,而咱们反射执行某个方法,要的是 Object[],那怎么办?反序列化咯,先前是用 gson去序列化的,这里一样使用 gson去反序列化, 我定义了一个名为:restoreParams的方法去反序列化成 Object[].

二、以前在 request中,定义了一个 type,用来区分静态的 getInstance方法,和 普通的业务 method,这里要根据 request中的 type值,区分对待。getInstance方法,会获得一个业务实现类的 Object,咱们利用 RegistryputObject把它保存起来。而,普通 method,再从 Registry中将刚才业务实现类的 Object取出来,反射执行 method

三、静态 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类代码,两个关键:

一、 bindService+ServiceConnection 供客户端调用,绑定服务,而且将链接成功以后的binder保存起来

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

4)动态代理实现 RPC

终于到了最后一步,前面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通讯执行服务端过程拿到返回值。这个设计确实精妙。

5、 写在最后的话

  1. 本案例提供的两个Demo,都只是做为演示效果做用的,代码不够精致,请各位不要在乎这些细节。

  2. 此框架并不是本人原创,课题内容来自享学课堂Lance老师,本文只作学习交流之用,转载请务必注明出处,谢谢合做。

  3. 第二个DemoIPC通讯框架实现RPC),个人原装代码中只实现了服务端1个服务2个客户端同时调用,可是这个框架是支持服务端多个服务多个客户端同时调用的,因此,能够尝试在个人代码基础上扩展出服务端N个业务接口实现类多个客户端混合调用的场景。应该不会有bug。

  4. 建议读者去尝试扩展一下服务端和客户端的代码,由于这样能够最直观地感觉到框架的给咱们开发带来的便利。

结语

生活不止眼前的苟且...还要学会用大局观思考....

框架思想,若是咱们可以理解,甚至创造本身的框架,那么咱们就已经脱离了低级趣味,在走向进阶了。然而,进阶之路漫漫长。我昨天看了高手的一篇文章,或者一个视频,感受学了点干货,那我想要吸取知识为己所用,就不能真的把知识当成干货储存起来,我要想办法找点水把干货咽下去,消化吸取,才是我本身的东西。

喜欢的话记得点个赞,关注我,还有更多技术干货分享~

相关文章
相关标签/搜索