ASM是什么?
ASM is an all purpose Java bytecode manipulation and analysis framework.
ASM官网的描述:一个java字节码的处理和解析框架。
在Android中.java文件在编译成.class文件后还需要转换成Android 虚拟机能识别的.dex文件。针对这种编译流程,我们可以在.class文件到.dex之前,对.class文件进行增改。由于是直接操作.class文件,所以可以通过AOP的思想,简单的实现java源码中很繁琐的功能,比如说统计所有的点击事件等。这些对于字节码的处理和解析都可以通过[ASM](https://asm.ow2.io/)实现。
在使用ASM之前,还有个问题:我们如何拿到编译好的字节码文件?
Starting with 1.5.0-beta1, the Gradle plugin includes a Transform API allowing 3rd party plugins to manipulate compiled class files before they are converted to dex files. (The API existed in 1.4.0-beta2 but it's been completely revamped in 1.5.0-beta1)
从1.5.0-beta1开始,Gradle插件包含一个Transform API,允许第三方插件在将已编译的类文件转换为dex文件之前对其进行操作。 (该API已存在于1.4.0-beta2中,但已在1.5.0-beta1中进行了彻底修改)
所以我们要先了解gradle transform如何使用
1. Gradle Transform
除了创建新project 默认自带的app module外,我们单独创建一个frc-pluginmodule用来实现Gradle Transform逻辑(以及后续的ASM逻辑)。
1.1 生成Transform
先创建一个 Android Library module,名称为frc-plugin
将frc-plugin module的build.gradle清空,然后替换成下面代码:
plugins {
id 'groovy'
id 'maven'
}
dependencies {
implementation gradleApi()
implementation localGroovy()
implementation 'com.android.tools.build:gradle:4.1.2'
}
repositories {
jcenter()
}
//后面会用到这个task 来生成maven依赖
uploadArchives {
repositories.mavenDeployer {
//本地仓库路径
repository(url: uri('../rep'))
//下面就是一种格式,具体参数自己定义 最后组成:'com.rongcheng:frc_plugin:1.0.0'
pom.groupId = 'com.rongcheng'
pom.artifactId = 'frc_plugin'
pom.version = '1.0.0'
}
}
如下图:
在src/main/目录下创建一个 groovy包,并删除自动生成的java包:
在groovy包下创建了rongcheng.plugin目录,并创建了FrcTransform.groovy类:
package rongcheng.plugin
import com.android.build.api.transform.Context;
import com.android.build.api.transform.QualifiedContent;
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformOutputProvider
import com.android.build.gradle.internal.pipeline.TransformManager;
/**
* @author: frc* @description: 用于测试 gradle transform 功能
* @date: 2/7/21 2:49 PM
*/
class FrcTransform extends Transform {
/**
* transform 最后会转换成 gradle task 执行,这里返回个任务名
* @return task name
*/
@Override
String getName() {
return 'frc_transform'
}
/**
* 指定当前transform 要处理的数据类型
* @return .class类型的数据
*/
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
/**
* 指定当前 transform 的作用范围
* @return
*/
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
/**
* 是否增量更新
* @return false
*/
@Override
boolean isIncremental() {
return false
}
/**
* 正在执行 transform逻辑的方法,在这里能拿到.class文件,操作后再写出去
* @param transformInvocation
* @throws TransformException
* @throws InterruptedException
* @throws IOException
*/
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
println '》》》》》》》》》》》》》》this is frc transform》》》》》》》》》》》'
}
}
每个方式上都有详细注释,可以看下。
现在我们在transform中打印个日志。看下能不能输出。
不过又引入另外一个问题,自定义的Transform怎么被执行呢?
我们可以通过gradle plugin 来使用这个Transform,然后再让app依赖该plugin。
1.2 自定义Gradle Plugin
如何自定义一个gradle plugin?只要实现Plugin接口,并依赖就行,如下就是在app module的build.gradle中写的一个简单的FrcPlugin:
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
class FrcPlugin implements Plugin<Project>{
@Override
void apply(Project project) {
println '》》》》》》 this is frc plugin 》》》》'
}
}
apply plugin:FrcPlugin.class
...
日志成功输出:
当然这种写法太死了,所以我们可以将这个FrcPlugin放到frc-plugin中:
package rongcheng.plugin
import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project;
/**
* @author: frc* @description:
* @date: 2/7/21 3:43 PM
*/
class FrcPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
def appExtension = project.extensions.findByType(AppExtension.class)
//注册
appExtension.registerTransform(new FrcTransform())
}
}
在apply中奖FrcTransform进行了注册。
然后在src/main目录下创建一个resources目录:
再在resources中创建一个/META-INF/gradle-plugins/xxxx.properites文件:
注意com.rongcheng.frc-plugin.properties文件名com.rongcheng.frc-plugin就是之后要在build.gradle中apply的。
文件中implementation-class=rongcheng.plugin.FrcPlugin指定了依赖的类,会被触发执行。
1.3 使用Gradle Plugin
找到frc-compliermodule的uploadArchivestask,点击执行:
如果成功会在根目录下生成rep文件夹:
现在让app module中使用frc-plugin。
首先需要在project的build.gradle中配置如下:
然后在app module的build.gradle中依赖
然后编译app项目,会有日志输出。这说明FrcTransform被成功执行了:
不过此时直接run app 是会有问题的,告诉你apk无法找到。
问题出在:我们重写的transform方法
/**
* 正在执行 transform逻辑的方法,在这里能拿到.class文件,操作后再写出去
* @param transformInvocation
* @throws TransformException
* @throws InterruptedException
* @throws IOException
*/
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
println '》》》》》》》》》》》》》》this is frc transform》》》》》》》》》》》'
}
因为这里传给你的.class文件你没有将它传出去,所以最终在生成.dex文件时找不到对应文件,导致无法生成apk。
1.4 transform()
/**
* 正在执行 transform逻辑的方法,在这里能拿到.class文件,操作后再写出去
* @param transformInvocation
* @throws TransformException
* @throws InterruptedException
* @throws IOException
*/
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
}
transform(TransformInvocation t)方法就给我们返回了个TransformInvocation对象,我们后续的操作都靠它了。
/**
* An invocation object used to pass of pertinent information for a
* {@link Transform#transform(TransformInvocation)} call.
*/
public interface TransformInvocation {
/**
* Returns the context in which the transform is run.
* @return the context in which the transform is run.
*/
@NonNull
Context getContext();
/**
* Returns the inputs/outputs of the transform.
* @return the inputs/outputs of the transform.
*/
@NonNull
Collection<TransformInput> getInputs();
/**
* Returns the referenced-only inputs which are not consumed by this transformation.
* @return the referenced-only inputs.
*/
@NonNull Collection<TransformInput> getReferencedInputs();
/**
* Returns the list of secondary file changes since last. Only secondary files that this
* transform can handle incrementally will be part of this change set.
* @return the list of changes impacting a {@link SecondaryInput}
*/
@NonNull Collection<SecondaryInput> getSecondaryInputs();
/**
* Returns the output provider allowing to create content.
* @return he output provider allowing to create content.
*/
@Nullable
TransformOutputProvider getOutputProvider();
/**
* Indicates whether the transform execution is incremental.
* @return true for an incremental invocation, false otherwise.
*/
boolean isIncremental();
}
如注释:一个用来传递Transform调用相关信息的对象。
其中最重要的是:
@NonNull
Collection<TransformInput> getInputs();
@Nullable
TransformOutputProvider getOutputProvider();
分别获取TransformInput和TransformOutputProvider对象
TransformInput: transform时输入的对象,包含两种类型:jar包和文件类型(.class)TransformOutputProvider: 用来获取文件的输出地址
添加如下代码,运行看下输出:
/**
* 正在执行 transform逻辑的方法,在这里能拿到.class文件,操作后再写出去
* @param transformInvocation
* @throws TransformException
* @throws InterruptedException
* @throws IOException
*/
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
println '》》》》》》》》》》》》》》this is frc transform》》》》》》》》》》》'
// Transform 的 inputs 有两种类型,一种是目录,一种是 jar 包,要分开遍历
def inputs = transformInvocation.getInputs()
def outputProvider = transformInvocation.outputProvider
inputs.each { TransformInput input ->
//遍历目录
input.directoryInputs.each { DirectoryInput directoryInput ->
println "input.directoryInputs input path: ${ directoryInput.getFile().absolutePath}"
//获取 output 目录
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
println "input.directoryInputs output path: ${dest.absolutePath}"
}
//遍历 jar
input.jarInputs.each { JarInput jarInput ->
// // 重命名输出文件(同目录copyFile会冲突)
def jarName = jarInput.name
println "jarName: ${jarName}"
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
println "md5Name: ${md5Name}"
//
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
//
File copyJarFile = jarInput.file
println "copyJarFile path : ${copyJarFile.getAbsoluteFile()}"
// 生成输出路径
def dest = outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
println "input.jarInputs path: ${dest.absolutePath}"
println "》》》》》》》》》》》》》》"
}
}
}
下面是我截取的一部分日志输出:
分为两部分:jar和class文件,
> Task :app:transformClassesWithFrc_transformForDebug
》》》》》》》》》》》》》》this is frc transform》》》》》》》》》》》
----jar 部分 ---
jarName: androidx.core:core-ktx:1.3.2
md5Name: b3193180d0f251aee2907fdab0b4c786
copyJarFile path : /Users/frc/.gradle/caches/transforms-2/files-2.1/9612c86e7c70e163e9cd77fb9435bfd7/jetified-core-ktx-1.3.2-runtime.jar
input.jarInputs path: /Users/frc/Code/android/Study/app/build/intermediates/transforms/frc_transform/debug/45.jar
》》》》》》》》》》》》》》
jarName: androidx.lifecycle:lifecycle-common:2.1.0
md5Name: 8000dbc5e16924134f6474ec97a8bd71
copyJarFile path : /Users/frc/.gradle/caches/modules-2/files-2.1/androidx.lifecycle/lifecycle-common/2.1.0/c67e7807d9cd6c329b9d0218b2ec4e505dd340b7/lifecycle-common-2.1.0.jar
input.jarInputs path: /Users/frc/Code/android/Study/app/build/intermediates/transforms/frc_transform/debug/78.jar
...
》》》》》》》》》》》》》》
---class 目录部分---
input.directoryInputs input path: /Users/frc/Code/android/Study/app/build/intermediates/javac/debug/classes
input.directoryInputs output path: /Users/frc/Code/android/Study/app/build/intermediates/transforms/frc_transform/debug/84
input.directoryInputs input path: /Users/frc/Code/android/Study/app/build/tmp/kotlin-classes/debug
input.directoryInputs output path: /Users/frc/Code/android/Study/app/build/intermediates/transforms/frc_transform/debug/85
input.directoryInputs input path: /Users/frc/Code/android/Study/app/build/tmp/kapt3/classes/debug
input.directoryInputs output path: /Users/frc/Code/android/Study/app/build/intermediates/transforms/frc_transform/debug/86
先输出的是jar和这些class文件原来所在的位置,然后输出是编译后他们应该去的位置。可以看到很多三方库原来的位置都是在/.gradle/caches目录下,想要编译当前apk,必须先要copy到默认目录,该默认目录通过outputProvider.getContentLocation()获取。不能随便传,不然编译器找不到,也没法编译。
以class文件为例,/Users/frc/Code/android/Study/app/build/intermediates/javac/debug/classes目录下有如下两个class文件。
但是/Users/frc/Code/android/Study/app/build/intermediates/transforms/frc_transform/debug/84目录却是空的。上面代码只是读取,没有copy。
所以,在对class文件操作后,还需要将这些文件拷贝到默认路径。下面是完整代码,可以成功编译apk了。
/**
* 正在执行 transform逻辑的方法,在这里能拿到.class文件,操作后再写出去
* @param transformInvocation
* @throws TransformException
* @throws InterruptedException
* @throws IOException
*/
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
println '》》》》》》》》》》》》》》this is frc transform》》》》》》》》》》》'
// Transform 的 inputs 有两种类型,一种是目录,一种是 jar 包,要分开遍历
def inputs = transformInvocation.getInputs()
def outputProvider = transformInvocation.outputProvider
inputs.each { TransformInput input ->
//遍历目录
input.directoryInputs.each { DirectoryInput directoryInput ->
//获取 output 目录
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
// 将 input 的目录复制到 output 指定目录
FileUtils.copyDirectory(directoryInput.file, dest)
}
//遍历 jar
input.jarInputs.each { JarInput jarInput ->
// 重命名输出文件(同目录copyFile会冲突)
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
File copyJarFile = jarInput.file
//生成输出路径
def dest = outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
// 将 input 的目录复制到 output 指定目录
FileUtils.copyFile(copyJarFile, dest)
}
}
}
现在``/Users/frc/Code/android/Study/app/build/intermediates/transforms/frc_transform/debug/`下就有文件了,也能成功编译了。
至此,gradle transform 这块基本清楚了。下面要做的就是学会使用asm,然后在读取目标字节码,处理后再传给编译器。
ASM
ASM的内容很多,下面以一个简单的例子展示下使用的流程,不对具体的api进行讲解。具体api可以参考官网材料
2.1 添加依赖
ASM是个三方框架,所以我们首先需要在frc-plugin 中添加依赖。
dependencies {
implementation gradleApi()
implementation localGroovy()
implementation 'com.android.tools.build:gradle:4.1.2'
//包中定义类基于核心 API 的相关操作类
//包中定义了核心 API 的类,其中 ClassVisitor、FieldVisitor、MethodVisitor 和 AnnotationVisitor
// 四个抽象类,用于访问 .class 字节码文件中的 fields、 methods 和 annotations 相关的指令。
implementation 'org.ow2.asm:asm:7.1'
//包中提供了实用的类或方法的 Adapter 方法转换器;
implementation 'org.ow2.asm:asm-commons:7.1'
//包中提供了常见的类分析框架和分析器类;
implementation 'org.ow2.asm:asm-analysis:7.1'
//包中提供了基于核心 API 的常见工具类。
implementation 'org.ow2.asm:asm-util:7.1'
//包中定义了基于树 API 的类,以及一些用于事件和树 API 转换的工具类;
implementation 'org.ow2.asm:asm-tree:7.1'
}
2.2 简单案例
现在希望通过ASM在MainActivity的onCreate方法最前面添加System.out.println("<<< default message >>>")
package com.rong.cheng.study
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.rong.cheng.asm.PrintLog
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
//类似动态添加了这行代码: System.out.println("<<< default message >>>");
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
首先我们要在transform的过程中找到MainActivity.class:
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
println '》》》》》》》》》》》》》》this is frc transform》》》》》》》》》》》'
// Transform 的 inputs 有两种类型,一种是目录,一种是 jar 包,要分开遍历
def inputs = transformInvocation.getInputs()
def outputProvider = transformInvocation.outputProvider
inputs.each { TransformInput input ->
//遍历目录
input.directoryInputs.each { DirectoryInput directoryInput ->
//获取 output 目录
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
//获取到class文件
def dir = directoryInput.file
if (dir) {
dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) {
File classFile ->
//过滤不需要修改的class
println "class file name : ${classFile.name}"
if (classFile.name == "MainActivity.class"){
println "get MainActivity class"
}
}
}
// 将 input 的目录复制到 output 指定目录
FileUtils.copyDirectory(directoryInput.file, dest)
}
//遍历 jar
...
}
}
编译后日志输出如下,是可以找到对应的class文件的。
获取到目标class文件后,使用 ClassReader来解析
if (classFile.name == "MainActivity.class") {
ClassReader classReader = new ClassReader(classFile.bytes)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor classVisitor = new FrcClassVisitor(Opcodes.ASM7, classWriter)
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
//覆盖原来的class文件
byte[] code = classWriter.toByteArray()
FileOutputStream fos = new FileOutputStream(classFile.parentFile.absolutePath + File.separator + classFile.name)
fos.write(code)
fos.close()
println "get MainActivity class"
}
上面我们使用了 ClassVisitor 来处理class文件
/**
* @author: frc
* @description:
* @date: 2/9/21 3:55 PM
*/
public class FrcClassVisitor extends ClassVisitor implements Opcodes {
public FrcClassVisitor(int api) {
super(api);
}
public FrcClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
System.out.println("FrcClassVisitor ----> start");
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
System.out.println("FrcClassVisitor ----> visit");
System.out.println("version: " + version);
System.out.println("access: " + access);
System.out.println("name: " + name);
System.out.println("signature: " + signature);
System.out.println("superName: " + superName);
System.out.println("interfaces: " + Arrays.toString(interfaces));
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
System.out.println("FrcClassVisitor ----> visitMethod");
System.out.println("access: " + access);
System.out.println("name: " + name);
System.out.println("signature: " + signature);
System.out.println("descriptor: " + descriptor);
System.out.println("exceptions: " + Arrays.toString(exceptions));
MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
if (name.equals("onCreate")) {
return new FrcMethodVisitor(Opcodes.ASM7, mv);
}
return mv;
}
@Override
public void visitEnd() {
super.visitEnd();
System.out.println("FrcClassVisitor ----> end");
}
}
visit是最先调用的方法,里面会返回一些类相关信息。
visitMethod 是当前class文件中方法的读取回调。
上面添加了日志,看下日志输出就知道了:
可以看到当前class为com/rong/cheng/study/MainActivity,父类是androidx/appcompat/app/AppCompatActivity,没有实现接口等。
而visitMethod执行了2次,说明com/rong/cheng/study/MainActivity有两个方法,一个是init,一个是onCreate()。
现在我们需要在该onCreate()方法中添加System.out.println("<<< default message >>>");
//先判断方法名
if (name.equals("onCreate")) {
return new FrcMethodVisitor(Opcodes.ASM7, mv);
}
方法的添加使用了MethodVisitor:
package rongcheng.plugin;
import org.objectweb.asm.MethodVisitor;
import static org.gradle.internal.impldep.bsh.org.objectweb.asm.Constants.GETSTATIC;
import static org.gradle.internal.impldep.bsh.org.objectweb.asm.Constants.INVOKEVIRTUAL;
/**
* @author: frc
* @description:
* @date: 2/9/21 4:35 PM
*/
class FrcMethodVisitor extends MethodVisitor {
public FrcMethodVisitor(int api) {
super(api);
}
public FrcMethodVisitor(int api, MethodVisitor mv) {
super(api, mv);
}
@Override
public void visitCode() {
//System.out.println("<<< default message >>>");的字节码
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("<<< default message >>>");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
//原方法之前
super.visitCode();
//原方法之后
}
}
在visitCode()中添加了具体逻辑的字节码。
最后编译看下MainActivity是否被修改:
2.3 字节码辅助插件
如果能自己不能手写字节码的话,可以借助了插件show bytecode outline。
不过该插件在android studio 4之后不兼容。所以我在 IDEA上使用了该插件。
如下图使用java正常写需要的内容,然后右击选择show bytecode outline.
选择需要的字节码,拷贝到MethodVistor中即可。
本篇文章只是记录了一个简单的使用流程,为解决开发中的问题提供一个思路。Transform API和ASM中的一些具体API的使用,需要在真实开发中逐步了解,多查文档即可。