乐趣区

Android-组件化最佳实践-ARetrofit-原理

本文首发于 vivo 互联网技术 微信公众号 https://mp.weixin.qq.com/s/TXFt7ymgQXLJyBOJL8F6xg
作者:朱壹飞

ARetrofit 是一款针对 Android 组件之间通信的路由框架,实现快速组件化开发的利器。本文主要讲述 ARetrofit 实现的原理。

简介

ARetrofit 是一款针对 Android 组件之间通信的路由框架,实现快速组件化开发的利器。

源码链接:https://github.com/yifei8/ARetrofit

组件化架构 APP Demo, ARetrofit 使用实例:https://github.com/yifei8/HappyNote

组件化

Android 组件化已经不是一个新鲜的概念了,出来了已经有很长一段时间了,大家可以自行 Google,可以看到一堆相关的文章。

简单的来说,所谓的组件就是 Android Studio 中的 Module,每一个 Module 都遵循高内聚的原则,通过 ARetrofit 来实现无耦合的代码结构,如下图:

每一个 Module 可单独作为一个 project 运行,而打包到整体时 Module 之间的通信通过 ARetrofit 完成。

ARetrofit 原理

讲原理之前,我想先说说为什么要 ARetrofit。开发 ARetrofit 这个项目的思路来源其实是 Retrofit,Retrofit 是 Square 公司开发的一款针对 Android 网络请求的框架,这里不对 Retrofit 展开来讲。主要是 Retrofit 框架使用非常多的设计模式,可以说 Retrofit 这个开源项目将 Java 的设计模式运用到了极致,当然最终提供的 API 也是非常简洁的。如此简洁的 API,使得我们 APP 中的网络模块实现变得非常轻松,并且维护起来也很舒服。因此我觉得有必要将 Android 组件之间的通信也变得轻松,使用者可以优雅的通过简洁的 API 就可以实现通信,更重要的是维护起来也非常的舒服。

ARetrofit 基本原理可以简化为下图所示:

1. 通过注解声明需要通信的 Activity/Fragment 或者 Class

2. 每一个 module 通过 annotationProcessor 在编译时生成待注入的 RouteInject 的实现类和 AInterceptorInject 的实现类。

这一步在执行 app[build]时会输出日志,可以直观的看到,如下图所示:

注: AInjecton::Compiler >>> Apt interceptor Processor start... <<<
注: AInjecton::Compiler enclosindClass = null
注: AInjecton::Compiler value = 3
注: AInjecton::Compiler auto generate class = com$$sjtu$$yifei$$eCGVmTMvXG$$AInterceptorInject
注: AInjecton::Compiler add path= 3 and class= LoginInterceptor
....
注: AInjecton::Compiler >>> Apt route Processor start... <<<
注: AInjecton::Compiler enclosindClass = null
注: AInjecton::Compiler value = /login-module/ILoginProviderImpl
注: AInjecton::Compiler enclosindClass = null
注: 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
注: AInjecton::Compiler add path= /login-module/TestFragment and class= null
注: AInjecton::Compiler add path= /login-module/LoginActivity and class= null
注: AInjecton::Compiler add path= /login-module/Test2Activity and class= null
注: AInjecton::Compiler add path= /login-module/ILoginProviderImpl and class= null
注: AInjecton::Compiler >>> Apt route Processor succeed <<<

3. 将编译时生成的类注入到 RouterRegister 中,这个类主要用于维护路由表和拦截器,对应的 [build] 日志如下:

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 ===========

4.Routerfit.register(Class<T> service) 这一步主要是通过动态代理模式实现接口中声明的服务。

前面讲的是整体的框架设计思想,便于读者从全局的觉得来理解 ARetrofit 的框架的架构。接下来,将待大家个个击破上面提到的 annotationProcessor、transform 在项目中如何使用,以及动态代理、拦截器功能的实现等细节。

一、annotationProcessor 生成代码

annotationProcessor(注解处理器)是 javac 内置的一个用于编译时扫描和处理注解 (Annotation) 的工具。简单的说,在源代码编译阶段,通过注解处理器,我们可以获取源文件内注解 (Annotation) 相关内容。Android Gradle 2.2 及以上版本提供 annotationProcessor 的插件。

在 ARetrofit 中 annotationProcessor 对应的 module 是 auto-complier,在使用 annotationProcessor 之前首先需要声明好注解。关于注解不太了解或者遗忘的同学可直接参考我之前写的 Java 注解这篇文章,本项目中声明的注解在 auto-annotation 这个 module 中,主要有:

  • @Extra 路由参数
  • @Flags intent flags
  • @Go 路由路径 key
  • @Interceptor 声明自定义拦截器
  • @RequestCode 路由参数
  • @Route 路由
  • @Uri
  • @IMethod 用于标记注册代码将插入到此方法中(transform 中使用)
  • @Inject 用于标记需要被注入类,最近都将插入到标记了 #com.sjtu.yifei.annotation.IMethod 的方法中(transform 中使用)

创建自定义的注解处理器,具体使用方法可参考利用注解动态生成代码,本项目中的注解处理器如下所示:

// 这是用来注册注解处理器要处理的源代码版本。@SupportedSourceVersion(SourceVersion.RELEASE_8)
// 这个注解用来注册注解处理器要处理的注解类型。有效值为完全限定名(就是带所在包名和路径的类全名
@SupportedAnnotationTypes({ANNOTATION_ROUTE, ANNOTATION_GO})
// 来注解这个处理器,可以自动生成配置信息
@AutoService(Processor.class)
public class IProcessor extends AbstractProcessor { }

