前言
字节码插桩,看起来挺牛皮,实际上是真的很牛皮。
但是牛皮不代表难学,只需要一点前置知识就能轻松掌握。
对Gradle和ASM不太了解的同学,强烈推荐下面的文章:
【Android】函数插桩(Gradle + ASM)
字节码插桩–你也可以轻松掌握
正所谓:”工欲善其事,必先利其器“。
开始之前,建议在Android Studio中安装”ASM Bytecode Viewer Support Kotlin”
代码右键 ASM Bytecode Viewer 便能自动生成ASM插桩代码,效果如下:

实战:
既然准备工作已经做好,那就正式进入实战,先看下目录结构:

1、StatisticPlugin (注册插件和获取配置的埋点信息)
class StatisticPlugin implements Plugin<Project> {
public final static HashMap<String, BuryPointCell> HOOKS = new HashMap<>()
@Override
void apply(Project project) {
def android = project.extensions.findByType(AppExtension)
// 注册BuryPointTransform
android.registerTransform(new BuryPointTransform())
// 获取gradle里面配置的埋点信息
def extension = project.extensions.create('buryPoint', BuryPointExtension)
project.afterEvaluate {
// 遍历配置的埋点信息,将其保存在HOOKS方便调用
extension.hooks.each { Map<String, Object> map ->
BuryPointCell cell = new BuryPointCell()
boolean isAnnotation = map.get("isAnnotation")
cell.isAnnotation = isAnnotation
cell.agentName = map.get("agentName")
cell.agentDesc = map.get("agentDesc")
cell.agentParent = map.get("agentParent")
if (isAnnotation) {
cell.annotationDesc = map.get("annotationDesc")
cell.annotationParams = map.get("annotationParams")
HOOKS.put(cell.annotationDesc, cell)
} else {
cell.methodName = map.get("methodName")
cell.methodDesc = map.get("methodDesc")
cell.methodParent = map.get("methodParent")
cell.methodParams = map.get("methodParams")
HOOKS.put(cell.methodName + cell.methodDesc, cell)
}
}
}
}
}
复制代码
2、BuryPointTransform (实现Transform进行.class文件遍历拿到所有方法)
class BuryPointTransform extends Transform {
@Override
String getName() {
return "BuryPoint"
}
/**
* 需要处理的数据类型,有两种枚举类型
* CLASS->处理的java的class文件
* RESOURCES->处理java的资源
* @return
*/
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
/**
* 指 Transform 要操作内容的范围,官方文档 Scope 有 7 种类型:
* 1. EXTERNAL_LIBRARIES 只有外部库
* 2. PROJECT 只有项目内容
* 3. PROJECT_LOCAL_DEPS 只有项目的本地依赖(本地jar)
* 4. PROVIDED_ONLY 只提供本地或远程依赖项
* 5. SUB_PROJECTS 只有子项目。
* 6. SUB_PROJECTS_LOCAL_DEPS 只有子项目的本地依赖项(本地jar)。
* 7. TESTED_CODE 由当前变量(包括依赖项)测试的代码
* @return
*/
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
/**
* 是否增量编译
* @return
*/
@Override
boolean isIncremental() {
return false
}
/**
*
* @param context
* @param inputs 有两种类型,一种是目录,一种是 jar 包,要分开遍历
* @param outputProvider 输出路径
*/
@Override
void transform(
@NonNull Context context,
@NonNull Collection<TransformInput> inputs,
@NonNull Collection<TransformInput> referencedInputs,
@Nullable TransformOutputProvider outputProvider,
boolean isIncremental
) throws IOException, TransformException, InterruptedException {
if (!incremental) {
//不是增量更新删除所有的outputProvider
outputProvider.deleteAll()
}
inputs.each { TransformInput input ->
//遍历目录
input.directoryInputs.each { DirectoryInput directoryInput ->
handleDirectoryInput(directoryInput, outputProvider)
}
// 遍历jar 第三方引入的 class
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 (访问class的类)
class BuryPointVisitor extends ClassVisitor {
BuryPointVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM7, classVisitor)
}
/**
* @param version 类版本
* @param access 修饰符
* @param name 类名
* @param signature 泛型信息
* @param superName 父类
* @param interfaces 实现的接口
*/
@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 visitEnd() {
super.visitEnd()
}
/**
* 扫描类的方法进行调用
* @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 (访问方法的类,在这里进行代码插入)
class BuryPointMethodVisitor extends AdviceAdapter {
String methodName
String methodDescriptor
BuryPointMethodVisitor(MethodVisitor methodVisitor, int access, String name, String desc) {
super(Opcodes.ASM7, methodVisitor, access, name, desc)
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)
// 通过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.methodName = handle.getName()
newCell.methodDesc = handle.getDesc()
StatisticPlugin.HOOKS.put(newCell.methodName + newCell.methodDesc, newCell)
}
}
}
/**
* 进入方法时调用
*/
@Override
protected void onMethodEnter() {
super.onMethodEnter()
BuryPointCell cell = StatisticPlugin.HOOKS.get(methodName + methodDescriptor)
if (cell != null) {
if (cell.isAnnotation) { // 遍历注解参数并赋值给采集方法
def entrySet = cell.annotationParams.entrySet()
def size = entrySet.size()
for (int i = 0; i < size; i++) {
def load = entrySet[i].getValue()
def store = getVarInsn(load)
mv.visitLdcInsn(cell.annotationData.get(entrySet[i].getKey()))
mv.visitVarInsn(store, i + 10)
mv.visitVarInsn(load, i + 10)
}
mv.visitMethodInsn(INVOKESTATIC, cell.agentParent, cell.agentName, cell.agentDesc, false)
} else { // 将扫描方法参数赋值给采集方法
for (int key : cell.methodParams.keySet()) {
mv.visitVarInsn(cell.methodParams.get(key), key)
}
mv.visitMethodInsn(INVOKESTATIC, cell.agentParent, cell.agentName, cell.agentDesc, false)
}
}
}
/**
* 推断类型
* int ILOAD = 21; int ISTORE = 54;
* 33 = ISTORE - ILOAD
*
* @param load
* @returno
*/
private static int getVarInsn(int load) {
return load + 33
}
}
复制代码
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'
}
import org.objectweb.asm.Opcodes
buryPoint {
hooks = [
[
'agentName' : 'viewOnClick', //采集数据的方法名
'agentDesc' : '(Landroid/view/View;)V', //采集数据的方法描述
'agentParent' : 'com/example/fragment/project/utils/StatisticHelper', //采集数据的方法的路径
'isAnnotation': false,
'methodName' : 'onClick', //插入的方法名
'methodDesc' : '(Landroid/view/View;)V', //插入的方法描述
'methodParent': 'Landroid/view/View$OnClickListener;', //插入的方法的实现接口
'methodParams': [
1: Opcodes.ALOAD //采集数据的方法参数起始索引(从1开始) : 参数类型对应的ASM指令,加载不同类型的参数需要不同的指令
]
],
[
'agentName' : 'testAnnotation',
'agentDesc' : '(Ljava/lang/String;Z)V',
'agentParent' : 'com/example/fragment/project/utils/StatisticHelper',
'isAnnotation' : true,
'annotationDesc' : 'Lcom/example/fragment/project/utils/TestAnnotation;', //扫描的方法注解名称
'annotationParams': [
'message': Opcodes.ALOAD, //方法注解的值名称 : 参数类型对应的ASM指令,加载不同类型的参数需要不同的指令
'sb' : Opcodes.ILOAD
]
],
]
}
复制代码
6、 运行项目查看输出日志
2021-06-28 20:04:49.544 25211-25211/com.example.fragment.project.debug I/----------自动埋点:注解: MainActivity.onCreate:false
2021-06-28 20:05:03.535 25211-25211/com.example.fragment.project.debug I/----------自动埋点: ViewId:menu ViewText:null
2021-06-28 20:05:06.085 25211-25211/com.example.fragment.project.debug I/----------自动埋点: ViewId:coin ViewText:我的积分
2021-06-28 20:05:08.039 25211-25211/com.example.fragment.project.debug I/----------自动埋点: ViewId:black ViewText:null
2021-06-28 20:05:11.616 25211-25211/com.example.fragment.project.debug I/----------自动埋点: ViewId:username ViewText:去登录
2021-06-28 20:05:16.816 25211-25211/com.example.fragment.project.debug I/----------自动埋点: ViewId:login ViewText:登录
复制代码
项目地址
- github: github.com/miaowmiaow
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END



















![[02/27][官改] Simplicity@MIX2 ROM更新-一一网](https://www.proyy.com/wp-content/uploads/2020/02/3168457341.jpg)



![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)