最通俗易懂的字节码插桩实战(Gradle + ASM)

input.jarInputs.each { JarInput jarInput ->

handleJarInput(jarInput, outputProvider)

}

}

}

/**

  • 处理文件目录下的class文件

*/

static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {

//是否是目录

if (directoryInput.file.isDirectory()) {

//列出目录所有文件(包含子文件夹,子文件夹内文件)

directoryInput.file.eachFileRecurse { File file ->

def name = file.name

if (filterClass(name)) {

ClassReader classReader = new ClassReader(file.bytes)

ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)

ClassVisitor classVisitor = new BuryPointVisitor(classWriter)

classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)

byte[] code = classWriter.toByteArray()

FileOutputStream fos = new FileOutputStream(file.parentFile.absolutePath + File.separator + name)

fos.write(code)

fos.close()

}

}

}

//文件夹里面包含的是我们手写的类以及R.class、BuildConfig.class以及R$XXX.class等

// 获取output目录

def dest = outputProvider.getContentLocation(

directoryInput.name,

directoryInput.contentTypes,

directoryInput.scopes,

Format.DIRECTORY)

//这里执行字节码的注入,不操作字节码的话也要将输入路径拷贝到输出路径

FileUtils.copyDirectory(directoryInput.file, dest)

}

/**

  • 处理jar文件,一般是第三方依赖库jar文件

*/

static void handleJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {

if (jarInput.file.getAbsolutePath().endsWith(“.jar”)) {

//重名名输出文件,因为可能同名,会覆盖

def jarName = jarInput.name

def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())

if (jarName.endsWith(“.jar”)) {

jarName = jarName.substring(0, jarName.length() - 4)

}

JarFile jarFile = new JarFile(jarInput.file)

Enumeration enumeration = jarFile.entries()

File tmpFile = new File(jarInput.file.getParent() + File.separator + “temp.jar”)

//避免上次的缓存被重复插入

if (tmpFile.exists()) {

tmpFile.delete()

}

JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))

//用于保存

while (enumeration.hasMoreElements()) {

JarEntry jarEntry = (JarEntry) enumeration.nextElement()

String entryName = jarEntry.getName()

ZipEntry zipEntry = new ZipEntry(entryName)

InputStream inputStream = jarFile.getInputStream(jarEntry)

//插桩class

if (filterClass(entryName)) {

//class文件处理

jarOutputStream.putNextEntry(zipEntry)

ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))

ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)

ClassVisitor classVisitor = new BuryPointVisitor(classWriter)

classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)

byte[] bytes = classWriter.toByteArray()

jarOutputStream.write(bytes)

} else {

jarOutputStream.putNextEntry(zipEntry)

jarOutputStream.write(IOUtils.toByteArray(inputStream))

}

jarOutputStream.closeEntry()

}

//结束

jarOutputStream.close()

jarFile.close()

//生成输出路径 + md5Name

def dest = outputProvider.getContentLocation(

jarName + md5Name,

jarInput.contentTypes,

jarInput.scopes,

Format.JAR)

FileUtils.copyFile(tmpFile, dest)

tmpFile.delete()

}

}

/**

  • 检查class文件是否需要处理

  • @param fileName

  • @return

*/

static boolean filterClass(String name) {

return (name.endsWith(“.class”)

&& !name.startsWith(“R$”)

&& “R.class” != name

&& “BuildConfig.class” != name)

}

}

3、BuryPointVisitor

通过visitMethod拿到方法进行修改

class BuryPointVisitor extends ClassVisitor {

…省略中间非关键代码,详细请到github中查看…

/**

  • 扫描类的方法进行调用

  • @param access 修饰符

  • @param name 方法名字

  • @param descriptor 方法签名

  • @param signature 泛型信息

  • @param exceptions 抛出的异常

  • @return

*/

@Override

MethodVisitor visitMethod(int methodAccess, String methodName, String methodDescriptor, String signature, String[] exceptions) {

MethodVisitor methodVisitor = super.visitMethod(methodAccess, methodName, methodDescriptor, signature, exceptions)

return new BuryPointMethodVisitor(methodVisitor, methodAccess, methodName, methodDescriptor)

}

}

4、BuryPointMethodVisitor

终于到了本次文章的核心代码了。

