Transform
Transform的作用:是用来替换(或转换)Class
利用Transform将旧的class文件取出来,再用AMS修改class的字节码,最后替换成我们新的class文件。
android gradle 插件自从1.5.0-beta1
版本开始就包含了一个Transform API,允许第三方插件在编译后的类文件转换为dex文件之前做处理操作。
注册Transform
在我们的gradle插件中,通过android.registerTransform(theTransform)
class StatisticsPlugin implements Plugin<Project> {
void apply(Project project) {
AppExtension appExtension = project.extensions.findByType(AppExtension.class)
appExtension.registerTransform(new MyTransform())
}
}
Transform的使用
自定义的Transform继承于com.android.build.api.transform.Transform
class MyTransform(val project: Project) : Transform() {
private var SCOPES: MutableSet<QualifiedContent.Scope> = mutableSetOf()
init {
SCOPES.add(QualifiedContent.Scope.PROJECT)
SCOPES.add(QualifiedContent.Scope.SUB_PROJECTS)
SCOPES.add(QualifiedContent.Scope.EXTERNAL_LIBRARIES)
}
/**
* transform 名字
*/
override fun getName(): String {
return "xxx"
}
/**
* 输入文件的类型
*/
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
return TransformManager.CONTENT_CLASS
}
/**
* 指定作用范围
*/
override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
return SCOPES
}
/**
* 是否支持增量
*/
override fun isIncremental(): Boolean {
return false
}
/**
* transform的执行
*/
override fun transform(transformInvocation: TransformInvocation?) {
transformInvocation?.inputs?.forEach {
// 项目中编写的代码
it.directoryInputs.forEach {directoryInput->
with(directoryInput){
//字节码操作
......
}
}
// 项目中引入第三方Jar包的代码
it.jarInputs.forEach { jarInput->
with(jarInput){
//字节码操作
......
}
}
}
}
}
作用域
通过Transform#getScopes指定的作用域:
作用对象
通过Transform#getInputTypes指定的作用对象
转换transform
Transform插桩主要是在override fun transform(transformInvocation: TransformInvocation?) 执行完成,对于有代码的地方都需要扫描到。
TransformInvocation
我们通过实现Transform#transform方法来处理我们的中间转换过程, 而中间相关信息都是通过TransformInvocation对象来传递
// transform的上下文
@NonNull
Context getContext();
// 返回transform的输入源
@NonNull
Collection<TransformInput> getInputs();
// 返回引用型输入源
@NonNull Collection<TransformInput> getReferencedInputs();
//额外输入源
@NonNull Collection<SecondaryInput> getSecondaryInputs();
//输出源
@Nullable
TransformOutputProvider getOutputProvider();
//是否增量
boolean isIncremental();
示例:
对全文件字节码扫描进行函数耗时统计。
插件中注册transform,然后apply form引用插件。
import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
class StatisticsPlugin implements Plugin<Project> {
void apply(Project project) {
AppExtension appExtension = project.extensions.findByType(AppExtension.class)
appExtension.registerTransform(new MyTransform())
}
}
MyTransform类,进行全局class文件夹扫描。
public class MyTransform extends Transform {
@Override
public String getName() {
//名字,输出的文件会默认生成在这个名字的目录下,比如:MyPlugin\app\build\intermediates\transforms\MyTransform..
return "MyTransform";
}
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
@Override
public boolean isIncremental() {
return true;
}
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation);
//可以从中获取jar包和class文件夹路径。需要输出给下一个任务
Collection<TransformInput> inputs = transformInvocation.getInputs();
//OutputProvider管理输出路径
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
//遍历所有的输入,有两种类型,分别是文件夹类型(也就是我们自己写的代码)和jar类型(引入的jar包),这里我们只处理自己写的代码。
for (TransformInput input: inputs) {
//遍历所有文件夹
for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
//获取transform的输出目录,等我们插桩后就将修改过的class文件替换掉transform输出目录中的文件,就达到修改的效果了。
File dest = outputProvider.getContentLocation(directoryInput.getName(),
directoryInput.getContentTypes(), directoryInput.getScopes(),
Format.DIRECTORY);
transformDir(directoryInput.getFile(), dest);
}
}
}
/**
* 遍历文件夹,对文件进行插桩
* @param input 源文件
* @param dest 源文件修改后的输出地址
* @throws IOException
*/
private static void transformDir(File input, File dest) throws IOException {
if (dest.exists()) {
FileUtils.delete(dest);
}
dest.mkdirs();
String srcDirPath = input.getAbsolutePath();
String destDirPath = dest.getAbsolutePath();
File[] fileList = input.listFiles();
if (fileList == null) {
return;
}
for (File file : fileList) {
String destFilePath = file.getAbsolutePath().replace(srcDirPath, destDirPath);
File destFile = new File(destFilePath);
if (file.isDirectory()) {
//如果是文件夹,继续遍历
transformDir(file, destFile);
} else if (file.isFile()) {
//创造了大小为0的新文件,或者,如果该文件已存在,则将打开并删除该文件关闭而不修改,但更新文件日期和时间
// FileUtils.touch(destFile);
MyASMCost.asmHandleFile(file.getAbsolutePath(), destFile.getAbsolutePath());
}
}
}
}
使用ClassReader,ClassWriter,ClassVisitor进行文件读取,在方法前后做字节码插桩操作。
public class MyASMCost {
/**
* 通过ASM进行插桩
* @param inputPath 源文件路径
* @param destPath 输出路径
*/
public static void asmHandleFile(String inputPath, String destPath) {
try {
File file = new File(inputPath);
FileInputStream fis = new FileInputStream(file);
//将class文件转成流
ClassReader cr = new ClassReader(fis);
//ClassWriter.COMPUTE_FRAMES 参数意义: 自动计算栈帧 和 局部变量表的大小
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
//执行分析
cr.accept(new MyClassVisitor(Opcodes.ASM5, cw), ClassWriter.COMPUTE_FRAMES);
//执行了插桩之后的字节码数据输出
byte[] bytes = cw.toByteArray();
FileOutputStream fos = new FileOutputStream(destPath);
fos.write(bytes);
fos.close();
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
static class MyClassVisitor extends ClassVisitor {
public MyClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
//类似于动态代理的机制,会将执行的方法进行回调,然后在方法执行之前和之后做操作
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
return new MyMethodVisitor(api, methodVisitor, access, name, descriptor);
}
}
static class MyMethodVisitor extends AdviceAdapter {
private int startTimeId = -1;
/**
* 用变量区分方法是否需要执行插桩
*/
boolean inject = false;
private String methodName = null;
protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
methodName = name;
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
//descriptor为方法的注解类型 行如: Lcom/example/bytecodeProject/ASMTest
//如果方法的注解为ASMTest,则执行插桩代码
if (descriptor.equals("Lcom/example/asmbytecode/simpledemo/ASMTest")) {
inject = true;
}
return super.visitAnnotation(descriptor, visible);
}
@Override
protected void onMethodEnter() { //代码插入到方法头部
super.onMethodEnter();
if (!inject) {
return;
}
//在Java kotlin中写代码直接写,但是ASM写代码有最大区别,就是需要用方法签名的格式来写。
//long l = System.currentTimeMillis();
//要写如上一行代码的字节码,需要执行一个静态方法,,类是System,方法名是currentTimeMillis,所以有如下代码:
startTimeId = newLocal(Type.LONG_TYPE);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitIntInsn(LSTORE, startTimeId);
}
@Override
protected void onMethodExit(int opcode) { //代码插入到方法结尾
super.onMethodExit(opcode);
if (!inject) {
return;
}
int durationId = newLocal(Type.LONG_TYPE);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(LLOAD, startTimeId);
mv.visitInsn(LSUB);
mv.visitVarInsn(LSTORE, durationId);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitLdcInsn("The cost time of " + methodName + "() is ");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(LLOAD, durationId);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
mv.visitLdcInsn(" ms");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}
}
参考:
https://blog.csdn.net/qq_30379689/article/details/127986526
https://juejin.cn/post/7129381154121056292