写这篇文章的初心:因为项目中有使用到AppJoint框架来搭建组件化的项目,自己也一直很好奇AppJoint框架的源码&代码逻辑,所以才有了这篇文章的出现。
直接上库的开源地址 有兴趣的小伙伴不妨尝试一下使用之来搭建组件化框架,绝对开拓你的思路,不多逼逼,下面直接进入我们的主题。
AppJoint开源项目地址:https://github.com/PrototypeZ/AppJoint
概述
AppJoint是在构建组件化过程中,解决某些问题手段工具。可以用来解决:多Application合并和初始化问题,模块间相互调用的问题。
AppJoint能够解决组件化遇到的问题
- 如何初始化多个Application
- 如何在不相互依赖的模块间相互访问
解决问题一,如何初始化多个Application
场景分析
一般组件化工程包含一个主模块的Application和多个模块中的Application。那如何解决多个Application初始化问题。
解决方式
一般人认为:因为主模块依赖所有的模块,理所当然可以访问所有的公共类。那么只需要在主模块Application的各个生命周期中,按一定顺序调用各个模块Application对应的生命周期方法,那么就可以解决这个问题
AppJoint角度
没错少年,你这个想法是非常正确的。但是做法有点他太low。如果有一百个Application你要手动在主模块Application的各个生命周期中调用一百次对应的各个Application的生命周期方法?是不是感觉太麻烦,所以AppJoint就是为了解决这个问题,引入了AOP,自动帮我们完成代码的插入(原理后面会提到)。从而达到了减少开发者的工作量,加快工作效率的目的
AppJoint类库文件中包含两个特殊的注解:
@AppSpec
@ModuleSpec(priority = ${value})//其中vlaue为指定初始化的优先级
第一个注解AppSpec是用来标记壳Application,第二个注解ModuleSpec用来标记业务模块的Application。
那么AppJoint类库就是根据这些标记,在壳Application中主动调用被ModuleSpec标记的Application中的生命周期方法,
来达到初始化目的
解决问题二,如何在不相互依赖的模块间相互访问
场景分析:如果有两个模块A,B,而且两个模块之间是没有相互依赖的,那么A,B模块之间就不可以相互访问。
解决方式
如果模块A和B有个共同依赖模块C,假设有个接口a1存在模块C中,并且模块A中存在接口a1的实现类A1。此时如果我想让模块B使用模块A中的 类A1,可是因为模块B没有依赖模块A , 所以访问不到类A1 。这种情况下可以使用反射创建类A1对象,然后通过JAVA语言多态的特性,赋值给类型a1。那么此时模块B就可以间接访问类A1对象和方法了。此处有不懂的小伙伴可以亲自尝试
AppJoint实现方式
其实AppJoint也是大致使用了上面的技巧(反射),但是它还帮我们完成了一步,就是自动找出所有模块中需要反射的类,然后我们需要的时候通过反射生成给我们使用。从而达到了减少开发者的工作量,加快工作效率的目的
使用下面的注解标记需要被反射的类
@ServiceProvider(${"value"})//此处的Value用来标识当前实现类的唯一标识
使用下面的方法生成需要的类对象
Module1Service service = AppJoint.service(Module1Service.class);
AppJoint库实现逻辑
AppJoint原理.pngAppJonit具体代码分析
使用了Aop技术其实就是自动完成AppJoint.java源文件应该需要承担的代码职责。减少开发者工作量,提高开发效率。
要想彻底了解下面的执行逻辑,需要懂得技术:
1.jar文件的格式,可以自行百度百科中查看
2.安卓编写Gradle的插件流程
3.ASM基本的原理,熟悉相关api(ASM是AOP实现的其中一种方案)
- 如何填充AppJoint中的字段moduleApplications?看看AppJointPlugin是如何根据@ModuleSpec注解找寻相关的类文件。
代码逻辑
//file为java源文件,output为文件要保存的文件夹
boolean findAnnotatedClasses(File file, File output) {
if (!file.exists() || !file.name.endsWith(".class")) {
return
}
def needsModification = false
def inputStream = new FileInputStream(file)
//开始解析java源文件
ClassReader cr = new ClassReader(inputStream)
cr.accept(new ClassVisitor(Opcodes.ASM5) {
static class AnnotationMethodsVisitor extends AnnotationVisitor {
AnnotationMethodsVisitor() {
super(Opcodes.ASM5)
}
@Override
void visit(String name, Object value) {
mProject.logger.info("Annotation value: name=$name value=$value")
super.visit(name, value)
}
}
@Override//解析类文件头部是否包含注解
AnnotationVisitor visitAnnotation(String desc, boolean visible) {
mProject.logger.info("visiting $desc")
switch (desc) {
//解析@ModuleSpec注解
case "Lio/github/prototypez/appjoint/core/ModuleSpec;":
addModuleApplication(new AnnotationModuleSpec(cr.className))
return new AnnotationMethodsVisitor() {
@Override
void visit(String name, Object value) {
def moduleApplication = moduleApplications.find({
it.className == cr.className
})
if (moduleApplication) {
moduleApplication.order = Integer.valueOf(value)
}
super.visit(name, value)
}
}
case "Lio/github/prototypez/appjoint/core/AppSpec;":
appApplications[file] = output
needsModification = true
break
//解析@ServiceProvider
case "Lio/github/prototypez/appjoint/core/ServiceProvider;":
return new AnnotationMethodsVisitor() {
boolean valueSpecified;
@Override
void visit(String name, Object value) {
valueSpecified = true;
cr.interfaces.each {
routerAndImpl[new Tuple2(it, value)] = cr.className
}
super.visit(name, value)
}
@Override
void visitEnd() {
if (!valueSpecified) {
cr.interfaces.each {
routerAndImpl[new Tuple2(it, SERVICE_PROVIDER_DEFAULT_NAME)] =
cr.className
}
}
super.visitEnd()
}
}
}
return super.visitAnnotation(desc, visible)
}
}, 0)
inputStream.close()
return needsModification
}
//往moduleApplications添加元素(被ModuleSpec标记的Application)
private void addModuleApplication(AnnotationModuleSpec annotationOrder) {
for (int i = 0; i < moduleApplications.size(); i++) {
if (annotationOrder.className == moduleApplications.get(i).className) {
// the module application class ready to be added is already marked
return
}
}
moduleApplications.add(annotationOrder)
}
//将相关代码添加到AppJoint.java的构造器中
class AddCodeToConstructorVisitor extends MethodVisitor {
AddCodeToConstructorVisitor(MethodVisitor mv) {
super(Opcodes.ASM5, mv)
}
@Override
void visitInsn(int opcode) {
switch (opcode) {
case Opcodes.IRETURN:
case Opcodes.FRETURN:
case Opcodes.ARETURN:
case Opcodes.LRETURN:
case Opcodes.DRETURN:
case Opcodes.RETURN:
//在构造器结尾处,插入moduleApplications包含所有Module的Application
moduleApplications.sort { a, b -> a.order <=> b.order }
for (int i = 0; i < moduleApplications.size(); i++) {
mProject.logger.info("insertApplicationAdd order:${moduleApplications[i].order} className:${moduleApplications[i].className}")
//moduleApplications[i].className,Application对应的文件名。作用插入代码
insertApplicationAdd(moduleApplications[i].className)
}
routerAndImpl.each {
Tuple2<String, String> router, impl -> insertRoutersPut(router, impl)
}
break
}
super.visitInsn(opcode)
}
/**
* 使用字节码的方式,往AppJoint.class文件中的构造器插入下面的字节码
* 字节码大概的意思:获取AppJoint类中的mmoduleApplications字段,
* 并使用add方法添加New出来的applicationName实例。
*/
void insertApplicationAdd(String applicationName) {
mv.visitVarInsn(Opcodes.ALOAD, 0)
mv.visitFieldInsn(Opcodes.GETFIELD, "io/github/prototypez/appjoint/AppJoint", "moduleApplications", "Ljava/util/List;")
mv.visitTypeInsn(Opcodes.NEW, applicationName)
mv.visitInsn(Opcodes.DUP)
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, applicationName, "<init>", "()V", false)
mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/List", "add", "(Ljava/lang/Object;)Z", true)
mv.visitInsn(Opcodes.POP)
}
-
如何填充AppJoint中的字段routersMap?看看AppJointPlugin是如何根据@ServiceProvider注解找寻相关的类文件。
在AppJointTransform.groovy文件中有个routerAndImpl字段,专门用来保存被@ServiceProvider标注的类文件,具体字段的类型为:
HashMap<Tuple2<String, String>, String>()
第一个String保存的字段为抽象的接口名字。
第二个字段String保存的字段为@ServiceProvider中的Value字段。
第三个String保存的具体的接口抽象类的名字。
//大致的逻辑和解析@ModuleSpec注解差不多,具体可以参考上面的代码
//最后就是将查找的类信息,往AppJoint.class文件中的构造方法插入相关代码,来完成AppJoint的字段routersMap的初始化
/**
* 解释一下代码中的操作含义:获取AppJoint中的routersMap字段,然后获取抽象接口的Class对象,获取具体抽象类接口的实现类的Class对象
* 和最后的ServiceProvider的Values字符串,一同保存到routerMap中。
*/
void insertRoutersPut(Tuple2<String, String> router, String impl) {
mv.visitVarInsn(Opcodes.ALOAD, 0)
mv.visitFieldInsn(Opcodes.GETFIELD, "io/github/prototypez/appjoint/AppJoint", "routersMap",
"Lio/github/prototypez/appjoint/util/BinaryKeyMap;")
mv.visitLdcInsn(Type.getObjectType(router.first))//返回Class对象
mv.visitLdcInsn(router.second)//String类型
mv.visitLdcInsn(Type.getObjectType(impl))//返回Class对象
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "io/github/prototypez/appjoint/util/BinaryKeyMap",
"put", "(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V", true)
}
-
如何完成AppJointTransform中的appApplications赋值?看看AppJointPlugin是如何根据@AppSpec注解找寻相关的类文件。
用来保存@AppSpec注解字段的数据,声明为def appApplications = [:],Key-Value的形式。
左边的Key类型为:对应的类文件,类型为File,右边的Value类型为:对应类文件要保存的文件夹,类型为File
逻辑执行图.png
// 开始解析壳app的Application文件
appApplications.each { File classFile, File output ->
inputStream = new FileInputStream(classFile)
ClassReader reader = new ClassReader(inputStream)
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS)
ClassVisitor visitor = new ApplicationClassVisitor(writer)//这里的ApplicationClassVisitor承担了代码插入的功能
reader.accept(visitor, 0)
output.bytes = writer.toByteArray()//将插入的字节赋值给最终要输出文件
inputStream.close()
}
//添加字节码到Application类文件中
class ApplicationClassVisitor extends ClassVisitor {
boolean onCreateDefined
boolean attachBaseContextDefined
boolean onConfigurationChangedDefined
boolean onLowMemoryDefined
boolean onTerminateDefined
boolean onTrimMemoryDefined
ApplicationClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv)
}
@Override
MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions)
mProject.logger.info("visiting method: $access, $name, $desc, $signature, $exceptions")
switch (name + desc) {
case "onCreate()V":
onCreateDefined = true
return new AddCallAppJointMethodVisitor(methodVisitor, "onCreate", "()V", false, false)
case "attachBaseContext(Landroid/content/Context;)V":
attachBaseContextDefined = true
return new AddCallAppJointMethodVisitor(methodVisitor, "attachBaseContext", "(Landroid/content/Context;)V", true, false)
case "onConfigurationChanged(Landroid/content/res/Configuration;)V":
onConfigurationChangedDefined = true
return new AddCallAppJointMethodVisitor(methodVisitor, "onConfigurationChanged", "(Landroid/content/res/Configuration;)V", true, false)
case "onLowMemory()V":
onLowMemoryDefined = true
return new AddCallAppJointMethodVisitor(methodVisitor, "onLowMemory", "()V", false, false)
case "onTerminate()V":
onTerminateDefined = true
return new AddCallAppJointMethodVisitor(methodVisitor, "onTerminate", "()V", false, false)
case "onTrimMemory(I)V":
onTrimMemoryDefined = true
return new AddCallAppJointMethodVisitor(methodVisitor, "onTrimMemory", "(I)V", false, true)
}
return methodVisitor
}
@Override
void visitEnd() {
if (!attachBaseContextDefined) {
defineMethod(4, "attachBaseContext", "(Landroid/content/Context;)V", true, false)
}
if (!onCreateDefined) {
defineMethod(1, "onCreate", "()V", false, false)
}
if (!onConfigurationChangedDefined) {
defineMethod(1, "onConfigurationChanged", "(Landroid/content/res/Configuration;)V", true, false)
}
if (!onLowMemoryDefined) {
defineMethod(1, "onLowMemory", "()V", false, false)
}
if (!onTerminateDefined) {
defineMethod(1, "onTerminate", "()V", false, false)
}
if (!onTrimMemoryDefined) {
defineMethod(1, "onTrimMemory", "(I)V", false, true)
}
super.visitEnd()
}
void defineMethod(int access, String name, String desc, boolean aLoad1, boolean iLoad1) {
MethodVisitor methodVisitor = this.visitMethod(access, name, desc, null, null)
methodVisitor.visitVarInsn(Opcodes.ALOAD, 0)
if (aLoad1) {
methodVisitor.visitVarInsn(Opcodes.ALOAD, 1)
}
if (iLoad1) {
methodVisitor.visitVarInsn(Opcodes.ILOAD, 1)
}
methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "android/app/Application", name, desc, false)
methodVisitor.visitInsn(Opcodes.RETURN)
methodVisitor.visitEnd()
}
}
//在对应得生命周期得方法中调用AppJoint.class对应的生命周期的方法
class AddCallAppJointMethodVisitor extends MethodVisitor {
String name
String desc
boolean aLoad1
boolean iLoad1
AddCallAppJointMethodVisitor(MethodVisitor mv, String name, String desc, boolean aLoad1, boolean iLoad1) {
super(Opcodes.ASM5, mv)
this.name = name
this.desc = desc
this.aLoad1 = aLoad1
this.iLoad1 = iLoad1
}
void visitInsn(int opcode) {
switch (opcode) {
case Opcodes.IRETURN:
case Opcodes.FRETURN:
case Opcodes.ARETURN:
case Opcodes.LRETURN:
case Opcodes.DRETURN:
case Opcodes.RETURN:
//调用AppJoint的get方法,获取对应的AppJoint对象。
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "io/github/prototypez/appjoint/AppJoint", "get", "()Lio/github/prototypez/appjoint/AppJoint;", false)
if (aLoad1) {
mv.visitVarInsn(Opcodes.ALOAD, 1)
}
if (iLoad1) {
mv.visitVarInsn(Opcodes.ILOAD, 1)
}
//访问AppJoint对应name方法。
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "io/github/prototypez/appjoint/AppJoint", name, desc, false)
break
}
super.visitInsn(opcode)
}
}
AppJoint得源码就分析完了,看完源码之后,我们知道AOP得这种技术是多么得好用,强大。也看到了会操作字节码,看懂字节码在AOP编程中有多重要。建议大家平时可以多了解AOP在安卓得应用,AOP目前有多种实现框架,都可以列出来对比对比。
提示:要实现跨模块调用可以用多种方式技巧,将接口下沉到公共模块,再使用反射技术,构造具体接口的实现类,从而达到跨模块调用。其实AppJoint也是实现了这个逻辑,不过他们为了减少开发者工作量,自动帮我们填充了必要的代码,完成了模块调用之间的关联。
下面介绍几篇学习ASM的文章:
asm学习笔记之生成方法
https://blog.csdn.net/aesop_wubo/article/details/48948211
推荐大家一本书:《深入理解Java虚拟机》里面有详细介绍GC机制,.class的结构,包括字节码等等
网友评论