Android AOP编程(五)——Gradle插件+TransformAPI+字节码插桩实战

开篇

在前面几篇博文中,我记录了Android AOP编程的一些基础知识,包括Gradle插件的开发、TransformAPI的使用,以及一些操作字节码的工具如AspectJ,Javassist和ASM:

本篇将要记录的是将这些知识点串联起来的实战开发,要完成如下几个功能:

  1. 在Activity的onCreate方法中插入新代码(本篇主要是在每个Activity的onCreate中插入一个Toast)
  2. 处理点击事件重复触发问题(使用自定义注解处理快速点击时事件重复触发问题)
  3. 统计某个方法的执行时长(使用自定义注解统计某个方法的执行时长,类似JakeWharton开发的hugo
  4. 修复第三方jar包中的错误代码(比如修复某个jar包中的bug,直接修改字节码而不是修改源码后重新打包成jar)

下面请跟着我的步骤一步步完成上面几个功能吧!

开始

在实现上面说的4个功能之前,我们有一些通用的步骤:

  1. 创建新的Android项目AopDemo
  2. 在根目录下创建buildSrc目录并编译项目,buildSrc目录下会自动创建一些文件,我们的插件项目会基于buildSrc目录编写
  3. 在buildSrc目录下创建build.gradle文件并添加如下配置:
    apply plugin: 'java-library'
    apply plugin: 'groovy'
    apply plugin: 'maven'
    
    repositories {
        google()
        mavenCentral()
        mavenLocal()
    }
    
    dependencies {
        implementation gradleApi()
        implementation localGroovy()
        implementation 'com.android.tools.build:gradle:4.2.2'
        // javassist.jar文件可以在文末的源码中找到
        implementation files('libs/javassist.jar')
    }
    
    sourceSets {
        main {
            java {
                srcDir 'src/main/java'
            }
            resources {
                srcDir 'src/main/resources'
            }
        }
    }
    
    java {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    
  4. 在buildSrc目录下创建src/main/javasrc/main/resources目录
  5. src/main/java目录下创建包com.test.plugin,在src/main/resources目录下创建META-INF/gradle-plugins目录,并在该目录中添加文件,名为com.test.plugin.properties,文件内容为implementation-class=com.test.plugin.MyPlugin
  6. com.test.plugin包中加入如下两个类
    //MyPlugin.java
    package com.test.plugin;
    
    import com.android.build.gradle.BaseExtension;
    
    import org.gradle.api.Plugin;
    import org.gradle.api.Project;
    
    public class MyPlugin implements Plugin<Project> {
        @Override
        public void apply(Project target) {
            // 在Plugin中注册自定义的Transform
            BaseExtension baseExtension = target.getExtensions().findByType(BaseExtension.class);
            if (baseExtension != null) {
                baseExtension.registerTransform(new MyTransform());
            }
        }
    }
    
    //MyTransform.java
    package com.test.plugin;
    
    import com.android.build.api.transform.DirectoryInput;
    import com.android.build.api.transform.Format;
    import com.android.build.api.transform.JarInput;
    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.TransformInvocation;
    import com.android.build.gradle.internal.pipeline.TransformManager;
    import com.android.utils.FileUtils;
    import com.test.plugin.utils.InjectUtil;
    
    import org.apache.commons.io.IOUtils;
    
    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.util.Collection;
    import java.util.Enumeration;
    import java.util.Set;
    import java.util.jar.JarEntry;
    import java.util.jar.JarFile;
    import java.util.jar.JarOutputStream;
    import java.util.zip.ZipEntry;
    
    public class MyTransform extends Transform {
    
        @Override
        public String getName() {
            return "MyCustomTransform";
        }
    
        @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 false;
        }
    
        @Override
        public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
            super.transform(transformInvocation);
            if (!transformInvocation.isIncremental()) {
                // 非增量编译,则删除之前的所有输出
                transformInvocation.getOutputProvider().deleteAll();
            }
            // 拿到所有输入
            Collection<TransformInput> inputs = transformInvocation.getInputs();
            if (!inputs.isEmpty()) {
                for (TransformInput input : inputs) {
                    // directoryInputs保存的是存放class文件的所有目录,可以通过方法内打印的log查看具体的目录
                    Collection<DirectoryInput> directoryInputs = input.getDirectoryInputs();
                    handleDirInputs(transformInvocation, directoryInputs);
    
                    // jarInputs保存的是所有依赖的jar包的地址,可以通过方法内打印的log查看具体的jar包路径
                    Collection<JarInput> jarInputs = input.getJarInputs();
                    handleJarInputs(transformInvocation, jarInputs);
                }
            }
        }
    
        // 处理输入的目录
        private void handleDirInputs(TransformInvocation transformInvocation, Collection<DirectoryInput> directoryInputs) {
            for (DirectoryInput directoryInput : directoryInputs) {
                String absolutePath = directoryInput.getFile().getAbsolutePath();
    //            System.out.println(">>>> directory input file path: " + absolutePath);
                // 处理class文件
                InjectUtil.inject(absolutePath);
                // 获取目标地址
                File contentLocation = transformInvocation.getOutputProvider().getContentLocation(directoryInput.getName(),
                        directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);
                // 拷贝目录
                try {
                    FileUtils.copyDirectory(directoryInput.getFile(), contentLocation);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    
        // 处理输入的Jar包
        private void handleJarInputs(TransformInvocation transformInvocation, Collection<JarInput> jarInputs) {
            for (JarInput jarInput : jarInputs) {
                String absolutePath = jarInput.getFile().getAbsolutePath();
    //            System.out.println(">>>> jar input file path: " + absolutePath);
                File contentLocation = transformInvocation.getOutputProvider().getContentLocation(jarInput.getName(),
                        jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR);
                try {
                    // 匹配要修复的jar包
                    if (absolutePath.endsWith("calculator-0.0.1.jar")) {
                        // 原始的jar包
                        JarFile jarFile = new JarFile(absolutePath);
                        // 处理后的jar包路径
                        String tmpJarFilePath = jarInput.getFile().getParent() + File.separator + jarInput.getFile().getName() + "_tmp.jar";
                        File tmpJarFile = new File(tmpJarFilePath);
                        JarOutputStream jos = new JarOutputStream(new FileOutputStream(tmpJarFile));
    //                    System.out.println("origin jar file path: " + jarInput.getFile().getAbsolutePath());
    //                    System.out.println("tmp jar file path: " + tmpJarFilePath);
                        Enumeration<JarEntry> entries = jarFile.entries();
                        // 遍历jar包中的文件,找到需要修改的class文件
                        while (entries.hasMoreElements()) {
                            JarEntry jarEntry = entries.nextElement();
                            String name = jarEntry.getName();
                            jos.putNextEntry(new ZipEntry(name));
                            InputStream is = jarFile.getInputStream(jarEntry);
                            // 匹配到有问题的class文件
                            if ("com/bug/calculator/BugCalculator.class".equals(name)) {
                                // 处理有问题的class文件并将新的数据写入到新jar包中
                                jos.write(InjectUtil.fixJarBug(absolutePath));
                            } else {
                                // 没有问题的直接写入到新的jar包中
                                jos.write(IOUtils.toByteArray(is));
                            }
                            jos.closeEntry();
                        }
                        // 关闭IO流
                        jos.close();
                        jarFile.close();
                        // 拷贝新的Jar文件
    //                    System.out.println(">>>>>>>>copy to dest: " + contentLocation.getAbsolutePath());
                        FileUtils.copyFile(tmpJarFile, contentLocation);
                        // 删除临时文件
    //                    System.out.println(">>>>>>>>tmpJarFile: " + tmpJarFile.getAbsolutePath());
                        tmpJarFile.delete();
                    } else {
                        FileUtils.copyFile(jarInput.getFile(), contentLocation);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    

以上代码主要是创建了一个gradle插件并在其中添加了一个自定义的Transform,这样Android项目在编译过程中,会自动执行我们定义的MyTransform类中的transform方法。

上面的代码中最主要的是MyTransform类中的transform方法,其编写格式基本固定如上面的代码所示,通过TransformInvocation对象拿到所有的class输入,其中又包括目录和jar包,对目录和jar包要单独处理,分别对应上面代码中的handleDirInputs() handleJarInputs()方法。

handleDirInputs()方法主要是通过InjectUtil.inject()方法完成对class文件的处理。

handleJarInputs()方法主要是匹配需要处理的jar包,然后通过Java提供的一些操作Jar包的API来读取Jar包内需要修改的class文件,再将jar包拷贝到指定的目录下。

InjectUtil类在com.test.plugin.utils包下,它主要完成对class文件的一些操作。

在开篇中我们需要完成的四个功能,主要逻辑都集中在InjectUtil类中,下面就看看每个功能是如何实现的吧!

在方法中插入新代码

在本例子中,我们将通过一个gradle插件,在代码编译期,向所有Activity的onCreate方法中插入一句Toast代码,弹出当前Activity的名称。下面直接上源码:

/**
 * 使用Javassist操作Class字节码,在所有Activity的onCreate方法中插入Toast
 * @param filePath class文件路径
 */
private static void addToast(String filePath) {
    try {
        CtClass ctClass = classPool.getCtClass(getFullClassName(filePath));
        if (ctClass.isFrozen()) {
            ctClass.defrost();
        }
        // 获取Activity中的onCreate方法
        CtMethod onCreate = ctClass.getDeclaredMethod("onCreate");
        // 要插入的代码
        // getSimpleClassName()方法返回的是类名如"MainActivity"
        String insertCode = String.format(Locale.getDefault(),
                "Toast.makeText(this, \"%s\", Toast.LENGTH_SHORT).show();",
                getSimpleClassName(filePath));
        // 在onCreate方法开始处插入上面的Toast代码
        onCreate.insertBefore(insertCode);
        // 写回原来的目录下,覆盖原来的class文件
        // mainModuleClassPath是Android项目主module存放class的路径,一般是"<Android项目根目录>/app/build/intermediates/javac/debug/classes/"
        ctClass.writeFile(mainModuleClassPath);
        ctClass.detach();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

在Activity的onCreate方法中插入Toast的处理代码非常简短,以上代码仅仅调用了CtMethod的insertBefore方法即可在某个方法开始处插入新代码。

要验证以上代码是否正常工作也非常简单,直接打开编译后的class文件即可,或者运行项目到手机或模拟器,可以看到每个Activity在打开时都会弹出toast提示当前的Activity名称。

处理点击事件重复触发

Android AOP编程(二)——AspectJ语法&实战这篇文章中,我记录过使用AspectJ处理点击事件重复触发的问题,也是使用自定义注解,结合AspectJ匹配注解来完成的。本篇将使用Javassist库完成同样的功能。

首先需要创建一个java-library,这里命名为annotation,在annotation的根目录下编辑build.gradle文件,内容如下:

apply plugin: 'java-library'
apply plugin: 'maven'

repositories {
    mavenLocal()
}

dependencies {
    implementation gradleApi()
}

//publish to local directory
group "com.example.annotation"
version "1.0.0"

uploadArchives{ //当前项目可以发布到本地文件夹中
    repositories {
        mavenDeployer {
            repository(url: uri('./repo')) //定义本地maven仓库的地址
        }
    }
}

java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}

之所以上面要配置uploadArchives,是因为在buildSrc中无法直接引用annotation库,需要将annotation库上传到本地maven,再在buildSrc中依赖它。

annotation库中的代码很简单,在src/main/java目录下创建一个com.example.annotation包并在其中添加ClickOnce这个注解,代码如下:

package com.example.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface ClickOnce {
}

然后我们将annotation这个library上传到本地maven,上传的方法为打开AndroidStudio右侧的gradle视图,在其中找到uploadArchives菜单双击即可,如下图所示:
在这里插入图片描述
执行uploadArchives成功后,会在annotation目录下生成repo目录,在其中可以看到上传的library。

为了让buildSrc插件项目可以引用我们创建的annotation,需要在buildSrc目录的build.gradle文件中,加入如下配置:

repositories {
    ...
    maven {
    	// 这个地址填
        url '/Users/xxx/IdeaProjects/AopDemo/annotation/repo/'
    }
}

dependencies {
    ...
    implementation 'com.example.annotation:annotation:1.0.0'
}

我们需要实现的功能是,使用@ClickOnce注解的方法,在600ms内只执行一次,这样就能保证在快速点击时,不重复触发点击事件,在MainActivity中我们加入测试代码如下:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = findViewById(R.id.textView);
        textView.setOnClickListener(this);
    }

    @Override
    @ClickOnce
    public void onClick(View v) {
        startActivity(new Intent(this, OtherActivity.class));
    }
}

然后我们使用InjectUtil工具类来操作class文件,找到被@ClickOnce注解的方法,主要逻辑如下:

/**
 * 处理被@ClickOnce注解的方法,确保这个方法在600ms内只执行一次
 * @param filePath
 */
private static void ensureClickOnce(String filePath) {
    try {
        CtClass ctClass = classPool.getCtClass(getFullClassName(filePath));
        if (ctClass.isFrozen()) {
            ctClass.defrost();
        }
        CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
        // 类中是否有被@ClickOnce注解的方法
        boolean clzHasClickAnnotation = false;
        for (CtMethod m : declaredMethods) {
            if (m.hasAnnotation(ClickOnce.class)) {
                clzHasClickAnnotation = true;
                break;
            }
        }
        // 如果类中有被@ClickOnce注解的方法,则创建新方法,并在所有被@ClickOnce注解的方法开始处插入检查代码
        if (clzHasClickAnnotation) {
            // 创建新方法并添加到类中
            createClickOnceMethod(ctClass);
            // 重新读取并加载class,因为上一步中写入了新的方法
            ctClass = classPool.get(getFullClassName(filePath));
            if (ctClass.isFrozen()) ctClass.defrost();
            declaredMethods = ctClass.getDeclaredMethods();
            for (CtMethod m : declaredMethods) {
                if (m.hasAnnotation(ClickOnce.class)) {
                    System.out.println("found @ClickOnce method: " + m.getName());
                    // 在当前被@ClickOnce注解的方法体前面执行上面创建的新方法
                    m.insertBefore("if (!is$Click$Valid()) {" +
                            "Log.d(\"ClickOnce\", \"Click too fast, ignore this click...\");" +
                            "return;" +
                            "}");
                }
            }
        }
        ctClass.writeFile(mainModuleClassPath);
        ctClass.detach();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

/**
 * 构造一个新的方法,方法体为:
 * private boolean is$Click$Valid() {
 *     long time = System.currentTimeMills();
 *     boolean canClick = (time - lastClickTime > 600L);
 *     lastClickTime = time;
 *     return canClick;
 * }
 * 如果点击有效,上面的方法返回true,否则返回false
 * @param clz
 * @throws Exception
 */
private static void createClickOnceMethod(CtClass clz) throws Exception {
    // 先给类新增一个成员变量,用于记录上次点击的时间
    CtField lastClickField = new CtField(CtClass.longType, "lastClickTime", clz);
    lastClickField.setModifiers(Modifier.PRIVATE | Modifier.STATIC);
    clz.addField(lastClickField);
    // 新的方法体
    String body = "{" +
            "long time = System.currentTimeMillis();" +
            "boolean canClick = (time - lastClickTime > 600);" +
            "lastClickTime = time;" +
            "return canClick;" +
            "}";
    // 创建新方法并添加到类中
    CtMethod newMethod = CtNewMethod.make(Modifier.PRIVATE, CtClass.booleanType, "is$Click$Valid",
            new CtClass[]{}, new CtClass[]{}, body, clz);
    clz.addMethod(newMethod);
    clz.writeFile(mainModuleClassPath);
}

以上代码注释得比较详细了,其主要功能是判断某个类中是否有被@ClickOnce注解的方法,如果有,就在当前类中添加一个is$Click$Valid()方法,用于判断当前点击是否有效,再修改被@ClickOnce注解的方法体,在其中调用is$Click$Valid()方法。经过反编译后可以看到MainActivity的代码长下面这样:

public class MainActivity extends AppCompatActivity implements OnClickListener {
    private TextView textView;
    private static long lastClickTime; // 这是注入的字段

    public MainActivity() {
    }

    protected void onCreate(Bundle savedInstanceState) {
        Toast.makeText(this, "MainActivity", 0).show();
        super.onCreate(savedInstanceState);
        this.setContentView(2131427356);
        this.textView = (TextView)this.findViewById(2131230959);
        this.textView.setOnClickListener(this);
    }

    @ClickOnce
    public void onClick(View v) {
        // 调用注入的方法
        if (!this.is$Click$Valid()) {
            Log.d("ClickOnce", "Click too fast, ignore this click...");
        } else {
            this.startActivity(new Intent(this, OtherActivity.class));
        }
    }

    // 这是注入的新方法
    private boolean is$Click$Valid() {
        long var1 = System.currentTimeMillis();
        boolean var3 = var1 - lastClickTime > (long)600;
        lastClickTime = var1;
        return var3;
    }
}

可以看到新的class类中注入了我们的新代码,并完成了600ms的判断,确保使用@ClickOnce注解的方法在600ms内只执行一次。

统计某个方法执行时长

本例子会实现这样一个功能:使用@CountTime注解某个方法,会自动统计这个方法执行消耗的时间并通过log输出。

首先还是在annotation中定义一个注解:

package com.example.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface CountTime {
}

别忘了要重新发布一下annotation到本地仓库,可以不用修改annotation library的版本号。

然后新建一个OtherActivity,在其中插入测试代码如下:

package com.example.aopdemo;

import android.os.Bundle;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import com.example.annotation.CountTime;

public class OtherActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_other);

        findViewById(R.id.textView).setOnClickListener(v -> {
            doSomething();
        });
    }

    @CountTime
    private void doSomething() {
        Toast.makeText(this, "fb(30) = " + fb(30), Toast.LENGTH_SHORT).show();
    }

    // 计算非波拉契数列第n项的值
    private int fb(int n) {
        if (n == 0 || n == 1) {
            return n;
        }
        return fb(n - 1) + fb(n - 2);
    }
}

测试代码很简单,就是点击页面上的文本,触发点击事件计算非波拉契数列第n项的值,并Toast弹出这个值,我们在doSomething方法上加了自定义注解@CountTime希望统计这个方法执行消耗多长时间。

下面是InjectUtil类对class字节码的处理方式:

/**
 * 通过注入字节码,计算被@CountTime注解的方法执行消耗的时间
 * @param filePath class文件路径
 */
private static void calculateMethodTime(String filePath) {
    try {
        CtClass ctClass = classPool.getCtClass(getFullClassName(filePath));
        if (ctClass.isFrozen()) {
            ctClass.defrost();
        }
        CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
        for (CtMethod m : declaredMethods) {
            if (m.hasAnnotation(CountTime.class)) {
                // 定义long类型的局部变量start
                m.addLocalVariable("start", CtClass.longType);
                // 在方法体开始处插入代码记录时间
                m.insertBefore("start = System.currentTimeMillis();");
                // 定义long类型的局部变量end
                m.addLocalVariable("end", CtClass.longType);
                // 在方法体结束时插入代码记录时间
                m.insertAfter("end = System.currentTimeMillis();");
                // 获取方法名
                String methodName = m.getName();
                String logMsg = "execute " + methodName + " use time: ";
                m.insertAfter(String.format(Locale.getDefault(), "Log.d(\"%s\", \"%s\" + (end - start));", "CountTime", logMsg));
            }
        }
        ctClass.writeFile(mainModuleClassPath);
        ctClass.detach();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

处理的方式很简单,还是匹配到被@CountTime注解的方法,然后在方法体中原有的代码前后分别插入新的代码,用于记录方法开始执行和结束执行的时间,并通过Log输出,编译项目后可以看到OtherActivity被修改成如下样子:

public class OtherActivity extends AppCompatActivity {
    public OtherActivity() {
    }

    protected void onCreate(Bundle savedInstanceState) {
        Toast.makeText(this, "OtherActivity", 0).show();
        super.onCreate(savedInstanceState);
        this.setContentView(2131427357);
        this.findViewById(2131230959).setOnClickListener((v) -> {
            this.doSomething();
        });
    }

    @CountTime
    private void doSomething() {
        long start = System.currentTimeMillis();
        Toast.makeText(this, "fb(30) = " + this.fb(30), 0).show();
        Object var6 = null;
        long end = System.currentTimeMillis();
        Object var8 = null;
        Log.d("CountTime", "execute doSomething use time: " + (end - start));
    }

    private int fb(int n) {
        return n != 0 && n != 1 ? this.fb(n - 1) + this.fb(n - 2) : n;
    }
}

对于这个功能,需要注意的地方有如下几点:

  1. 插入代码用于记录方法开始执行和结束执行的时间,需要使用CtMethod的addLocalVariable方法定义局部变量,然后再插入代码,否则,在方法结束时计算两个时间差时,无法得知之前保存的时间值存在哪个变量里,如果你直接用m.insertBefore("long start = System.currentTimeMillis();");这种方式插入代码,实际上保存时间的变量并不叫start,可以自己尝试一下,编译过程中会报错找不到变量
  2. @CountTime需要注解在没有返回值的方法上,因为以上代码是在方法体执行开始处和末尾处插入代码,如果方法有返回值,肯定是不对的,比如将@CountTime注解在上面的fb()方法上,肯定是有问题的。

修复jar包中的bug

假设我们的项目中引用了一个第三方的jar包,这个jar包中有一些错误需要修复,如果我们通过解压修改源码再打包的方式,可以做到但是比较麻烦,一种比较简单的方式是直接修改jar包中的字节码,从而达到目的。

本例中我们会引入一个有问题的jar包,这个jar包中提供了如下方法:

public class BugCalculator {
    public BugCalculator() {
    }

	// add方法返回的是整型,精度会丢失
    public static int add(float a, float b) {
        System.out.println("result is " + (a + b));
        return (int)(a + b);
    }
}

我们的目的是去掉上面add方法中打印的日志,并且将方法返回值改为float而不是int,下面就看看具体的操作方法吧:

/**
 * 修复jar包中的bug
 * @param jarFilePath jar包路径
 */
public static byte[] fixJarBug(String jarFilePath) throws Exception {
    classPool.appendClassPath(jarFilePath);
    CtClass ctClass = classPool.getCtClass("com.bug.calculator.BugCalculator");
    if (ctClass.isFrozen()) {
        ctClass.defrost();
    }
    // 创建一个新的方法fAdd,接收两个float参数,返回一个float值
    CtMethod newMethod = CtNewMethod.make("public static float fAdd(float a, float b) { return a + b; }", ctClass);
    ctClass.addMethod(newMethod);
    byte[] bytes = ctClass.toBytecode();
    ctClass.detach();
    return bytes;
}

Javassist提供了删除某个类中已存在的方法的API,但是没有提供API修改某个类中已存在的方法的返回值,所以我们直接在com.bug.calculator.BugCalculator类中添加了一个新方法fAdd(直接删除某个类中已有的方法也不太好,因为不确定有没有其他地方调用了这个方法),然后编写测试代码如下:

try {
    // 通过反射调用jar包中新增的方法
    // 注意不能直接显式调用fAdd方法,否则编译不通过
    Class<?> clz = BugCalculator.class;
    Method fAdd = clz.getDeclaredMethod("fAdd", float.class, float.class);
    Object result = fAdd.invoke(clz, 1.5f, 2.2f);
    Log.d("BugCalculator", "fAdd(1.5 + 2.2) = " + result);
    // 调用原来的add方法,返回的是丢失了精度的值
    Log.d("BugCalculator", "add(1.5 + 2.2) = " + BugCalculator.add(1.5f, 2.2f));
} catch (Exception e) {
    e.printStackTrace();
}

将代码跑到真机上,可以看到控制台输出如下:
在这里插入图片描述
可以看到我们在类中添加的方法成功执行。
关于修改Jar包中的bug,这里有如下几点注意事项需要说明:

  1. 针对jar包的修改,需要先在自定义Transform中拿到输入的jar包并匹配到需要修改的jar包,再通过JarFile JarOutputStream JarEntry等API来获取并修改jar包中有问题的class文件
  2. 在类中添加了新的方法后,不能直接在代码中显式调用,这样编译都不会通过,需要使用反射来调用新增的方法。

总结

本篇通过4个小例子记录了使用Gradle插件+TransformAPI+Javassist处理字节码的一些方式,实际上本篇仅仅使用了Javassist,但是某些功能也可以使用AspectJ和ASM去处理。

源码

https://github.com/yubo725/android-aop-demo

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yubo_725

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值