Author: Enjoy learning class lifelong VIP week

Reprint please state the source!

In the last session of “IPC Framework by Hand”, students shared the concept of QA and pre-skills, the comparison between traditional IPC communication writing method and RPC communication using IPC framework, and the Demo. In this installment, he will continue to share the IPC framework with you.

The body of the outline

Concept QA and pre-production skills

2. The comparison between traditional IPC communication writing method and RPC communication using IPC framework

Third, Demo demonstration

Iv. Explanation of the framework’s core ideas

Five, write the last words

To follow:Star Student works: Hand by hand IPC Framework (1)

Iv. Explanation of the framework’s core ideas

There are two things that are really gross when we don’t use the IPC framework:

1. As our business grew, we had to change the AIDL file frequently (because of the new business interface), and AIDL changed without any code cues. Only after compilation did the compiler tell me what was wrong, and directly referenced Javabeans had to be declared again manually. I really don’t want to waste my time on this.

2. All client activities that want to communicate with the binder process must manually bindService, then handle the binder connection, rewrite the ServiceConnection, and release the connection when appropriate. Write as little code as possible that is irrelevant and repetitive.

The IPC framework will focus on these two issues. Let’s start with the core design ideas

Note: 1. Building a framework will involve a wide range of knowledge, I can not explain every detail in detail, some basic parts of a brush, if you have any questions, please leave a comment for discussion.

2. Design ideas are interlinked, and it is best to read from top to bottom.

Four tetralogy of framework ideas:

1) Business registration

As mentioned above, using AIDL communication directly, we need to make changes to AIDL files as our business expands, which can be laborious and error-prone. How to do? By means of business registration, the class object of the business class is saved in the server memory. Enter the Demo code registry.java:

public class Ipc { /** * @param business */ public static void register(Class<? > business) {// Register is a separate process, so extract it separately and put it in a class to registry.getinstance ().register(business); // Register is a singleton, start the server, // there is a register object, unique, will not be affected by the unbinding of the service}... Omit extraneous code}Copy the code
/** * public class Registry {... /** * private ConcurrentHashMap<String, Class<? >> mBusinessMap = new ConcurrentHashMap<>(); /** * Service method table, 2d map, key is serviceId string value, value is a method map-key, method name; value */ private ConcurrentHashMap<String, ConcurrentHashMap<String, Method>> mMethodMap = new ConcurrentHashMap<>(); /** * an instance of the business class is required to reflect execution methods, if not static methods. Private ConcurrentHashMap<String, Object> mObjectMap = new ConcurrentHashMap<>(); private ConcurrentHashMap<String, Object> mObjectMap = new ConcurrentHashMap<>(); /** * public void register(class <? ServiceId = business.getannotation (serviceid.class); ServiceId = business.getannotation (serviceid.class);  // Get the annotation on that class headerif (serviceId == null) {
			throw  new  RuntimeException("Business classes must use ServiceId annotations"); } String value = serviceId.value(); mBusinessMap.put(value, business); // Use the value as the key of the business class object. ConcurrentHashMap<String, method > tempMethodMap = mMethodMap.get(value); ConcurrentHashMap<String, method > tempMethodMap = mMethodMap.get(value); // See if the method table already exists for the entire businessif(tempMethodMap == null) { tempMethodMap = new ConcurrentHashMap<>(); // New mmethodMap. put(value, tempMethodMap); // And store it in}for(Method method : business.getMethods()) { String methodName = method.getName(); Class<? >[] parameterTypes = method.getParameterTypes(); String methodMapKey = getMethodMapKeyWithClzArr(methodName, parameterTypes); tempMethodMap.put(methodMapKey, method); }... Omit non-critical code}... /** * How to find a Method? See the build procedure above, * * @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); // Build a StringBuilder in the same wayreturnmap.get(methodMapKey); } /** * Add an instance ** @param serviceId * @param object */ public void putObject(String serviceId, Object object) { mObjectMap.put(serviceId, object); } @param serviceId */ public Object getObject(String serviceId) {returnmObjectMap.get(serviceId); }}Copy the code
/** * custom annotations, */ @target (elementtype.type) @Retention(retentionPolicy.runtime) public @interface ServiceId {String value(); }Copy the code

Using a singleton Registry class, I split the current business class object, each Method, into a map collection. These classes, methods, are saved in preparation for reflection to execute the specified business Method. Here are a few clever designs:

1. Use the custom @ServiceId annotation to constrain both the business interface and the implementation class, so that the implementation class has a unique constraint, because in Registry class, A ServiceId is only for one type of business, and if you use the Registry class to register a business class without the @ServiceID annotation, an exception will be thrown.

2. Save all the classes of the service implementation Class and all the public methods of the Class into the map set by using the @serviceId annotation value as the key. What methods a business class has that can be called from outside. · When you run the Demo and start the server, filter the log and you can see:

3. If you need to extend the service, you just need to modify the @ServiceId annotated service class. If I add a logout method in the IUserBusiness interface and implement it in the implementation class. If you start the server app again, the logout method will appear in the log above.

4. Provide a Map collection for each ServiceId Object, and provide getObject and putObject methods to reflect what is needed when executing Method.

OK, everything is ready. Basically, every part of the business class is stored in the server process’s memory, reflecting Method, which is available at any time.

2) Custom communication protocol

