AppJoint的实现核心代码主要在其Groovy实现的动态编译插件中,其实他的逻辑对于我们来说不难,但是Groovy编写动态编译插件的具体实现理解起来还是需要下一些功夫的。想要顺利的读懂AppJoint的插件,需要先做一些预备知识的准备。
零、相关知识储备
1.Groovy语言
https://blog.csdn.net/u010451990/article/details/105382861
2.在AndroidStudio中实现Gradle自定义插件
http://www.aoaoyi.com/archives/1274.html
3.了解Transform
4.了解使用ASM
一、看看框架为我们做了什么
在App Joint源码解读开始之前,我们先看下AppJoint在生成 apk的时候都做了哪些事(核心逻辑在plugin中)。
这里我们使用的是AppJoint提供的Demo,没有做任何更改直接编译。(编译后解压apk,再反编译成.java文件)
这里我们需要重点看两个类,第一个就是在Module:Core中的AppJoint。一个就是在Module:app中带有
@AppSpec
的App即在manifest中注册的主Application。
1.编译后的AppJoint
这里我们只将重点代码取出,方便大家找到重点和理解。
编译前后的AppJoint构造方法对比:
//Core中编译前构造方法
private AppJoint() {
}
//打包成apk后反解压的构造方法
private AppJoint() {
//含有@ModuleSpec的 子moudle Application
this.moduleApplications.add(new Module2Application());
this.moduleApplications.add(new Module1Application());
//含有@ServiceProvider的 对外暴露的服务接口
this.routersMap.put(AppService.class, "__app_joint_default", AppServiceImpl.class);
this.routersMap.put(AppService.class, "another", AppServiceImpl2.class);
this.routersMap.put(Module1Service.class, "__app_joint_default", Module1ServiceImpl.class);
this.routersMap.put(Module2Service.class, "__app_joint_default", Module2ServiceImpl.class);
}
这里我们看到,编译后,AppJoint为我们自动注入了:
- 含有@ModuleSpec的 子moudle Application
- 含有@ServiceProvider的 对外暴露的服务接口
其中moduleApplications是列表,routersMap核心是一个map。那么结合AppJoint的设计思想和源码:
public void onCreate() {
for (Application app : moduleApplications) {
app.onCreate();
}
}
public static synchronized <T> T service(Class<T> routerType, String name) {
T requiredRouter = (T) get().getRouterInstanceMap().get(routerType, name);
if (requiredRouter == null) {
try {
requiredRouter = (T) get().routersMap.get(routerType, name).newInstance();
get().getRouterInstanceMap().put(routerType, name, requiredRouter);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
return requiredRouter;
}
已经很明显了,这里就是将含有注解的关键单元,注入到统一管理类AppJoint中,然后在实际运行的时候,提供遍历初始化,和通过Map找到已经实例化的服务对象。
2.编译后的 App (module:app中的Application)
看完AppJoint,我们在来看下module:app中的Application:
//编译前的代码
@AppSpec
class App : AppBase() {
override fun onCreate() {
super.onCreate()
Log.i("app", "app init is called")
}
}
//编译后的代码
public final class App extends AppBase {
protected void attachBaseContext(Context paramContext) {
super.attachBaseContext(paramContext);
AppJoint.get().attachBaseContext(paramContext);
}
public void onConfigurationChanged(Configuration paramConfiguration) {
super.onConfigurationChanged(paramConfiguration);
AppJoint.get().onConfigurationChanged(paramConfiguration);
}
public void onCreate() {
super.onCreate();
Log.i("app", "app init is called");
AppJoint.get().onCreate();
}
public void onLowMemory() {
super.onLowMemory();
AppJoint.get().onLowMemory();
}
public void onTerminate() {
super.onTerminate();
AppJoint.get().onTerminate();
}
public void onTrimMemory(int paramInt) {
super.onTrimMemory(paramInt);
AppJoint.get().onTrimMemory(paramInt);
}
}
将AppJoint模板中的生命周期和主Application相绑定,进而做到,子Application中的逻辑和主Application中的生命周期同步。
二、AppJoint的实现流程图
这里还是先不谈技术细节,我们先谈谈逻辑,实现上面的代码需要做哪些工作。
先给出流程图:
简单来说,就是找到含有注解的类,然后写到AppJoint中,再把AppJoint写到主Application中。上面的流程和AppJoint的实现流程基本一致,所以下面我们就按照流程图中的流程一步一步的为大家解读源码。
三、找到需要打包的组件
3.1遍历输入
找到打包的组件逻辑上来说并不复杂,只是将参与打包的组件遍历出来就可以了,下面是这段代码可以变成固定写法用于在transform阶段遍历input:
// Maybe contains the AppJoint class to write code into
def maybeStubs = []
// Maybe contains @ModuleSpec, @AppSpec or @ServiceProvider
def maybeModules = []
transformInvocation.inputs.each {
TransformInput input ->
// Find annotated classes in jar
input.jarInputs.each {
JarInput jarInput ->}
// Find annotated classes in dir
input.directoryInputs.each {
DirectoryInput dirInput ->}
}
上面这段代码我们可以看出,在transform阶段我们需要对输入遍历的时候,遍历主要体现在jarInputs和directoryInputs两个集合上,另外还声明了两个集合,分别是可能含有框架组件的集合和可能还有注解的组件集合。
3.2遍历jarInputs
jarInputs:是指以jar包方式参与项目编译的所有本地jar包和远程jar包(此处的jar包包括aar)
input.jarInputs.each { JarInput jarInput ->
if (!jarInput.file.exists()) return
def jarName = jarInput.name
if (jarName == ":core") {
// maybe stub in dev and handle them later
if (maybeStubs.size() == 0) {
maybeStubs.add(jarInput)
}
maybeModules.add(jarInput)
} else if (jarName.startsWith(":")) {
maybeModules.add(jarInput)
} else if (jarName.startsWith("io.github.prototypez:app-joint-core")) {
// find the stub
maybeStubs.clear()
maybeStubs.add(jarInput)
} else {
//固定写法,拿到输出文件夹
def dest = transformInvocation.outputProvider.getContentLocation(
jarName,
jarInput.contentTypes,
jarInput.scopes,
Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
}
}
这个阶段我们的重点在if分支中,首先拿到inputJar集合中的Jar之后,按照命名规则对他们进行区分。
其中 “:core"和"io.github.prototypez:app-joint-core” 代表的是框架module,组件module则使用的规则
if (jarName.startsWith(":"))
进行区分,另外这里最值得主要的是,普通的jar包是不会参与接下来的代码插桩的(最后那几行代码,直接复制到输出区),换句话说,如果你对外提供的功能含有注解又是已jar的形式提供的,那么这些代码是不会生效的!
3.3 遍历directoryInputs
directoryInputs是指以源码的方式参与项目编译的所有目录结构及其目录下的源码文件
input.directoryInputs.each {
DirectoryInput dirInput ->
def outDir = transformInvocation.outputProvider
.getContentLocation(
dirInput.name,
dirInput.contentTypes,
dirInput.scopes,
Format.DIRECTORY)
// dirInput.file is like "build/intermediates/classes/debug"
int pathBitLen = dirInput.file.toString().length()
def callback = { File file ->
if (file.exists()) {
def path = "${file.toString().substring(pathBitLen)}"
if (file.isDirectory()) {
new File(outDir, path).mkdirs()
} else {
def output = new File(outDir, path)
findAnnotatedClasses(file, output)
if (!output.parentFile.exists())
output.parentFile.mkdirs()
output.bytes = file.bytes
}
}
}
if (dirInput.changedFiles != null && !dirInput.changedFiles.isEmpty()) {
dirInput.changedFiles.keySet().each(callback)
}
if (dirInput.file != null && dirInput.file.exists()) {
dirInput.file.traverse(callback)
}
}
此处的流程就是,遍历有变化的文件集合,对这些文件的处理是在前面声明的callback闭包中进行的。
changedFiles是一个Map<File, Status>,里面的key是文件,Status是文件变化所对应的状态。
这里主要做的就是为输出文件做准备,这种准备主要体现在,为输出文件创建对应的文件夹。
四、找到含有注解的类
上面遍历directoryInputs中的callback闭包,里面调用findAnnotatedClasses方法来处理输入的文件。
下面我们看看findAnnotatedClasses的核心业务逻辑。
//第一个参数,我们要处理的类文件,第二个是他要输出的位置。
boolean findAnnotatedClasses(File file, File output)
这个方法大多数的代码都较为简单,这里我们主要看的是他类访问器中的(访问类后,重写访问注解的方法)
//第一个参数返回的是注解的全类名,第二个是是否可访问注解的值
visitAnnotation(String desc, boolean visible)
这里按照程序的顺利,来看在查找注解部分都做了什么。
4.1找到含有@ModuleSpec注解的类
case "Lio/github/prototypez/appjoint/core/ModuleSpec;":
//将有注解的类加入到 之前声明的moduleApplications
addModuleApplication(new AnnotationModuleSpec(cr.className))
//返回一个注解方法访问器 主要是解析 注解中携带的优先级 进行解析赋值
return new AnnotationMethodsVisitor() {
@Override
void visit(String name, Object value) {
//现有队列中根据类名 找到 moduleApplication
def moduleApplication = moduleApplications.find({
it.className == cr.className
})
if (moduleApplication) {
moduleApplication.order = Integer.valueOf(value)
}
super.visit(name, value)
}
}
此处要处理逻辑是:
1.将含有此注解的类的类名作为构造参数创建AnnotationModuleSpec(主要存含有注解的类名,此注解的值,即优先级)实例加入到moduleApplications的集合中。
2.实例化内部类注解访问器AnnotationMethodsVisitor(主要是对日志输出做了切片)。
注解访问器,拿到刚才添加到moduleApplications中的注解类,解析注解的值,将这个值赋给优先级字段。
此处可以理解为,为后面顺序调用子Module的优先级在做准备工作。
4.2找到含有@AppSpec的类
case "Lio/github/prototypez/appjoint/core/AppSpec;":
//将含有AppSpec注解的文件 放到 appApplications Map中 设置需要更新
appApplications[file] = output
needsModification = true
break
这里就更简单了,将含有@AppSpec注解的类,已文件为Key输出地址为value,存到我们之前声明的appApplications map中。此处需要注意的是,我们的主Applicaiton是不一定在主module中的!
needsModification 是整个方法的返回值,这个值,代表这个类是否需要被编辑。只有主Applicaiton是需要被编辑的,这个类是AppJoint要绑定生命周期的主Application类。
4.3找到含有@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 { String interfaceName ->
//使用Tuple2存储 接口名和对应值,根据接口 注入类 压入routerAndImpl中
routerAndImpl[new Tuple2(interfaceName, 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()
}
}
简单来说这一步就是将接口类名和注解对应的值结合作为Key,然后将此类作为Value存放到
def routerAndImpl = new HashMap<Tuple2<String, String>, String>()
结合中。这里需要注意的是visitEnd()这里,如果存在没有值的注解,也会将这个类放到routerAndImpl 结合中,只不过此时的名称为
public static final String SERVICE_PROVIDER_DEFAULT_NAME = "__app_joint_default";
的默认名称。
4.4总结findAnnotatedClasses
简单来说这个方法就是找到我们所有包含框架内注解的类,将他们放到各自的结合中,为后面插桩做准备。
五、找到Jar中可能包含的注解类
在解析jarInput和dirctoryInput这两个单元的时候,他们的不同是dirctoryInput以及可以拿到.class文件了,那么此时可以直接访问,判断其是是否是组件化过程中要用到的类;而jarInput这个单元,知识找到了对应的module,并没有对其内部的类做出来。
之所以有这样的区分,是因为对jar的处理是需要先解压,解压后才能拿到他里面的.class。
下面我们来看下对jar包的处理。
maybeModules.each { JarInput jarInput ->
def repackageAction = traversalJar(
transformInvocation,
jarInput,
{ File outputFile, File input ->
return findAnnotatedClasses(input, outputFile)
}
)
if (repackageAction) repackageActions.add(repackageAction)
}
这段的难点是
Closure traversalJar(TransformInvocation transformInvocation, JarInput jarInput, Closure closure)
此段代码是对Groovy中闭包语法一段较好的展示,他充分的体现了闭包的灵活性!
下面讲下traversalJar方法处理的逻辑:
- Jar解压,拿到.class文件
- 将解压的文件,复制到需要打包的文件夹下
- 处理这些文件,此处是方法参数闭包的执行逻辑。闭包实际处理方法是findAnnotatedClasses,这个方法返回的是这个类是否需要做,编辑处理,如果需要编辑,那么他的打包延迟(编辑后才能打包)。
- 处理完成之后,生成一段重打包的闭包代码段。
- 如果不需要延迟打包,那么直接打包什么都不返回;需要延迟打包的话,返回的是打包代码段。
最后,此方法由于闭包的原因,他和其他模块并没有耦合,所以可以直接提取出来,作为工具类。
六、找到AppJoint
maybeStubs.each { JarInput jarInput ->
def repackageAction = traversalJar(
transformInvocation,
jarInput,
{ File outputFile, File input ->
return findAppJointClass(input, outputFile)
}
)
if (repackageAction) repackageActions.add(repackageAction)
}
有了前面的基础,看这段代码就轻松多了。这里的操作是找到AppJoint类,然后将重打包代码段,加到重打包集合中。
findAppJointClass(input, outputFile)
{
if (name == "io/github/prototypez/appjoint/AppJoint")
}
此方法就不不在贴出源码了,此处就是通过类访问器,然后看类名是否是框架中指定的AppJoint路径。
七、将代码写入到AppJoint中
经过上面的步骤,我们重要找全了,我们要处理的类了。
接下来就是代码插桩了。
这里的步骤是:
1.先读入文件
2.然后通过类访问器找到构造函数
3.通过方法访问器访问构造函数。
4.重写visitInsn方法,将子Applicaiton代码和对外接口插入到构造函数中。
这一步的两个方法:
void insertApplicationAdd(String applicationName)
void insertRoutersPut(Tuple2<String, String> router, String impl)
里面放的都是字节码代码,这些代码大家可能感觉写起来很难受,这里我们可以通过,ASM插件来生成。
代码生成插件。
添加子Applicaiton到构造函数中的语句:
mv.visitInsn(Opcodes.DUP)
//执行new 操作
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, applicationName, "<init>", "()V", false)
//执行add操作
mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/List", "add", "(Ljava/lang/Object;)Z", true)
mv.visitInsn(Opcodes.POP)
将接口方法,注入到方法Map中给的语句。
mv.visitLdcInsn(Type.getObjectType(router.first))
mv.visitLdcInsn(router.second)
mv.visitLdcInsn(Type.getObjectType(impl))
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "io/github/prototypez/appjoint/util/BinaryKeyMap",
"put",
"(Ljava/lang/Object;
Ljava/lang/Object;
Ljava/lang/Object;)V",
true)
将类名注入到Map中。
八、将代码写到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 ClassVisitorApplication(writer)
reader.accept(visitor, 0)
output.bytes = writer.toByteArray()
inputStream.close()
}
这里面的逻辑类似,还是通过类访问器,将代码插入到主Applicaiton中。
首先找到需要绑定到生命周期上的方法:
switch (name + desc) {
case "onCreate()V":
onCreateDefined = true
return new MethodVisitorAddCallAppJoint(methodVisitor, "onCreate", "()V", false, false)
case "attachBaseContext(Landroid/content/Context;)V":
attachBaseContextDefined = true
return new MethodVisitorAddCallAppJoint(methodVisitor, "attachBaseContext", "(Landroid/content/Context;)V", true, false)
case "onConfigurationChanged(Landroid/content/res/Configuration;)V":
onConfigurationChangedDefined = true
return new MethodVisitorAddCallAppJoint(methodVisitor, "onConfigurationChanged", "(Landroid/content/res/Configuration;)V", true, false)
case "onLowMemory()V":
onLowMemoryDefined = true
return new MethodVisitorAddCallAppJoint(methodVisitor, "onLowMemory", "()V", false, false)
case "onTerminate()V":
onTerminateDefined = true
return new MethodVisitorAddCallAppJoint(methodVisitor, "onTerminate", "()V", false, false)
case "onTrimMemory(I)V":
onTrimMemoryDefined = true
return new MethodVisitorAddCallAppJoint(methodVisitor, "onTrimMemory", "(I)V", false, true)
}
这里使用的都是一个方法MethodVisitorAddCallAppJoint。
最后进行插入:
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)
}
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "io/github/prototypez/appjoint/AppJoint", name, desc, false)
九、重打包
还记4.2中说的重打包吗?当以上,代码编辑输出完毕之后,就会进行压缩输出,作为下一个单元的输入。
repackageActions.each { Closure action -> action.call() }
总结:总的来说,AppJoint的源码拆解之后阅读难度还是不大的。不过需要较多的Gradle和Groovy的基础知识。另外直接读源码的时候可能由于文件整体交代,带来一定给的困难,我们可以将一些内部类和工具型的方法提炼出来,以此来降低核心业务类的代码行数,进而降低阅读难度。