生成代码的关键部分在 GenerateAInterceptorInjectImpl 和 GenerateRouteInjectImpl 中,以下贴出关键代码:

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();
        }
 
    }

二、Transform

Android Gradle 工具在 1.5.0 版本后提供了 Transfrom API, 允许第三方 Plugin 在打包 dex 文件之前的编译过程中操作 .class 文件。这一部分面向高级 Android 工程师的,面向字节码编程,普通工程师可不做了解。

写到这里也许有人会有这样一个疑问,既然 annotationProcessor 这么好用为什么还有 Transform 面向字节码注入呢?这里需要解释以下,annotationProcessor 具有局限性,annotationProcessor 只能扫描当前 module 下的代码,且对于第三方的 jar、aar 文件都扫描不到。而 Transform 就没有这样的局限性,在打包 dex 文件之前的编译过程中操作.class 文件。

关于 Transfrom API 在 Android Studio 中如何使用可以参考 Transform API — a real world example,顺便提供一下字节码指令方便我们读懂 ASM。

本项目中的 Transform 插件在 AInject 中,实现源码 TransformPluginLaunch 如下,贴出关键部分:

/**
 *
 * 标准 transform 的格式,一般实现 transform 可以直接拷贝一份重命名即可
 *
 * 两处 todo 实现自己的字节码增强/优化操作
 */
class TransformPluginLaunch extends Transform implements Plugin<Project> {
 
    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {super.transform(transformInvocation)
  
        //todo step1: 先扫描
        transformInvocation.inputs.each {
            TransformInput input ->
                input.jarInputs.each { JarInput jarInput ->
                   ...
                }
 
                input.directoryInputs.each { DirectoryInput directoryInput ->
                    // 处理完输入文件之后,要把输出给下一个任务
                  ...
                }
        }
        
        //todo step2: ... 完成代码注入
        if (InjectInfo.get().injectToClass != null) {...}
   
    }
 
    /**
     * 扫描 jar 包
     * @param jarFile
     */
    static void scanJar(File jarFile, File destFile) { }
 
    /**
     * 扫描文件
     * @param file
     */
    static void scanFile(File file, File dest) {...}
}

注入代码一般分为两个步骤:

  • 第一步:扫描
    这一部分主要是扫描的内容有:
    注入类和方法的信息,是 AutoRegisterContract 的实现类和其中 @IMethod,@Inject 的方法。
    待注入类的和方法信息,是 RouteInject 和 AInterceptorInject 实现类且被 @Inject 注解的。
  • 第二步:注入
    以上扫描的结果,将待注入类注入到注入类的过程。这一过程面向 ASM 操作,可参考字节码指令来读懂以下的关键注入代码:
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)
        }
...
    }
...
}

三、动态代理

定义:为其它对象提供一种代理以控制对这个对象的访问控制;在某些情况下,客户不想或者不能直接引用另一个对象,这时候代理对象可以在客户端和目标对象之间起到中介的作用。

Routerfit.register(Class<T> service) 这里就是采用动态代理的模式,使得 ARetrofit 的 API 非常简洁,使用者可以优雅定义出路由接口。关于动态代理的学习难度相对来说还比较小,想了解的同学可以参考这篇文章 java 动态代理。

本项目相关源码:

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;
            }
        });
    }
...
}

这里 ServiceMethod 是一个非常重要的类,使用了外观模式,主要用于解析方法中的被注解所有信息并保存起来。

四、拦截器链实现

本项目中的拦截器链设计,使得使用者可以非常优雅的处理业务逻辑。如下:

@Interceptor(priority = 3)
public class LoginInterceptor implements AInterceptor {
 
    private static final String TAG = "LoginInterceptor";
    @Override
    public void intercept(final Chain chain) {
        //Test2Activity 需要登录
        if ("/login-module/Test2Activity".equalsIgnoreCase(chain.path())) {Routerfit.register(RouteService.class).launchLoginActivity(new ActivityCallback() {
                @Override
                public void onActivityResult(int i, Object data) {if (i == Routerfit.RESULT_OK) {// 登录成功后继续执行
                        Toast.makeText(ActivityLifecycleMonitor.getTopActivityOrApp(), "登录成功", Toast.LENGTH_LONG).show();
                        chain.proceed();} else {Toast.makeText(ActivityLifecycleMonitor.getTopActivityOrApp(), "登录取消 / 失败", Toast.LENGTH_LONG).show();}
                }
            });
        } else {chain.proceed();
        }
    }
 
}

这一部分实现的思想是参考了 okhttp 中的拦截器,这里使用了 java 设计模式责任链模式,具体实现欢迎阅读源码。

总结

基本上读完本文可以对 ARetrofit 的核心原理有了很清晰的理解. 简单来说 ARetrofit 通过 annotationProcessor 在编译时获取路由相关内容,通过 ASM 实现了可跨模块获取对象,最终通过动态代理实现面向切面编程(AOP)。

ARetrofit 相对于其他同类型的路由框架来说,其优点是提供了更加简洁的 API,其中高阶用法对开发者提供了更加灵活扩展方式,开发者还可以结合 RxJava 完成复杂的业务场景。具体可以参考 ARetrofit 的基本用法,以及 Issues。

————  参考资料   ————

  1. Java 注解:https://www.jianshu.com/p/ef1146a771b5
  2. 利用注解动态生成代码:https://blog.csdn.net/Gaugamela/article/details/79694302
  3. Transform API — a real world example:https://medium.com/grandcentrix/transform-api-a-real-world-example-cfd49990d3e1

更多内容敬请关注 vivo 互联网技术 微信公众号

注:转载文章请先与微信号:labs2020 联系。

退出移动版