浅谈Android ASM自动埋点方案实践
作者:C6C 发布时间:2021-10-19 22:01:18
这段时间想到一个有趣的功能,就是在Android的代码编译期间进行一些骚操作,来达到一些日常情境下难以实现的功能,比如监听应用中的所有onClick点击时间,或者监听某些方法的运行耗时,如果在代码中一个方法一个方法修改会很蛋疼,所以想通过Gradle插件来实现在应用的编译期间进行代码插入的功能。
1、AOP的概念
其实这已经涉及到AOP(Aspect Oriented Programming),即面向切面编程,在编译期间对代码进行动态管理,以达到统一维护的目的。
AOP切面
举个栗子,Android开发我们都知道,在项目越来越大的时候,应用可能被分解为多个模块,如果你要往所有模块的方法里头加一句‘我是大傻叼'的Toast,那是不是得跪。所以最好的方式是想办法在编译的时候拿到所有方法,往方法里头怼一个Toast,这样还不会影响到运行期间性能。
2、Transform
Android打包流程
如图所示是Android打包流程,.java文件->.class文件->.dex文件,只要在红圈处拦截住,拿到所有方法进行修改完再放生就可以了,而做到这一步也不难,Google官方在Android Gradle的1.5.0 版本以后提供了 Transfrom API, 允许第三方 Plugin 在打包 dex 文件之前的编译过程中操作 .class 文件,我们做的就是实现Transform进行.class文件遍历拿到所有方法,修改完成对原文件进行替换。
/**
* 自动埋点追踪,遍历所有文件更换字节码
*/
public class AutoTransform extends Transform {
@Override
String getName() {
return "AutoTrack"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
public void transform(
@NonNull Context context,
@NonNull Collection<TransformInput> inputs,
@NonNull Collection<TransformInput> referencedInputs,
@Nullable TransformOutputProvider outputProvider,
boolean isIncremental) throws IOException, TransformException, InterruptedException {
//此处会遍历所有文件
/**遍历输入文件*/
inputs.each { TransformInput input ->
/**
* 遍历jar
*/
input.jarInputs.each { JarInput jarInput ->
...
}
/**
* 遍历目录
*/
input.directoryInputs.each { DirectoryInput directoryInput ->
...
}
}
}
3、Gradle插件实现
通过Transform提供的api可以遍历所有文件,但是要实现Transform的遍历操作,得通过Gradle插件来实现,关于Gradle插件的知识可以看相关博客,也可以直接看博主的项目 Luffy 。编写Gradle插件可能需要一点Goovy知识,具体编写直接用java语言写也可以,Goovy是完全兼容java的,只截取插件入口部分实现PluginEntry.groovy
class PluginEntry implements Plugin<Project> {
@Override
void apply(Project project) {
...
//使用Transform实行遍历
def android = project.extensions.getByType(AppExtension)
registerTransform(android)
...
}
def static registerTransform(BaseExtension android) {
AutoTransform transform = new AutoTransform()
android.registerTransform(transform)
}
4、字节码编写
完成上面的操作以后就剩下一件事了,那就是拿到.class文件了,大家都知道.class文件是字节码格式的,操作起来难度是相当于大的,所以需要一个字节码操作库来减轻难度,那就是ASM了。
4.1、ASM简介
ASM 可以直接产生二进制的class 文件,也可以在增强既有类的功能。Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。
4.2、具体使用ASM
ASM框架中的核心类有以下几个:
ClassReader:该类用来解析编译过的class字节码文件。
ClassWriter:该类用来重新构建编译后的类,比如说修改类名、属性以及方法,甚至可以生成新的类的字节码文件。
ClassVisitor:主要负责 “拜访” 类成员信息。其中包括标记在类上的注解,类的构造方法,类的字段,类的方法,静态代码块。
AdviceAdapter:实现了MethodVisitor接口,主要负责 “拜访” 方法的信息,用来进行具体的方法字节码操作。
ClassVisitor的全部方法如下,按一定的次序来遍历类中的成员。
ClassVisitor的全部方法如下,按一定的次序来遍历类中的成员。
ClassVisitor全部api
在ClassVisitor中根据你的条件进行判断,满足条件的类才会修改其中方法,比如要统计点击事件的话,需要实现View$OnClickListener接口的类才会遍历其中的方法进行操作。
class AutoClassVisitor extends ClassVisitor {
AutoClassVisitor(final ClassVisitor cv) {
super(Opcodes.ASM4, cv)
}
@Override
void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
//进行需要满足类的条件过滤
...
super.visit(version, access, name, signature, superName, interfaces)
}
@Override
void visitInnerClass(String name, String outerName, String innerName, int access) {
// 内部类信息
...
super.visitInnerClass(name, outerName, innerName, access)
}
@Override
MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
// 拿到需要修改的方法,执行修改操作
MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions)
MethodVisitor adapter = null
...
adapter = new AutoMethodVisitor(methodVisitor, access, name, desc)
...
return methodVisitor
}
@Override
void visitEnd() {
//类中成员信息遍历介绍
...
super.visitEnd()
}
}
在MethodVisitor中根据对已经拿到的方法进行修改了。
MethodVisitor adapter = new AutoMethodVisitor(methodVisitor, access, name, desc) {
boolean isAnnotation = false
@Override
protected void onMethodEnter() {
super.onMethodEnter()
//进入方法时可以插入字节码
...
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode)
//退出方法前可以插入字节码
...
}
/**
* 需要通过注解的方式加字节码才会重写这个方法来进行条件过滤
*/
@Override
AnnotationVisitor visitAnnotation(String des, boolean visible) {
...
return super.visitAnnotation(des, visible)
}
}
5、实战演练
来源:https://www.jianshu.com/p/9039a3e46dbc


