编写Gradle插件配合ASM框架实战
Posted 郭霖
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了编写Gradle插件配合ASM框架实战相关的知识,希望对你有一定的参考价值。
昨日11时左右,美团服务器出现大面积崩溃,外卖订单付款出现延迟,部分用户付款后系统仍提示尚未付款;团购页面内容也无法正常显示。对此美团方面负责人回应称:因技术原因导致平台部分订单出现支付故障,经紧急修复后,现已陆续恢复,由此给用户带来的不便我们深感抱歉。针对此次故障受到影响的订单,已在陆续解决中,我们将确保故障期间用户权益不会受到任何影响。
明天就是周六啦,提前祝大家周末愉快!
本篇文章来自 左手木亽 的投稿。分享了通过编写Gradle插件来配合ASM框架的使用,具有很强的实用价值,希望对大家有所帮助!
http://blog.csdn.net/neacy_zz
首先,现在世面上的项目基本上都是N多个module并行开发很容易就会出现 moduleA 想跳转到 moduleB 某一个界面去如果你没有把 moduleB 在对应的 build.gradle 中配置的话,AS就会友好的提示你跳不过去,这时候就需要一个路由来分发跳转操作了。
其次,随着时间的慢慢迭代发现需求功能已经写完了,慢慢开始要各种优化了,常见的优化是速度优化自然而然就需要查看方法的耗时情况,那么解放双手的时候就需要一个正确的姿势来统计方法耗时。
https://github.com/Neacy/NeacyPlugin
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插件,如何写主要参考区长:
http://blog.csdn.net/sbsujjbcy/article/details/50782830
按着步骤就好,假设我们看完了并设置好了那么就有一个雏形了:
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)
}
}
对于代码中陌生的代码风格可以查阅这篇文章:
http://blog.csdn.net/innost/article/details/48228651
保证看完之后什么都懂了,好文强烈推荐。
然后,最麻烦的就是字节码注入的部分功能了,先看一下主要的调用代码:
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供我们调用,我们只要对着编写就好了,按照上面的操作很大程度上减少了巨大的工作难度,再次感谢巴掌大神(http://www.wangyuwei.me)。
所以我们路由框架的代码字节生成,我把整个类贴上来吧代码量不是很多:
ddd
/** * 生成路由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()
}
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) 的判断。先定义一个Extension:
/** * 配置 */
public class NeacyExtension {
boolean debugOn = true public NeacyExtension(Project project) {}
}
然后在 transfrom 中进行读取:
void apply(Project project) {
this.project = project
project.extensions.create("neacy", NeacyExtension, project)
def android = project.extensions.getByType(AppExtension);
android.registerTransform(this)
project.afterEvaluate {
def extension = project.extensions.findByName("neacy") as NeacyExtension
def debugOn = extension.debugOn
project.logger.error '========= debugOn = ' + debugOn
project.android.applicationVariants.each { varient ->
project.logger.error '======== varient Name = ' + varient.name if (varient.name.contains(DEBUG) && debugOn) {
isDebug = true
}
}
}
}
最后在 build.gradle 中进行配置就可以愉快的使用了..
apply plugin: com.neacy.plugin.NeacyPlugin
neacy {
debugOn true
}
当然更多的代码可以参考 demo 的 git库 了解更多。
最后路由库要怎么让代码调用呢,这就是前面讲到的反射因为是编译生成的 class 无法直接调用唯有反射大法,反射会稍微影响性能所以我们一开始就直接做好这些初始化工作就可以了。
/** * 初始化路由 */
public void initRouter() {
try {
Class clazz = Class.forName("com.neacy.router.NeacyProtocolManager");
Object newInstance = clazz.newInstance();
Field field = clazz.getField("map");
field.setAccessible(true);
HashMap<String, String> temps = (HashMap<String, String>) field.get(newInstance);
if (temps != null && !temps.isEmpty()) {
mRouters.putAll(temps);
Log.w("Jayuchou", "=== mRouters.Size === " + mRouters.size());
}
} catch (Exception e) {
e.printStackTrace();
}
}
/** * 根据协议找寻路由实现跳转 */
public void startIntent(Context context, String protocol, Bundle bundle) {
if (TextUtils.isEmpty(protocol)) return;
String protocolValue = mRouters.get(protocol);
try {
Class destClass = Class.forName(protocolValue);
Intent intent = new Intent(context, destClass);
if (bundle != null) {
intent.putExtras(bundle);
}
context.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
}
}
最最最后,怎么使用呢?
@NeacyProtocol("Neacy://app/MainActivity")
public class MainActivity extends AppCompatActivity {
@Override
@NeacyCost("MainActivity.onCreate")
protected void onCreate(Bundle savedInstanceState) {}
根据上面的注解标识之后,方法耗时就已经完成当然路由还需要哪里需要哪里传协议进行跳转就好了,当然也是一句代码的事。
NeacyRouterManager.getInstance().startIntent(TestActivity.this, "Neacy://neacymodule/NeacyModuleActivity", bundle);
这样一个完整的路由框架以及方法耗时统计V1.0版本就打完收工了。
Thanks…………
感谢巴神的文章:
http://www.wangyuwei.me/2017/03/05/ASM实战统计方法耗时
欢迎长按下图 -> 识别图中二维码
以上是关于编写Gradle插件配合ASM框架实战的主要内容,如果未能解决你的问题,请参考以下文章
Gradle 插件 + ASM 实战 - JVM 虚拟机加载 Class 原理
Gradle 插件 + ASM 实战 - JVM 虚拟机加载 Class 原理
Gradle 插件 + ASM 实战 - JVM 虚拟机加载 Class 原理
Gradle 插件 + ASM 实战 - JVM 虚拟机加载 Class 原理