visitAnnotation在扫描到注解时调用。我们通过 descriptor 来判断是否是需要埋点的注解,如果是则保存注解参数和对应的方法名称,等到onMethodEnter时进行代码插入。

visitInvokeDynamicInsn在描到lambda表达式时调用,bootstrapMethodArguments[0] 得到方法描述,通过 name + desc 判断当前lambda表达式是否是需要的埋点的方法,如果是则保存lambda方法名称,等到onMethodEnter时进行代码插入。

onMethodEnter在进入方法时调用,这里就是我们插入代码的地方了。通过 methodName + methodDescriptor 判断当前方法是否是需要的埋点的方法,如果是则插入埋点方法。

######——重点,要考,画起来——

  1. mv.visitVarInsn(store, i + methodArgumentSize + 1) 为什么要 +methodArgumentSize 呢?

答:简单的说就是我们通过visitLdcInsn把注解参数压入到局部变量表中,而局部变量表(Local Variable Table)是一组变量值存储空间,用于存放 方法参数和方法内定义的局部变量,所以在取值的时候要方法参数的数量。 2.int slotIndex = isStatic(methodAccess) ? 0 : 1 为什么 static 方法是0开始计算? 答:普通方法的局部变量表第一个参数是this(当前对象的引用)所以要加1。

class BuryPointMethodVisitor extends AdviceAdapter {

int methodAccess

String methodName

String methodDescriptor

BuryPointMethodVisitor(MethodVisitor methodVisitor, int access, String name, String desc) {

super(Opcodes.ASM7, methodVisitor, access, name, desc)

this.methodAccess = access

this.methodName = name

this.methodDescriptor = desc

}

/**

  • 扫描类的注解时调用

  • @param descriptor 注解名称

  • @param visible

  • @return

*/

@Override

AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {

AnnotationVisitor annotationVisitor = super.visitAnnotation(descriptor, visible)

// 通过descriptor判断是否是需要扫描的注解

BuryPointCell cell = StatisticPlugin.HOOKS.get(descriptor)

if (cell != null) {

BuryPointCell newCell = cell.clone()

return new BuryPointAnnotationVisitor(annotationVisitor) {

@Override

void visit(String name, Object value) {

super.visit(name, value)

// 保存注解的参数值

newCell.annotationData.put(name, value)

}

@Override

void visitEnd() {

super.visitEnd()

newCell.methodName = methodName

newCell.methodDesc = methodDescriptor

StatisticPlugin.HOOKS.put(newCell.methodName + newCell.methodDesc, newCell)

}

}

}

return annotationVisitor

}

/**

  • lambda表达式时调用

  • @param name

  • @param descriptor

  • @param bootstrapMethodHandle

  • @param bootstrapMethodArguments

*/

@Override

void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object… bootstrapMethodArguments) {

super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments)

String desc = (String) bootstrapMethodArguments[0]

BuryPointCell cell = StatisticPlugin.HOOKS.get(name + desc)

if (cell != null) {

String parent = Type.getReturnType(descriptor).getDescriptor()

if (parent == cell.methodParent) {

Handle handle = (Handle) bootstrapMethodArguments[1]

BuryPointCell newCell = cell.clone()

newCell.isLambda = true

newCell.methodName = handle.getName()

newCell.methodDesc = handle.getDesc()

StatisticPlugin.HOOKS.put(newCell.methodName + newCell.methodDesc, newCell)

}

}

}

/**

  • 方法进入时调用

*/

@Override

protected void onMethodEnter() {

super.onMethodEnter()

BuryPointCell buryPointCell = StatisticPlugin.HOOKS.get(methodName + methodDescriptor)

if (buryPointCell != null && !buryPointCell.isMethodExit) {

onMethod(buryPointCell)

}

}

/**

  • 方法退出前调用

*/

@Override

protected void onMethodExit(int opcode) {

BuryPointCell buryPointCell = StatisticPlugin.HOOKS.get(methodName + methodDescriptor)

if (buryPointCell != null && buryPointCell.isMethodExit) {

onMethod(buryPointCell)

}

super.onMethodExit(opcode)

}

