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。
———— 参考资料 ————
利用注解动态生成代码:https://blog.csdn.net/Gaugamela/article/details/79694302
Transform API — a real world example:https://medium.com/grandcentrix/transform-api-a-real-world-example-cfd49990d3e1
更多内容敬请关注 vivo 互联网技术 微信公众号
注:转载文章请先与微信号:labs2020 联系。
Android 组件化最佳实践 ARetrofit 原理的更多相关文章
- Android组件化最佳实践 ARetrofit原理
ARetrofit原理讲原理之前,我想先说说为什么要ARetrofit.开发ARetrofit这个项目的思路来源其实是Retrofit,Retrofit是Square公司开发的一款针对Android网 ...
- Android组件化路由实践
Android应用组件化各个组件页面之间要实现跳转使用路由是一个很好的选择.本文将实现一个比较轻量级的路由组件,主要涉及以下知识: Annotation (声明路由目标信息) AnnotationPr ...
- Android组件化开发实践
转载请注明出处:http://blog.csdn.net/crazy1235/article/details/76533115 http://mdsa.51cto.com/art/201707/544 ...
- Android组件化
附:Android组件化和插件化开发 App组件化与业务拆分那些事 Android项目架构之业务组件化 Android组件化核心之路由实现 Android组件化开发实践
- Android组件化框架设计与实践
在目前移动互联网时代,每个 APP 就是流量入口,与过去 PC Web 浏览器时代不同的是,APP 的体验与迭代速度影响着用户的粘性,这同时也对从事移动开发人员提出更高要求,进而移动端框架也层出不穷. ...
- 教你打造一个Android组件化开发框架
*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 CC:Component Caller,一个android组件化开发框架, 已开源,github地址:https://github ...
- 我所理解的Android组件化之通信机制
之前写过一篇关于Android组件化的文章,<Android组件化框架设计与实践>,之前没看过的小伙伴可以先点击阅读.那篇文章是从实战中进行总结得来,是公司的一个真实项目进行组件化架构改造 ...
- Gradle自动实现Android组件化模块构建
背景 随着App的不断迭代,业务会变得越来越复杂,业务模块会越来越多,且每个模块的代码也会变得越来越多.为了应对这一场景,我们需要把不同的业务模块划分成一个个组件,在修改业务代码的时候只需要在对应模块 ...
- 得到、微信、美团、爱奇艺APP组件化架构实践
一.背景 随着项目逐渐扩展,业务功能越来越多,代码量越来越多,开发人员数量也越来越多.此过程中,你是否有过以下烦恼? 项目模块多且复杂,编译一次要5分钟甚至10分钟?太慢不能忍? 改了一行代码 或只调 ...
随机推荐
- 解决Android调用相机拍照,要报“打开相机失败”查看debug日志显示“setParameters failed”的问题
使用CameraLibrary项目,在部分手机或平板上不能正常使用,要报“打开相机失败”查看debug日志显示“setParameters failed”. 找到CameraView.java中的se ...
- Java面试题_第四阶段
1.1 电商行业特点 1.分布式 垂直拆分:根据功能模块进行拆分 水平拆分:根据业务层级进行拆分 2.高并发 用户单位时间内访问服务器数量,是电商行业中面临的主要问题 3.集群 抗击高兵发的有效手段, ...
- GitHub 设置和取消代理,加速 git clone
git 设置代理: git config --global git 取消代理: git config --global --unset http.proxy 针对 github.com 设置代理: g ...
- 两种最常用的 HTTP 操作方法是:GET 和 POST。
什么是 HTTP? 超文本传输协议(HTTP)的设计目的是保证客户机与服务器之间的通信. HTTP 的工作方式是客户机与服务器之间的请求-应答协议. web 浏览器可能是客户端,而计算机上的网络应用程 ...
- 破解另一家网站的反爬机制 & HMAC 算法
零.写在前面 本文涉及的反爬技术,仅供个人技术学习,禁止并做到: 干扰被访问网站的正常运行 抓取受到法律保护的特定类型的数据或信息 搜集到的数据禁止传播.交给第三方使用.或者牟利 如有可能,在爬到数据 ...
- VS Code Remote,在服务器上开发程序,开启全新开发模式
一直使用Idea开发java 程序,头疼的是太太太占用内存了,笔记本电脑经常卡爆,在服务器开发的话又太麻烦,VS Code Remote的带来,解决了这一烦恼.下面来实战一下. VS Code Rem ...
- 小程序实现日期时间控件picker
小程序自带的组件中有日期跟时间的picker,但就缺个日期时间picker组件,那没办法,只能自己弄一个,这个组件不是很难,比较难的地方是要根据不同年份(是否闰年)跟月份决定一个月里有多少天. 我直接 ...
- library: Vulnhub Walkthrough
网络主机探测: 端口主机扫描: ╰─ nmap -p1-65535 -sV -A -O -sT 10.10.202.136 21/tcp open ftp vsftpd 3.0.380/tcp ope ...
- Go交叉编译(Go语言Mac/Linux/Windows下交叉编译)
Go交叉编译(Go语言Mac/Linux/Windows下交叉编译) 2019/11/21 Chenxin 在很多时候,由于开发的方便,会有这样的场景出现,使用Mac开发或使用Windows开发,需要 ...
- 012.MongoDB读写分离
一 读写分离概述 1.1 读写分离描述 从应用程序角度来看,使用Replica Set 和使用单台mongo很像.默认的驱动程序会连接primary节点,并且将所有读写请求都路由到主节点.但也可以通过 ...