For cross-process communication, we’re essentially using BinderAIDL, so AIDL code is still written, but it’s written in the framework layer, and once the protocol is established, the AIDL doesn’t change it as the business changes, because it’s framework code. It’s not that complicated to determine your own protocol. When a client sends a message to a server and receives a response, the core method is send:

The input parameter is Request and the return value is Response. Request and Response are both custom. Note that javabeans that participate in cross-process communication must implement the Parcelable interface, as must their attribute types.

The important elements in the Request include: serviceId The client tells the server which business to call. MethodName Which method to call. Parameters which method to call. However, because my business implementation class is defined as a singleton, it has a static getInstance method. Static and normal methods have different reflection calls, so a type attribute is added to distinguish them.

public  class  Request  implements  Parcelable {
	private  int type; Public final static int TYPE_CREATE_INSTANCE = 0; public final static int TYPE_CREATE_INSTANCE = 0; Public final static int TYPE_BUSINESS_METHOD = 1; public final static int TYPE_BUSINESS_METHOD = 1; public intgetType() {
		return type; } private String serviceId; // The client tells the server which business to call. Private Parameter[] parameters; // What arguments to pass to this method... Omit extraneous code}Copy the code

The important elements in Response are as follows: Result string type, using JSON string to represent the result of interface execution isSuccess is true, interface execution succeeds, false interface execution fails.

public class Response implements Parcelable { private String result; // result json string private Boolean isSuccess; }Copy the code

Finally, the Parameter class referenced by Request: type represents the Parameter type (java.long.String if it is a String) value represents the Parameter value, which Gson serialized as a String.

public class Parameter implements Parcelable { private String value; // Serialized JSON private Stringtype; // Parameter type obj.getClass}Copy the code

Why is such a Parameter designed? Why not just use Object? If you use Object[], you are not sure that all the parameters in the array are Parcelable. If you do not have any, communication will fail (Binder AIDL communicates with all the objects, Object[] to Parameter[], which is passed to Request, is a good choice. When reflection is needed, Deserialize Parameter[] to Object[].

OK, the three classes of the communication protocol are explained, so the next step should be to use the protocol.

3) Binder connection package

Refer to the Demo source code for the two core classes in this step: IpcService, Channel

Say firstIpcService.java

It is a extendsandroid. App. The Service of a common Service, start it on the server, and then communicate with the client. It must be registered in the manifest file of the server app. Also, it must return a Binder object when the client connects to it successfully, so we need to do two things:

1. The servermanifestTo register it

ps: IpcService (IpcService) {IpcService (IpcService) {IpcService (IpcService) {IpcService (IpcService) {IpcService (IpcService) {IpcService (IpcService) {IpcService (IpcService) {IpcService Here’s an example: In the figure above, there are two subclasses of IpcService. I make IpcService0 responsible for UserBusiness and IpcService1 responsible for DownloadBusiness. When the client needs to use UserBusiness, Connect to IpcService0 and IpcService1 when DownloadBusiness is needed. A service interface A corresponds to A subclass of IpcService A. If the client wants to access the service interface A, it can directly communicate with the subclass of IpcService A. Similarly, a service interface B corresponds to subclass B of IpcService. To access service interface B, the client communicates with subclass B of IpcService directly. (This is my understanding, if you have any objection, please leave a message)

Bind bind bind bind bind bind bind bind bind bind bind bind bind bind bind It is for the client to use, the client to call remote methods with it, so, we first two big steps to prepare the Registry, and communication protocol request,response, is here to show their skills.

public IBinder onBind(Intent intent) {
	return new IIpcService.Stub() {return a binder object that clients can use to call server methods @override Public Response Send (Request Request) throws RemoteException { // When the client calls send // the IPC framework layer should reflect the specified method that executes the server's business class and return a different response depending on the situation // The client will tell the framework that I want to execute which method of which class, String serviceId = request.getServiceId(); String serviceId = request.getServiceId(); String methodName = request.getMethodName(); Object[] paramObjs = restoreParams(request.getParameters()); // Are you ready to start reflection calls? 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("Business class object generated successfully".true);
				}
				catch (Exception e) {
					e.printStackTrace();
					return  new  Response("Business class object generation failed".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("Business method execution failed" + e.getMessage(), false);
					}
				}
				Log.d(TAG, "3");
				break;
			}
			returnnull; }}; }Copy the code

Here are a few details to summarize: 1. The Parameter list from request is Parameter[], and we want an Object[] to execute a method. RestoreParams method to deserialize Object[]

2. Request defines a type that distinguishes static getInstance methods from ordinary business methods based on the value of type in request. The getInstance method returns an Object of the business implementation class, which we store with Registry’s putObject. The normal method, on the other hand, reflects the method by fetching the Object of the business implementation class from Registry

3. The execution result of static getInstance does not need to be notified to the client, so no Response object is returned. However, ordinary Method may have a return value, so the return value must be serialized gson, encapsulated in Response, and returned.

Again,ChannelClass:

I don’t like to write bindService, ServiceConnection, unbindService repeatedly. But you have to write it anyway, in the IPC framework layer, and you only have to write it once.

public  class  Channel {
	String TAG = "ChannelTag"; private static final Channel ourInstance = new Channel(); Private ConcurrentHashMap<Class<? Private ConcurrentHashMap<Class<? extends IpcService>, IIpcService> binders = new ConcurrentHashMap<>(); public static ChannelgetInstance() {
		return ourInstance;
	}
	private  Channel() {} /** * consider calls inside and outside app, since external calls need to pass in package names */ public voidbind(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); // Reserve different binder objects for different client processes 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("No Binder found.".false);
			Log.d(TAG, "3");
		}
		returnresponse; }... Omit non-critical code}Copy the code

The above code is the Channel class code, with two key points:

BindService +ServiceConnection is called by the client, binds the service, and saves the binder after successful connection

2. Provide a send method that sends a Request and returns a response using the Binder corresponding to serviceId.

4) Dynamic proxy implements RPC

Finally, the last step, the first three, has done all the work for interprocess communication, except for the last step —— client invoke service. To reiterate the definition of RPC, let a client call a remote procedure as if it were a local method.

Like using local methods? How do we normally use local methods?

A a = new A();
a.xxx();
Copy the code

Something like that. However, the client and the server are two separate processes, and the memory cannot be shared. That is to say, the class objects existing on the server cannot be directly used by the client. Generics + dynamic proxies! We need to build a business proxy class object in the client process, which can perform the same methods as the business class on the server, but it is not the same object on the server process. How can we achieve this effect?

public class Ipc { ... /** * @param service * @param classType * @param getInstanceMethodName * @param params * @param <T> Generic, * @return*/ public static <T> T getInstanceWithName(Class<? extends IpcService> service, Class<T> classType, String getInstanceMethodName, Object... // Bind with binder to call remote methods, create business class objects on the server and store themif(! classType.isInterface()) { throw new RuntimeException("The getInstanceWithName method here must pass the class of the interface.");
		}
		ServiceId serviceId = classType.getAnnotation(ServiceId.class);
		if (serviceId == null) {
			throw  new  RuntimeException("Interface does not use the specified ServiceId annotation");
		}
		Response response = Channel.getInstance().send(Request.TYPE_CREATE_INSTANCE, service, serviceId.value(), getInstanceMethodName, params);
		if(Response.isSuccess ()) {// If the server business class object is successfully created, then we build a proxy object to implement RPCreturn (T) Proxy.newProxyInstance(
			classType.getClassLoader(), new  Class[]{
				classType
			}
			,
			new  IpcInvocationHandler(service, serviceId.value()));
		}
		returnnull; }}Copy the code

GetInstanceWithName, above, returns a dynamic proxy business class object (in the client process) that behaves exactly like the real business class (in the server process). This method takes 4 parameters which remote service @ParamService accesses, because different services return different binders which business class @ParamCLASstype accesses. Note that the business class here is completely defined by the client. The package name does not have to be the same as the server, but it must have an annotation that is the same as the server class. If the annotations are the same, the framework thinks you are accessing the same business. @ paramgetInstanceMethodName our business classes are designed to singleton, but not all of the methods for singleton is called getInstance, our framework to allow other method name @ paramparams parameter list, type of the Object [].

Above all, implement the last step of RPC, as shown below:

If the singleton Object on the server side is successfully created, it indicates that there is an Object of the business implementation class in the registry of the server side. Then, I can use this Object to execute the business method I want through binder communication, and get the method return value, and finally deserialize the return value into Object. The result of the execution of a method as a dynamic proxy business class.

Key code IpcInvocationHandler:

/** * 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 {} @override public Object invoke(Object proxy, Method Method, Object[] args) throws Throwable Execute the real procedure // and your real procedure is the remote communication log.d ("IpcInvocationHandler"."Class." + serviceId + "Method name" + method.getName());
		for (int i = 0; i < args.length; i++) {
			Log.d("IpcInvocationHandler"."Parameter:" + args.getClass().getName() + "/" + args[i].toString());
		}
		Response response = Channel.getInstance().send(Request.TYPE_BUSINESS_METHOD, service, serviceId, method.getName(), args);
		if(response.issuccess ()) {// If the method executed at this time returns a value Class<? >returnType = method.getReturnType();
			if (returnType ! = void.class &&returnType ! Void. Class) {// If there is a return value, we must deserialize the serialized return value into String resStr = response.getresult ();return gson.fromJson(resStr, returnType); }}returnnull; }}Copy the code

Ok, to sum up before finishing, the final implementation of RPC uses Proxy dynamic Proxy + Binder communication. A dynamic proxy is used to generate an object in this process, and when the Invoke is overridden, the binder communication is used to perform server-side procedures to retrieve the return value. The design is really exquisite.

Five, write the last words

  1. This case provides two demos, are only as a demonstration effect, the code is not delicate, please do not care about these details.

  2. This framework is not my original, the subject content is from Lance teacher, xiangxue class, this article is only for learning and exchange, please be sure to note the source, thank you for your cooperation.

  3. The second Demo (IPC communication framework implements RPC), my original code implements the service side one service, 2 client calls at the same time, but the framework is to support the service side multiple services, multiple clients at the same time call, so try in my code based on the extension of the service side N business interface and implementation classes, A scenario where multiple clients mix calls. There should be no bugs.

  4. Readers are advised to try to extend the server side and client side of the code, because this can be the most intuitive sense of the framework to our development of convenience.

conclusion

There is more to life than mere existence… But also learn to think with the big picture….

Frame thinking, if we can understand and even create our own frame, then we have moved beyond the vulgar and on to the higher level. However, there is a long way to go. I watched an article or a video of a master yesterday, and I felt that I had learned some dry stuff. If I want to absorb knowledge for my own use, I can’t really store it as dry stuff. I need to find some water and swallow the dry stuff, so that I can digest and absorb it.

If you like, please click a like, follow me, and share more technical dry goods ~