private void onMethod(BuryPointCell cell) {

// 获取方法参数

Type methodType = Type.getMethodType(methodDescriptor)

Type[] methodArguments = methodType.getArgumentTypes()

int methodArgumentSize = methodArguments.size()

if (cell.isAnnotation) { // 遍历注解参数并赋值给采集方法

def entrySet = cell.annotationParams.entrySet()

def size = entrySet.size()

for (int i = 0; i < size; i++) {

def key = entrySet[i].getKey()

if (key == “this”) {

mv.visitVarInsn(Opcodes.ALOAD, 0)

} else {

def load = entrySet[i].getValue()

def store = getVarInsn(load)

mv.visitLdcInsn(cell.annotationData.get(key))

mv.visitVarInsn(store, i + methodArgumentSize + 1)

mv.visitVarInsn(load, i + methodArgumentSize + 1)

}

}

mv.visitMethodInsn(INVOKESTATIC, cell.agentParent, cell.agentName, cell.agentDesc, false)

// 防止其他类重名方法被插入

StatisticPlugin.HOOKS.remove(methodName + methodDescriptor, cell)

} else { // 将扫描方法参数赋值给采集方法

// 采集数据的方法参数起始索引( 0:this,1+:普通参数 ),如果是static,则从0开始计算

int slotIndex = isStatic(methodAccess) ? 0 : 1

// 获取采集方法参数

Type agentMethodType = Type.getMethodType(cell.agentDesc)

Type[] agentArguments = agentMethodType.getArgumentTypes()

List agentArgumentList = new ArrayList(Arrays.asList(agentArguments))

// 遍历方法参数

for (Type argument : methodArguments) {

int size = argument.getSize()

int opcode = argument.getOpcode(ILOAD)

String descriptor = argument.getDescriptor()

Iterator agentIterator = agentArgumentList.iterator()

// 遍历采集方法参数

while (agentIterator.hasNext()) {

Type agentArgument = agentIterator.next()

String agentDescriptor = agentArgument.getDescriptor()

if (agentDescriptor == descriptor) {

mv.visitVarInsn(opcode, slotIndex)

agentIterator.remove()

break

}

}

slotIndex += size

}

if (agentArgumentList.size() > 0) { // 无法满足采集方法参数则return

return

}

mv.visitMethodInsn(INVOKESTATIC, cell.agentParent, cell.agentName, cell.agentDesc, false)

if(cell.isLambda){

StatisticPlugin.HOOKS.remove(methodName + methodDescriptor, cell)

}

}

}

/**

  • 推断类型

  • int ILOAD = 21; int ISTORE = 54;

  • 33 = ISTORE - ILOAD

  • @param load

  • @returno

*/

private static int getVarInsn(int load) {

return load + 33

}

private static boolean isStatic(int access) {

return (access & Opcodes.ACC_STATIC) != 0

}

}

5、 如何使用?
5.1、 先打包插件到本地仓库进行引用

5.2、 在项目的根build.gradle加入插件的依赖

repositories {

google()

mavenCentral()

jcenter()

maven{

url uri(‘repos’)

}

}

dependencies {

classpath “com.android.tools.build:gradle:$gradle_version”

classpath “org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version”

classpath ‘com.meituan.android.walle:plugin:1.1.7’

// 使用自定义插件

classpath ‘com.example.plugin:statistic:1.0.0’

// NOTE: Do not place your application dependencies here; they belong

// in the individual module build.gradle files

}

5.3、 在app的build.gradle中使用并配置参数

plugins {

id ‘com.android.application’

id ‘statistic’

}

buryPoint {

hooks = [

[

‘agentName’ : ‘viewOnClick’, //采集数据的方法名

‘agentDesc’ : ‘(Landroid/view/View;)V’, //采集数据的方法描述(参数应在methodDesc范围之内)

‘agentParent’ : ‘com/example/fragment/project/utils/StatisticHelper’, //采集数据的方法的路径

‘isAnnotation’: false,

‘methodName’ : ‘onClick’, //插入的方法名

最后,如果大伙有什么好的学习方法或建议欢迎大家在评论中积极留言哈,希望大家能够共同学习、共同努力、共同进步。

小编在这里祝小伙伴们在未来的日子里都可以 升职加薪,当上总经理,出任CEO,迎娶白富美,走上人生巅峰!!

不论遇到什么困难,都不应该成为我们放弃的理由!

很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言,一定会认真查询,修正不足,谢谢。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 15
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值