猜你喜欢
- 源码学习的好处不用多说,Mybatis源码量少、逻辑简单,将写个系列文章来学习。SqlSessionMybatis的使用入口位于org.ap
- 项目介绍java 开发中,参数校验是非常常见的需求。但是 hibernate-validator 在使用过程中,依然会存在一些问题。vali
- 我们在用maven构建java项目时,最常用的打包命令有mvn package、mvn install、deploy,这三个命令都可完成打j
- Mapper 就是“映射”的意思,Mapper 文件时 Mybatis 中的 SQL 语句的配置文件
- 分转元private String fenToYuan(String amount){ NumberFormat format
- 本课程的目标是帮你更有效的使用Java。其中讨论了一些高级主题,包括对象的创建、并发、序列化、反射以及其他高级特性。本课程将为你的精通Jav
- 初次遇见 native是在 java.lang.Object 源码中的一个hashCode方法:public native int hash
- SpringCloud feign无法注入接口接口:package cn.mn.app.service;import org.springf
- 人机猜拳小游戏,只要用到的java面向对象的思维模式。本游戏中有游戏玩家和计算机两个参与者。玩家和计算机都有的属性是姓名和得分。共分为4个类
- Thread dumps(线程转储)能帮助我们判断 CPU 峰值、死锁、内存异常、应用反应迟钝、响应时间变长和其他系统问题。一些在线的分析工
- springboot项目部署平时我们在部署springboot打成jar方式部署得时候,大多数都会编写启动脚本,脚本有很多种写法,但大多数意
- 这篇文章主要介绍了Mybatis 缓存原理及失效情况解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的
- 以下通过3个知识点给大家讲解了上拉加载和下拉刷新的Fragment实现的方法,在对每个知识点介绍了一下用法。1.效果预览1.1.这个首页就是
- 1、背景本系统调用外围系统接口(http+json),但是发现有时外围系统服务不太稳定,有时候会出现返回一串xml或者gateway bad
- 本文实例讲述了JAVA设计模式之建造者模式定义与用法。分享给大家供大家参考,具体如下:建造者模式:将复杂对象的构造与它的实现相分离,让相同的
- 概述RocketMQ 支持发送延迟消息,但不支持任意时间的延迟消息的设置,仅支持内置预设值的延迟时间间隔的延迟消息;预设值的延迟时间间隔为:
- 一、需求描述拼图是一款益智类经典游戏了,本游戏学习了一些前辈们的经验,整体来说讲,将图片用切图工具进行切割,监听用户手指滑动事件,当用户对凌
- 路径分隔符:Windows下是“\”unix|linux下是“/”考虑到程序的可移植性,创建文件时建议大家选用"/",因
- 1、redis的几种常见客户端:Jedis:是Redis的Java实现客户端,提供了比较全面的Redis命令的支持;Redisson:实现了
- Spring Boot 2.x 已经发布了很久,现在 Spring Cloud 也发布了 基于 Spring Boot 2.x 的 Finc