正文
首先,现在世面上的项目基本上都是N多个module并行开发很容易就会出现moduleA想跳转到moduleB某一个界面去如果你没有把moduleB在对应的build.gradle中配置的话,AS就会友好的提示你跳不过去,这时候就需要一个路由来分发跳转操作了。 其次,随着时间的慢慢迭代发现需求功能已经写完了,慢慢开始要各种优化了,常见的优化是速度优化自然而然就需要查看方法的耗时情况,那么解放双手的时候就需要一个正确的姿势来统计方法耗时。
附上Github项目地址:
github.com/Neacy/Neacy…
思路
1.采用注解(Annotation)在要跳转的界面和需要统计的地方加上相对应的协议。
2.用groovy语言实现一个Transform的gradle插件来解析相对应的注解。
3.采用ASM框架生成相对应的代码主要是写入或者插入class的字节码。
4.路由框架中需要反射拿到ASM生成的路由表然后代码中调用从而实现跳转。
==============带着这些思路接下来就是拼命写代码了………….
先上两个用到的注释,注释还是比较简单的分分钟写完,需要注意的是我们是class操作所以要选
@Retention(RetentionPolicy.CLASS)
/**
* 用于标记协议
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface NeacyProtocol {
String value();
}
/**
* 用于标记方法耗时
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface NeacyCost {
String value();
}
换个姿势写一个gradle插件,如何写主要参考区长的
blog.csdn.net/sbsujjbcy/a…
,按着步骤就好,假设我们看完了并设置好了那么就有一个雏形了:
public class NeacyPlugin extends Transform implements Plugin<Project> {
private static final String PLUGIN_NAME = "NeacyPlugin"
private Project project
@Override
void apply(Project project) {
this.project = project
def android = project.extensions.getByType(AppExtension);
android.registerTransform(this)
}
@Override
String getName() {
return PLUGIN_NAME
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return true
}
@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
}
}
我们要做的就是在transform中扫描相对应的注解并用ASM写入class字节码。我们知道TransformInput对应的有两种可能性一种是目录 一种是jar包所以要分开遍历:
inputs.each { TransformInput input ->
input.directoryInputs.each { DirectoryInput directoryInput ->
if (directoryInput.file.isDirectory()) {
println "==== directoryInput.file = " + directoryInput.file
directoryInput.file.eachFileRecurse { File file ->
// ...对目录进行插入字节码
}
}
//处理完输入文件之后,要把输出给下一个任务
def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}
input.jarInputs.each { JarInput jarInput ->
println "------=== jarInput.file === " + jarInput.file.getAbsolutePath()
File tempFile = null
if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
// ...对jar进行插入字节码
}
/**
* 重名输出文件,因为可能同名,会覆盖
*/
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
//处理jar进行字节码注入处理
def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
}
}
对于代码中陌生的代码风格可以查阅这篇文章:
blog.csdn.net/innost/arti…
保证看完之后什么都懂了,好文强烈推荐。
然后,最麻烦的就是字节码注入的部分功能了,先看一下主要的调用代码:
ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
NeacyAsmVisitor classVisitor = new NeacyAsmVisitor(Opcodes.ASM5, classWriter)
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
调用的主要代码量还是比较少的,主要是自定义一个ClassVisitor。在每一个ClassVisitor中它会分别
visitAnnotation
和
visitMethod
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
NeacyLog.log("=====---------- NeacyAsmVisitor visitAnnotation ----------=====");
NeacyLog.log("=== visitAnnotation.desc === " + desc);
AnnotationVisitor annotationVisitor = super.visitAnnotation(desc, visible);
if (Type.getDescriptor(NeacyProtocol.class).equals(desc)) {// 如果注解不为空的话
mProtocolAnnotation = new NeacyAnnotationVisitor(Opcodes.ASM5, annotationVisitor, desc);
return mProtocolAnnotation;
}
return annotationVisitor;
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
NeacyLog.log("=====---------- visitMethod ----------=====");
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
mMethodVisitor = new NeacyMethodVisitor(Opcodes.ASM5, mv, access, name, desc);
return mMethodVisitor;
}
在
visitAnnotation
中就是我们扫描相对应的注解的地方类似
Type.getDescriptor(NeacyProtocol.class).equals(desc)
判断是否是我们需要的处理的注解,像这里我们主要处理前面定义好的注解
NeacyProtocol
和
NeacyCost
两个注解就好。
这里我要展示一下注入成功之后的class中的代码是什么模样: 生成好的路由表:
注入成功的耗时代码:
看一眼logcat打印出来的耗时时间,感觉离成功不远了。可是是怎么注入的呢,首先要看一眼class结构 这里推荐使用IntelliJ IDEA然后装个插件叫Bytecode outline这里距离看一眼耗时的生成的class文件字节码。
左边是我们对应的java文件,右边是编译之后生成的class字节码。对于右边一般是看不懂的但是神奇的ASM就能看的懂而且提供了一系列的api供我们调用,我们只要对着编写就好了,按照上面的操作很大程度上减少了巨大的工作难度,再次感谢
巴掌大神
。
所以我们路由框架的代码字节生成,我把整个类贴上来吧代码量不是很多:
/**
* 生成路由class文件
*/
public class NeacyRouterWriter implements Opcodes {
public byte[] generateClass(String pkg, HashMap<String, String> metas) {
ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
// 生成class类标识
cw.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, pkg, null, "java/lang/Object", null);
// 声明一个静态变量
fv = cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "map", "Ljava/util/HashMap;", "Ljava/util/HashMap<Ljava/lang/String;Ljava/lang/String;>;", null);
fv.visitEnd();
// 默认的构造函数<init>
mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(1, 1);
// 生成一个getMap方法
mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "getMap", "()Ljava/util/HashMap;", "()Ljava/util/HashMap<Ljava/lang/String;Ljava/lang/String;>;", null);
mv.visitCode();
mv.visitFieldInsn(Opcodes.GETSTATIC, pkg, "map", "Ljava/util/HashMap;");
mv.visitInsn(Opcodes.ARETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
// 将扫描到的注解生成相对应的路由表 主要写在静态代码块中
mv = cw.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
mv.visitCode();
mv.visitTypeInsn(Opcodes.NEW, "java/util/HashMap");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/util/HashMap", "<init>", "()V", false);
mv.visitFieldInsn(Opcodes.PUTSTATIC, pkg, "map", "Ljava/util/HashMap;");
for (Map.Entry<String, String> entrySet : metas.entrySet()) {
String key = entrySet.getKey();
String value = entrySet.getValue();
NeacyLog.log("=== key === " + key);
NeacyLog.log("=== value === " + value);
mv.visitFieldInsn(Opcodes.GETSTATIC, pkg, "map", "Ljava/util/HashMap;");
mv.visitLdcInsn(key);
mv.visitLdcInsn(value);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/util/HashMap", "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", false);
mv.visitInsn(Opcodes.POP);
}
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(3, 0);
mv.visitEnd();
cw.visitEnd();
return cw.toByteArray();
}
}
然后对方法耗时的进行的代码插入主要代码有:
@Override
protected void onMethodEnter() {
if (isInject) {
NeacyLog.log("====== 开始插入方法 = " + methodName);
/**
NeacyCostManager.addStartTime("xxxx", System.currentTimeMillis());
*/
mv.visitLdcInsn(methodName);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "neacy/router/NeacyCostManager", "addStartTime", "(Ljava/lang/String;J)V", false);
}
}
@Override
protected void onMethodExit(int opcode) {
if (isInject) {
/**
NeacyCostManager.addEndTime("xxxx", System.currentTimeMillis());
*/
mv.visitLdcInsn(methodName);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "neacy/router/NeacyCostManager", "addEndTime", "(Ljava/lang/String;J)V", false);
/**
NeacyCostManager.startCost("xxxx");
*/
mv.visitLdcInsn(methodName);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "neacy/router/NeacyCostManager", "startCost", "(Ljava/lang/String;)V", false);
NeacyLog.log("==== 插入结束 ====");
}
}
基本上这样子相对应的路由表相对应的代码插入都写完,然后只需要在gradle插件中进行调用一下即可,而对于遍历目录的时候没有什么难点就是直接覆盖当前class即可:
if (isDebug) {// 只有Debug才进行扫描const耗时
// 扫描耗时注解 NeacyCost
byte[] bytes = classWriter.toByteArray()
File destFile = new File(file.parentFile.absoluteFile, name)
project.logger.debug "========== 重新写入的位置->lastFilePath = " + destFile.getAbsolutePath()
FileOutputStream fileOutputStream = new FileOutputStream(destFile)
fileOutputStream.write(bytes)
fileOutputStream.close()
}
而对于jar遍历的时候需要做的是先拆jar然后注入代码完成之后需要再生产一个jar,所以我们需要创建一个临时地址来存放新的jar。
if (isDebug) {
// 将jar包解压后重新打包的路径
tempFile = new File(jarInput.file.getParent() + File.separator + "neacy_const.jar")
if (tempFile.exists()) {
tempFile.delete()
}
fos = new FileOutputStream(tempFile)
jarOutputStream = new JarOutputStream(fos)
// 省略一些代码....
ZipEntry zipEntry = new ZipEntry(entryName)
jarOutputStream.putNextEntry(zipEntry)
// 扫描耗时注解 NeacyCost
byte[] bytes = classWriter.toByteArray()
jarOutputStream.write(bytes)
}
这里有必要插入一个插件配置,因为对于方法耗时统计只要开发的时候debug模式下使用就好其他模式禁止使用了,这就是为什么上面有
if(debugOn)
的判断。