Hello, all the great programmers, happy New Year to you in advance. I wish you a fruitful, profitable and auspicious New Year of the Pig.

ARetrofit has been open source for about half a year without any promotion or introduction. Today, it has 160+ stars. Thank you very much for your support. I also wanted to write something about this framework before the end of the year. Both ARetrofit users and those interested in the source code will find this article useful.

Introduction to the

ARetrofit is a framework for communication between Android components that decouple and communicate with each other.

Source link

Welcome star, issues, fork

componentization

Android componentization is not a new concept, it has been around for a long time, you can Google it and see a bunch of articles about it.

Modules in Android Studio follow the principle of high cohesion, and ARetrofit implements uncoupled code structure, as shown below:


Component structure.png

Each Module can be run individually as a project, and communication between modules when packaged as a whole is done through ARetrofit.

ARetrofit principle

Before I get into the rationale, I want to talk about why ARetrofit is called. The idea behind ARetrofit came from Retrofit, a framework developed by Square for Android web requests that we won’t cover here. The main thing is that the Retrofit framework uses a lot of design patterns. Retrofit is an open source project that takes Java’s design patterns to the extreme, and ultimately provides a very concise API. Such a simple API makes the implementation of the network module in our APP very easy, and it is also very comfortable to maintain. Therefore, I felt the need to make communication between Android components easy, so that users can communicate elegantly with a simple API and, more importantly, maintain it comfortably.

The ARetrofit rationale can be simplified as follows:




Schematic diagram. PNG

  1. Use annotations to declare activities/fragments or classes that need to communicate
  2. Each module generates an implementation class for RouteInject and an implementation class for AInterceptorInject at compile time through the annotationProcessor. This step will output logs when executing app[build], as can be seen intuitively, as shown in the figure below:
AInjecton::Compiler >>> Apt Interceptor Processor start... AInjecton::Compiler value = null AInjecton::Compiler value = 3 AInjecton::Compiler auto generate class = com? sjtu? yifei? eCGVmTMvXG? AInterceptorInject Note: AInjecton::Compiler add path= 3 and class= LoginInterceptor.... AInjecton::Compiler >>> Apt Route Processor start... < < < note: AInjecton: : the Compiler enclosindClass = null note: AInjecton: : the Compiler value = / login module/ILoginProviderImpl note: AInjecton::Compiler enclosindClass = null Note: AInjecton::Compiler value = /login-module/LoginActivity AInjecton::Compiler enclosindClass = null AInjecton::Compiler value = /login-module/Test2Activity AInjecton::Compiler enclosindClass = null AInjecton::Compiler value = /login-module/TestFragment AInjecton::Compiler auto generate class = com? sjtu? yifei? VWpdxWEuUx? RouteInject note: AInjecton::Compiler add path= /login-module/TestFragment and class= null Note: AInjecton::Compiler add path= /login-module/LoginActivity and class= null note: AInjecton::Compiler add path= /login-module/Test2Activity and class= null note: AInjecton: : the Compiler add path = / login module/ILoginProviderImpl and class = null note: AInjecton::Compiler >>> Apt route Processor succeed <<<Copy the code
  1. Inject the compile-time class that maintains the routing table and interceptors into the RouterRegister. The corresponding [build] log is as follows:
TransformPluginLaunch >>> ========== Transform scan start ===========
TransformPluginLaunch >>> ========== Transform scan end cost 0.238 secs and start inserting ===========
TransformPluginLaunch >>> Inserting code to jar >> /Users/yifei/as_workspace/ARetrofit/app/build/intermediates/transforms/TransformPluginLaunch/release/8.jar
TransformPluginLaunch >>> to class >> com/sjtu/yifei/route/RouteRegister.class
InjectClassVisitor >>> inject to class:
InjectClassVisitor >>> com/sjtu/yifei/route/RouteRegister{
InjectClassVisitor >>>        public *** init() {
InjectClassVisitor >>>            register("com.sjtu.yifei.FBQWNfbTpY.com?sjtu?yifei?FBQWNfbTpY?RouteInject")
InjectClassVisitor >>>            register("com.sjtu.yifei.klBxerzbYV.com?sjtu?yifei?klBxerzbYV?RouteInject")
InjectClassVisitor >>>            register("com.sjtu.yifei.JmhcMMUhkR.com?sjtu?yifei?JmhcMMUhkR?RouteInject")
InjectClassVisitor >>>            register("com.sjtu.yifei.fpyxYyTCRm.com?sjtu?yifei?fpyxYyTCRm?AInterceptorInject")
InjectClassVisitor >>>        }
InjectClassVisitor >>> }
TransformPluginLaunch >>> ========== Transform insert cost 0.017 secs end ===========
Copy the code
  1. Routerfit.register(Class

    Service) implements the service declared on the interface in dynamic proxy mode.

