Android 组件化最佳实践 ARetrofit 原理

Stella981
• 阅读 673

本文首发于 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 来实现无耦合的代码结构,如下图:

Android 组件化最佳实践 ARetrofit 原理

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

ARetrofit 原理

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

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

Android 组件化最佳实践 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 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 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 互联网技术 微信公众号

Android 组件化最佳实践 ARetrofit 原理

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

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
3个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Android So动态加载 优雅实现与原理分析
背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.!(https://oscimg.oschina.net/oscnet/00d1ff90e4b34869664fef59e3ec3fdd20b.png)点击上方“蓝字”关注我
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Docker 部署SpringBoot项目不香吗?
  公众号改版后文章乱序推荐,希望你可以点击上方“Java进阶架构师”,点击右上角,将我们设为★“星标”!这样才不会错过每日进阶架构文章呀。  !(http://dingyue.ws.126.net/2020/0920/b00fbfc7j00qgy5xy002kd200qo00hsg00it00cj.jpg)  2
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
9个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这