The above is the overall framework design idea, so that readers can understand the framework architecture of ARetrofit from a global perspective. Next, we will discuss how annotationProcessor and Transform are used in the project, as well as how dynamic proxy and interceptor functions are implemented.

AnnotationProcessor generates code

Annotation Processor is a built-in javAC tool for scanning and processing annotations at compile time. To put it simply, during source code compilation, annotations processor can be used to obtain annotations in source files. Android Gradle 2.2 and later provide annotationProcessor plug-ins. AnnotationProcessor module is auto-complier in ARetrofit, so annotations need to be declared before using annotationProcessor. For those who are not familiar with or forget annotations, please refer to the Article Java annotations I wrote earlier. The annotations declared in this project are in the Auto-Annotation module, mainly including:

  • @extra Route parameters
  • @Flags intent flags
  • @go Route path key
  • @interceptor Declares a custom Interceptor
  • @requestCode Route parameters
  • @ the Route routing
  • @Uri
  • @iMethod is used to mark the method into which the registration code will be inserted (used in transform)
  • @ Inject for marking need to be injected into class, recently will be inserted into the tag # com. Sjtu. Yifei. The annotation. IMethod method (use) in the transform

Create a custom annotation handler. For details, see Dynamically generating code with Annotations. The annotation handler in this project is as follows:

// This is used to register the version of the source code to be processed by the annotation handler. SupportedSourceVersion(SourceVersion.release_8) // This annotation is used to register the annotation type to be handled by the annotation handler. Valid values are fully qualified @supportedannotationTypes ({ANNOTATION_ROUTE, ANNOTATION_GO}) // to annotate the processor, @autoService (processor.class) public class IProcessor extends AbstractProcessor {}Copy the code

A key part of the generated code in GenerateAInterceptorInjectImpl and GenerateRouteInjectImpl, posted the following key code:

public void generateAInterceptorInjectImpl(String pkName) { try { String name = pkName.replace(".",DECOLLATOR) + SUFFIX;  logger.info(String.format("auto generate class = %s", name)); TypeSpec.Builder builder = TypeSpec.classBuilder(name) .addModifiers(Modifier.PUBLIC) .addAnnotation(Inject.class) .addSuperinterface(AInterceptorInject.class); ClassName hashMap = ClassName.get("java.util", "HashMap"); //Map<String, Class<? >> TypeName wildcard = WildcardTypeName.subtypeOf(Object.class); TypeName classOfAny = ParameterizedTypeName.get(ClassName.get(Class.class), wildcard); TypeName string = ClassName.get(Integer.class); TypeName map = ParameterizedTypeName.get(ClassName.get(Map.class), string, classOfAny); MethodSpec.Builder injectBuilder = MethodSpec.methodBuilder("getAInterceptors") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .returns(map) .addStatement("$T interceptorMap = new $T<>()", map, hashMap); for (Map.Entry<Integer, ClassName> entry : interceptorMap.entrySet()) { logger.info("add path= " + entry.getKey() + " and class= " + entry.getValue().simpleName()); injectBuilder.addStatement("interceptorMap.put($L, $T.class)", entry.getKey(), entry.getValue()); } injectBuilder.addStatement("return interceptorMap"); builder.addMethod(injectBuilder.build()); JavaFile javaFile = JavaFile.builder(pkName, builder.build()) .build(); javaFile.writeTo(filer); } catch (Exception e) { e.printStackTrace(); } } public void generateRouteInjectImpl(String pkName) { try { String name = pkName.replace(".",DECOLLATOR) + SUFFIX; logger.info(String.format("auto generate class = %s", name)); TypeSpec.Builder builder = TypeSpec.classBuilder(name) .addModifiers(Modifier.PUBLIC) .addAnnotation(Inject.class) .addSuperinterface(RouteInject.class); ClassName hashMap = ClassName.get("java.util", "HashMap"); //Map<String, String> TypeName wildcard = WildcardTypeName.subtypeOf(Object.class); TypeName classOfAny = ParameterizedTypeName.get(ClassName.get(Class.class), wildcard); TypeName string = ClassName.get(String.class); TypeName map = ParameterizedTypeName.get(ClassName.get(Map.class), string, classOfAny); MethodSpec.Builder injectBuilder = MethodSpec.methodBuilder("getRouteMap") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .returns(map) .addStatement("$T routMap = new $T<>()", map, hashMap); for (Map.Entry<String, ClassName> entry : routMap.entrySet()) { logger.info("add path= " + entry.getKey() + " and class= " + entry.getValue().enclosingClassName()); injectBuilder.addStatement("routMap.put($S, $T.class)", entry.getKey(), entry.getValue()); } injectBuilder.addStatement("return routMap"); builder.addMethod(injectBuilder.build()); JavaFile javaFile = JavaFile.builder(pkName, builder.build()) .build(); javaFile.writeTo(filer); } catch (Exception e) { e.printStackTrace(); }}Copy the code

Second, the Transform

Android Gradle tools since version 1.5.0 provide a Transfrom API that allows third-party plugins to manipulate.class files during compilation prior to packaging dex files. This part is for senior Android engineers, oriented bytecode programming, ordinary engineers do not understand.

Now, one might wonder, if the annotationProcessor is so good, why is there a Transform for bytecode injection? The annotationProcessor is limited in that it can only scan code under the current Module, not third-party JAR and AAR files. Transform has no such limitation and operates on the.class file during compilation before packaging the dex file.

Transform API — A Real World example for how to use the Transfrom API in Android Studio, and bytecode instructions for reading ASM.

The TransformPluginLaunch plugin in the auto-InjectModule implements the source code TransformPluginLaunch as follows, and posts the key parts:

/** ** standard transform format, Todo implements its own bytecode enhancement/optimization operation */ class TransformPluginLaunch implements Transform implements Plugin<Project> { @Override void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { super.transform(transformInvocation) //todo step1: Scan transformInvocation. Inputs. Each {TransformInput input - > input. JarInputs. Each {JarInput JarInput - >... } input. DirectoryInputs. Each {DirectoryInput DirectoryInput - > / / after processing the input file, the output to the next task... } } //todo step2: ... If (injectInfo.get ().injectToclass! = null) { ... @param jarFile */ static void scanJar(File jarFile, File destFile) {} @param File */ static void scanFile(File File, File dest) {... }}Copy the code

Injection code is typically divided into two steps

  • Step 1: Scan this part mainly scans the information of classes and methods, the implementation class of AutoRegisterContract and the method of @iMethod and @Inject. The class to be injected and the method information are the RouteInject and AInterceptorInject implementation classes and are annotated by @Inject.
  • Step 2: Inject the results of the scan above, and inject the class to be injected into the injected class. This process is ASM oriented, and you can refer to the bytecode instructions to read the following key injection code:
class InjectClassVisitor extends ClassVisitor {
...
    class InjectMethodAdapter extends MethodVisitor {

        InjectMethodAdapter(MethodVisitor mv) {
            super(Opcodes.ASM5, mv)
        }

        @Override
        void visitInsn(int opcode) {
            Log.e(TAG, "inject to class:")
            Log.e(TAG, own + "{")
            Log.e(TAG, "       public *** " + InjectInfo.get().injectToMethodName + "() {")
            if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
                InjectInfo.get().injectClasses.each { injectClass ->
                    injectClass = injectClass.replace('/', '.')
                    Log.e(TAG, "           " + method + "(\"" + injectClass + "\")")
                    mv.visitVarInsn(Opcodes.ALOAD, 0)
                    mv.visitLdcInsn(injectClass)
                    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, own, method, "(Ljava/lang/String;)V", false)
                }
            }
            Log.e(TAG, "       }")
            Log.e(TAG, "}")
            super.visitInsn(opcode)
        }
...
    }
...
}
Copy the code

Dynamic proxy

Definition: provides a proxy for other objects to control access to that object. A proxy object can act as an intermediary between the client and the target object in cases where the client does not want or cannot directly reference another object. Routerfit.register(Class

service) Uses dynamic proxy mode, which makes ARetrofit API concise and allows users to elegantly define outlet interfaces. Dynamic proxy learning about the difficulty is relatively small, want to understand the students can refer to this article Java dynamic proxy.

Relevant source code of this project:

public final class Routerfit { ... private <T> T create(final Class<T> service) { RouterUtil.validateServiceInterface(service); return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<? >[]{service}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable { // If the method is a method from Object then defer to normal invocation. if  (method.getDeclaringClass() == Object.class) { return method.invoke(this, args); } ServiceMethod<Object> serviceMethod = (ServiceMethod<Object>) loadServiceMethod(method, args); if (! TextUtils.isEmpty(serviceMethod.uristring)) { Call<T> call = (Call<T>) new ActivityCall(serviceMethod); return call.execute(); } try { if (serviceMethod.clazz == null) { throw new RouteNotFoundException("There is no route match the path \"" + serviceMethod.routerPath + "\""); } } catch (RouteNotFoundException e) { Toast.makeText(ActivityLifecycleMonitor.getApp(), e.getMessage(), Toast.LENGTH_SHORT).show(); e.printStackTrace(); } if (RouterUtil.isSpecificClass(serviceMethod.clazz, Activity.class)) { Call<T> call = (Call<T>) new ActivityCall(serviceMethod); return call.execute(); } else if (RouterUtil.isSpecificClass(serviceMethod.clazz, Fragment.class) || RouterUtil.isSpecificClass(serviceMethod.clazz, android.app.Fragment.class)) { Call<T> call = new FragmentCall(serviceMethod); return call.execute(); } else if (serviceMethod.clazz ! = null) { Call<T> call = new IProviderCall<>(serviceMethod); return call.execute(); } if (serviceMethod.returnType ! = null) { if (serviceMethod.returnType == Integer.TYPE) { return -1; } else if (serviceMethod.returnType == Boolean.TYPE) { return false; } else if (serviceMethod.returnType == Long.TYPE) { return 0L; } else if (serviceMethod. ReturnType == Double.TYPE) {return 0.0d; } else if (serviceMethod. ReturnType == float.type) {return 0.0f; } else if (serviceMethod.returnType == Void.TYPE) { return null; } else if (serviceMethod.returnType == Byte.TYPE) { return (byte)0; } else if (serviceMethod.returnType == Short.TYPE) { return (short)0; } else if (serviceMethod.returnType == Character.TYPE) { return null; } } return null; }}); }... }Copy the code

ServiceMethod is a very important class here, which uses a facade pattern and is mainly used to parse and store all the annotated information in a method.

Implementation of interceptor chain

The interceptor chain design in this project enables users to process business logic very elegantly. As follows:

@Interceptor(priority = 3) public class LoginInterceptor implements AInterceptor { private static final String TAG = "LoginInterceptor"; @override public void Intercept (Final Chain Chain) {//Test2Activity requires login if ("/login-module/Test2Activity".equalsIgnoreCase(chain.path())) { Routerfit.register(RouteService.class).launchLoginActivity(new ActivityCallback() { @Override public void onActivityResult(int i, The Object data) {if (I = = Routerfit. RESULT_OK) {/ / continue Toast. After a successful login makeText (ActivityLifecycleMonitor. GetTopActivityOrApp (), "Login succeeded ", toast.length_long).show(); chain.proceed(); } else {Toast. MakeText (ActivityLifecycleMonitor getTopActivityOrApp (), "login cancellation/failure", Toast. LENGTH_LONG), show (); }}}); } else { chain.proceed(); }}}Copy the code

The idea of this part of the implementation is to reference the interceptor in OKHTTP, which uses the Java design pattern chain of responsibility pattern. The specific implementation is welcome to read the source code.

conclusion

So far, the ARetrofit open source project has been basically covered. In the process of doing this project, I actually encountered various problems, among which ASM took a long time, which was still small for me at that time. Of course, harvest is also quite a lot, this is my first open source project, there are shortcomings welcome readers and users to put forward, you can directly put forward in the QQ group.