0
点赞
收藏
分享

微信扫一扫

《Android AOP探密系列》一步一步跟我ASM实战

一、前言

在 Android 开发中,要想使用 ASM 库来开发自己的字节码插桩库,需要 Hook Android 的编译流程,基于 Gradle(Gradle 是基于 Groovy 语言来开发的) 的API 来实现 class / lib 文件的遍历与操作。

二、模块 & 配置

2.1、新建模块

基于 Android Studio 新建『Module』 ,选择『Java Library』,输入模块名和包名,然后完成。

2.2、依赖配置

  • 首先,我们是基于 Gradle(即 Groovy)来开发的,所以需要引入『groovy插件』;
  • 其次,我们需要依赖『ASM』库;
  • 再次,我们还要使用『Gradle的API』;

OK,配置如下:

apply plugin: 'groovy'
apply plugin: 'java'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    // 使得 gradle 提供的 api
    implementation gradleApi()
    implementation localGroovy()

    // 最新版本的 asm 库是 9.0
    implementation 'org.ow2.asm:asm:9.0'
    implementation 'org.ow2.asm:asm-commons:9.0'
    
    // 目前最新的 gradle for android 的工具是 3.5
    implementation 'com.android.tools.build:gradle:3.5.0'
}

sourceCompatibility = "8"
targetCompatibility = "8"

2.3、实现 Gradle 的API:Plugin

  • 在 main 目录下创建『groovy』目录;
  • 创建包名;
  • 新建 groovy 类;

代码截图如下:

这是我们整个流程入口,但是,大家发现一个问题没?我们的 AopAsmPlugin 这个类有个波浪线,表明没有被引用,那如何解决呢?

2.4、Gradle Plugin 配置

在『main』目录下,新建『resources』目录,并依次创建『META-INF』目录,及再其下再创建『gradle-plugins』目录,然后创建一个自定义名称的 properties 文件,并填写如下:

我们看到,完成了 properties 文件定义后,我们的入口类『AopAsmPlugin』就自动被引用了。

2.5、继承 Android Build 提供的API:Transform

继承 Transform,必需要实现以下四个方法

package com.chris.aop.asm.plugin;

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.TransformInvocation;
import com.android.build.gradle.internal.pipeline.TransformManager;

import java.io.IOException;
import java.util.Set;

public class AopAsmTransform extends Transform {
    @Override
    public String getName() {
        return "AopAsmPlugin";
    }
    
    @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 {
        // 重载该方法,用来 Hook 住 class -> dex 的过程
        // 这里就是我们来读取 class 文件,进行 asm 操纵的真正入口
        // 这里一定要实现,否则在 ./build/intermediates/transforms/dexBuilder目录下,是空目录
        // 可以前后对比 dexBuilder 使用插件与不使用插件的输出内容
    }
}

2.6、Gradle Plugin 注册自定义 Transform(registerTransform)

插件入口处注册 自定义 Transform

// AopAsmPlugin.groovy
package com.chris.aop.asm.plugin

import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

class AopAsmPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        if (!project.plugins.hasPlugin("com.android.application")) {
            throw new Exception("AopAsmPlugin must run at application")
        }

        /*****************************************************************************
         * 注册 Transform
         *****************************************************************************/
        def extension = project.extensions.getByType(AppExtension)
        extension.registerTransform(new AopAsmTransform())
    }
}
  • 自定义 Transform 重载 transform 方法
// AopAsmTransform.java
package com.chris.aop.asm.plugin;

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.TransformInvocation;
import com.android.build.gradle.internal.pipeline.TransformManager;

import java.io.IOException;
import java.util.Set;

public class AopAsmTransform extends Transform {
    @Override
    public String getName() {
        return "AopAsmPlugin";
    }

    @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 {
        System.out.println("=========> " + System.currentTimeMillis());
        // 重载该方法,用来 Hook 住 class -> dex 的过程
        // 这里就是我们来读取 class 文件,进行 asm 操纵的真正入口
        // 这里一定要实现,否则在 ./build/intermediates/transforms/dexBuilder目录下,是空目录
        // 可以前后对比 dexBuilder 使用插件与不使用插件的输出内容
    }
}

三、窥探transform

3.1、QualifiedContent

该接口定义了一个输入内容的类型(ContentType)和表现形式(Scope),它有两个子接口:

  • DirectoryInput
  • JarInput

这两个子接口就代表了两种文件:目录/Class文件、Jar/AAR包;输入源(XXXInput)取决于你的 ContentType + Scope 的定义。

package com.android.build.api.transform;

import com.android.annotations.NonNull;
import java.io.File;
import java.util.Set;

public interface QualifiedContent {
    interface ContentType {
        .......
    }
    enum DefaultContentType implements ContentType {
        .......
    }


    interface ScopeType {
        .......
    }
    enum Scope implements ScopeType {
        .......
    }

    /**
     * 返回输入源的名称,不可信任(因为会用在 transform 的不同阶段)
     */
    @NonNull
    String getName();

    @NonNull
    File getFile();

    /**
     * 定义输入流的类型(Class/Jar/AAR,或者是 Resources)
     * 但是,即便指定了期望 transform 的类型,实际也可能会返回包含有其它类型的文件(源英文如下):
     *
     * Even though this may return only {RESOURCES} or {CLASSES}, the actual content (the folder
     * or the jar) may contain files representing other content types. This is because the 
     * transform mechanism avoids duplicating files around to remove unwanted types for performance.
     */
    @NonNull
    Set<ContentType> getContentTypes();

    @NonNull
    Set<? super Scope> getScopes();
}

3.1.1、QualifiedContent.ContentType

public interface QualifiedContent {
    /**
     * A content type that is requested through the transform API.
     */
    interface ContentType {
        String name();
        int getValue();
    }
    enum DefaultContentType implements ContentType {
        // 可能是 JAR/AAR,也可能是 Directory
        CLASSES(0x01),

        /** The content is standard Java resources. */
        RESOURCES(0x02);
    }
    ......
}

3.1.2、QualifiedContent.Scope

public interface QualifiedContent {
    /**
     * Definition of a scope.
     */
    interface ScopeType {
        String name();
        int getValue();
    }
    enum Scope implements ScopeType {
        /** 主项目模块 */
        PROJECT(0x01),
        /** 子项目或子模块 */
        SUB_PROJECTS(0x04),
        /** 外部依赖库 jar/aar */
        EXTERNAL_LIBRARIES(0x10),
        /** 用于当前环境变体的测试代码,包括其依赖荐 */
        TESTED_CODE(0x20),
        /** Local or remote dependencies that are provided-only */
        PROVIDED_ONLY(0x40),

        /**
         * 项目本地jar/aar包,改用 EXTERNAL_LIBRARIES
         */
        @Deprecated
        PROJECT_LOCAL_DEPS(0x02),
        /**
         * 子项目本地jar/aar包,改用 EXTERNAL_LIBRARIES
         */
        @Deprecated
        SUB_PROJECTS_LOCAL_DEPS(0x08);
    }
}

3.2、Transform.transform

该方法就是中间结果转换的处理方法,而中间信息(数据)是通过 TransformInvocation 这个对象来传递的。

Transform 是一个『转换链』:

整个编译过程会有多个 Transform 参与:
如果你的项目里用了多个 Gradle Plugin,每个 Plugin 都有Hook Transform 的方法,每个 Plugin 肯定各司其责,因此,就有多个 Transform Chain了。

  • TransformInvocation
/**
 * An invocation object used to pass of pertinent information for a
 * Transform#transform(TransformInvocation) call.
 */
public interface TransformInvocation {
    /**
     * transform 上下文
     */
    @NonNull
    Context getContext();
    /**
     * 输入源(XXXInput):DirectoryInput / JarInput
     */
    @NonNull
    Collection<TransformInput> getInputs();
    /**
     * 引用源:当前 transform 中不会去操作,但可查看
     */
    @NonNull Collection<TransformInput> getReferencedInputs();
    /**
     * Only secondary files that this
     * transform can handle incrementally will be part of this change set.
     * 其它输入源:返回上次编译后改动的文件列表。
     */
    @NonNull Collection<SecondaryInput> getSecondaryInputs();
    /**
     * 输出内容
     */
    @Nullable
    TransformOutputProvider getOutputProvider();
    /**
     * 是否支持增量
     */
    boolean isIncremental();
}

Transform 暂时就讲这么多,不然就跑题了。

四、工欲善其事必先利其器: ASM Bytecode Viewer

  • IDE:默认 Android Studio
  • 安装 IDE 插件

选择安装第一个,安装好了后,会提示『RestartIDE』。注:

  1. 第二个其实不用安装,因为 Kotlin 自带『Show Kotlin Bytecode』;
  2. 第二个插件我也尝试过,Kotlin 代码下,结果是一样的;
  3. 千万不要将上图中的两个插件都同时安装,否则 AS(我的是3.5)无法启动(只能手动去目录中删除一个才解决);
  • ASM Bytecode Viewer使用
    1. 打开任意 Java 文件;
    2. IDE的编辑器右键 -> 选择『ASM Bytecode Viewer』;
    3. 此时插件会判断:
    • 如果该文件已经有 Class 文件,则直接加载显示;
    • 没有则先 Recompile,再加载显示 ;

五、插桩实战

我们有了ASM Plugin工具,但是我们不懂 Java字节码啊!没关系,对于不懂的,我们可以学实践,通过修改 Java 源码,然后查看 ASM 编译后的字节码,前后比对,就能发现不同点,我们只需要记下差异点,去通过我们自己自定义的插件工具,插入到源文件的Class文件对应处即可。

5.1、保存修改前状态

如何保存?方法太多了,要么 Copy & Paste,要么截图,等等.....

[图片上传中...(ViewASM.png-2b9c12-1612181084651-0)]

这个我们修改前的 Java源码(左)和对应的 ASM字节码(右)。

5.2、添加进入/退出时间戳

经过修改 & 对比,我们发现了两处与原来不同的地方,两处改动唯一不同的,就是打印的字符串,其它都相同:

   LDC "Chris"
   NEW java/lang/StringBuilder
   DUP
   INVOKESPECIAL java/lang/StringBuilder.<init> ()V
   LDC "onCreate.onEnter timestamp = " // onEnter / onExit
   INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
   INVOKESTATIC java/lang/System.currentTimeMillis ()J
   INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
   INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
   INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I
   POP

可是知道了指令集,还是不会写,怎么办?ASM库连代码都为我们生成好了,直接用就行(不过建议大家还是学学基本的指令集,不要太依赖工具):

还原 Java 源码,开始编写我们的 Gradle Plugin。

5.3、编译后产物所在路径

再真正开始编写插件前,我们要先了解,我们的 Class 以及 Dex 文件最终会在哪里,这样我们才能查看我们操纵字节码插桩后,是否是正确的。

  • 对于 Java 文件,其 Class 路径在:

  • 对于 Kotlin 文件,其 Class 路径在:

  • 之后 Android Gradle Plugin 会将所有的 classes 文件,合并成不同的 dex,统一放在:

OK,接下来,我们真的要实现我们自定义的插件了!

5.4、自定义插件:AopAsmTransform

我们先尝试着打印输入源,代码与日志分别如下(如何使用插件之后会讲,别急!):

public class AopAsmTransform extends Transform {
    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        // 空方法,删除也可以
        super.transform(transformInvocation);
        System.out.println("【AOP ASM】---------------- begin");

        for (TransformInput input : transformInvocation.getInputs()) {

            for (DirectoryInput di : input.getDirectoryInputs()) {
                System.out.println("DirectoryInput = " + di.getFile().getAbsolutePath());
            }

            for (JarInput ji : input.getJarInputs()) {
                System.out.println("JarInput = " + ji.getFile().getAbsolutePath());
            }

        }

        System.out.println("【AOP ASM】----------------  end  ----------------");
    }
}

App模块编译时,Build 版面日志:

我们可以看到,有 JarInput 和 DirectoryInput 两种输入源,及其绝对路径。

如果细心的同学,应该会发现,AS IDE 最下面有个提示:

我们切换到 Run 面版,以及查看 transforms 路径

dexBuilder 目录下是空的,没有 dex 文件,也就没有 apk 文件。

5.4.1、确保输入源能放到指定的目录下

如果获取文件将要放置在哪里?总不能我们自己写死路径吧!

其实,文件的去向,transformInvocation 对象已经告诉我们了,我们通过 getOutputProvider.getContentLocation ,传入正确的 contentType, scope 和 format ,就能够获取正确的目标目录绝对路径:

public class AopAsmTransform extends Transform {
    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        // 空方法,删除也可以
        super.transform(transformInvocation);
        System.out.println("【AOP ASM】---------------- begin ----------------");

        TransformOutputProvider provider = transformInvocation.getOutputProvider();
        for (TransformInput input : transformInvocation.getInputs()) {

            for (DirectoryInput di : input.getDirectoryInputs()) {
                copyQualifiedContent(provider, di, null, Format.DIRECTORY);
            }

            for (JarInput ji : input.getJarInputs()) {
                copyQualifiedContent(provider, ji, getUniqueName(ji.getFile()), Format.JAR);
            }

        }

        System.out.println("【AOP ASM】----------------  end  ----------------");
    }

    /***********************************************************************************************
     * 重名名输出文件,因为可能同名(N个classes.jar),会覆盖
     ***********************************************************************************************/
    private String getUniqueName(File jar) {
        String name = jar.getName();
        String suffix = "";
        if (name.lastIndexOf(".") > 0) {
            suffix = name.substring(name.lastIndexOf("."));
            name = name.substring(0, name.lastIndexOf("."));
        }
        String hexName = DigestUtils.md5Hex(jar.getAbsolutePath());
        return String.format("%s_%s%s", name, hexName, suffix);
    }

    private void copyQualifiedContent(TransformOutputProvider provider, QualifiedContent file, String fileName, Format format) throws IOException {
        boolean useDefaultName = fileName == null;
        File dest = provider.getContentLocation(useDefaultName ? file.getName() : fileName, file.getContentTypes(), file.getScopes(), format);
        if (!dest.exists()) {
            dest.mkdirs();
            dest.createNewFile();
        }

        if (useDefaultName) {
            FileUtils.copyDirectory(file.getFile(), dest);
        } else {
            FileUtils.copyFile(file.getFile(), dest);
        }
    }
}

再次编译,效果如下:

能正常编译、通过,并且成功安装应用。
虽然成功了,但是,目录下多了很多的 jar 文件,对比我们 『5.3』中的图片,我们查看一下content.json文件:

发现,这些 jar 包全是外部依赖,『5.4中 Build 日志』中可以看到这些外部依赖有些是 kotlin的,有些是 Maven / JCenter 仓库中的;

5.4.2、递归遍历 Class 文件

因为本篇 Demo 暂不涉及到 Jar 包中 Class 文件的操纵,因此,我们只关注 DirectoryInput 中的 Class 文件(Jar包中的 Class 操作与之类似)。由于Java 的包名,与目录层次紧密相关,因此,我们需要一级一级目录向下递归来遍历出那些我们真正需要插桩的 Class 文件,有些 Class 文件可能是资源、编译配置等,所以我们还需要将这些给过滤掉。

// TransConstant.java
public class TransConstant {
    // 配置过滤文件信息
    public static final String[] CLASS_FILE_IGNORE = {"R.class", "R$", "Manifest", "BuildConfig"};
}
public class AopAsmTransform extends Transform {
    /***********************************************************************************************
     * 根据输入的目录,遍历需要插桩的 Class 文件
     ***********************************************************************************************/
    private void doDirectoryInputTransform(DirectoryInput input) {
        List<File> files = new ArrayList<>();
        listFiles(files, input.getFile());

        // TODO: 实际要插桩的 Class 文件
       ......
    }
    
    private void listFiles(List<File> list, File file) {
        if (file == null) {
            return;
        }
    
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            if (files == null || files.length == 0) {
                return;
            }
    
            for (File f : files) {
                listFiles(list, f);
            }
        } else if (needTrack(file.getName())) {
            list.add(file);
        }
    }
    
    private boolean needTrack(String name) {
        boolean ret = false;
        if (name.endsWith(".class")) {
            int len = TransConstant.CLASS_FILE_IGNORE.length;
            int i = 0;
            while (i < len) {
                if (name.contains(TransConstant.CLASS_FILE_IGNORE[i])) {
                    break;
                }
                i ++;
            }
            if (i == len) {
                ret = true;
            }
        }
        return ret;
    }
}

六、万事俱备,只待插桩

6.1、了解ASM框架

在插桩前,我们需要先稍微了解下 ASM 这个框架(之后我会单独开一篇文章,讲解 Class 数据结构),在这里只需让大家了解一下流程:

  1. IO读取 Class 文件;
  2. 基于IO流,创建 ClassReader 实例;
  3. 创建 ClassWriter 实例,用于修改字节码;
  4. 基于 ClassWriter 创建 ClassVisitor 实例;
  5. 触发 ClassReader 对象解析 Class 信息;

从上面的步骤可以看出,ASM 用到了访问者模式来读取和解析 Class 信息的,下图是访问 / 遍历流程:

6.2、继承 ClassVisitor 类

ASM 提供的 ClassVisitor 是一个抽象类,需要我们去继承;对于上面的流程,如果我们需要针对某块代码,例如:Annotation、Field、Method 等读取或是修改,就需要重载相应的方法;本篇是针对 Method 进行插桩,因此我们需要重载『visitMethod』方法,以及还要使用到 Method访问者类,在 ASM中,该类为『AdviceAdapter』抽象类,我们继承它,并需要重载『onMethodEnter』和『onMethodExit』方法,来实现我们的需求。

6.2.1、自定义 ClassVisitor 继承类

// ClassVisitorAdapter.java
public class ClassVisitorAdapter extends ClassVisitor {
    private String clazzName;

    public ClassVisitorAdapter(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        this.clazzName = name;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions);
        return new MethodAdviceAdapter(api, methodVisitor, access, name, descriptor, this.clazzName);
    }
}

6.2.2、自定义 MethodVisitor 继承类

// MethodAdviceAdapter.java
public class MethodAdviceAdapter extends AdviceAdapter {
    private String qualifiedName;
    private String clazzName;
    private String methodName;
    private int access;
    private String desc;

    protected MethodAdviceAdapter(int api, MethodVisitor methodVisitor, int access, String name, String descriptor, String clazzName) {
        super(api, methodVisitor, access, name, descriptor);
        this.qualifiedName = clazzName.replaceAll("/", ".");
        this.clazzName = clazzName;
        this.methodName = name;
        this.access = access;
        this.desc = descriptor;
    }

    @Override
    protected void onMethodEnter() {
        enter();
    }

    @Override
    protected void onMethodExit(int opcode) {
        exit(opcode);
    }

    private void enter() {
        System.out.println("【MethodAdviceAdapter.enter】 => " + clazzName + ", " + methodName + ", " + access + ", " + desc);
    }

    private void exit(int opcode) {
        System.out.println("【MethodAdviceAdapter.exit】 => " + opcode);
    }
}

编译再测试一下我们的输出结果:

> Task :app:transformClassesWithAopAsmPluginForDebug
【AOP ASM】---------------- begin ----------------

【MethodAdviceAdapter.enter】 => com/chris/aop/MainActivity, <init>, 1, ()V
【MethodAdviceAdapter.exit】 => 177
【MethodAdviceAdapter.enter】 => com/chris/aop/MainActivity, onCreate, 4, (Landroid/os/Bundle;)V
【MethodAdviceAdapter.exit】 => 177
 /Users/chris/Desktop/Source/AOPDemo/app/build/intermediates/javac/debug/classes/com/chris/aop/MainActivity.class/MainActivity.class

【MethodAdviceAdapter.enter】 => com/chris/aop/SecondActivity, onCreate, 4, (Landroid/os/Bundle;)V
【MethodAdviceAdapter.exit】 => 177
【MethodAdviceAdapter.enter】 => com/chris/aop/SecondActivity, <init>, 1, ()V
【MethodAdviceAdapter.exit】 => 177
【MethodAdviceAdapter.enter】 => com/chris/aop/SecondActivity, _$_findCachedViewById, 1, (I)Landroid/view/View;
【MethodAdviceAdapter.exit】 => 176
【MethodAdviceAdapter.enter】 => com/chris/aop/SecondActivity, _$_clearFindViewByIdCache, 1, ()V
【MethodAdviceAdapter.exit】 => 177
 /Users/chris/Desktop/Source/AOPDemo/app/build/tmp/kotlin-classes/debug/com/chris/aop/SecondActivity.class/SecondActivity.class

【AOP ASM】----------------  end  ----------------
  • 对于Java源文件:我们只有一个方法,编译生成时,有两个方法,其中一个是默认构造函数 <init>;
  • 对于Kotlin源文件:我们只有一个方法,编译生成时,有四个方法,一个是默认构造函数 <init>,还有两个是 kotlin 合成的方法,快速根据控件id查找视图控件,以及退出时清除内存;

6.3、插入打印日志

public class MethodAdviceAdapter extends AdviceAdapter {
    private String qualifiedName;
    private String clazzName;
    private String methodName;
    private int access;
    private String desc;

    protected MethodAdviceAdapter(int api, MethodVisitor methodVisitor, int access, String name, String descriptor, String clazzName) {
        super(api, methodVisitor, access, name, descriptor);
        this.qualifiedName = clazzName.replaceAll("/", ".");
        this.clazzName = clazzName;
        this.methodName = name;
        this.access = access;
        this.desc = descriptor;
    }

    @Override
    protected void onMethodEnter() {
        enter();
    }

    @Override
    protected void onMethodExit(int opcode) {
        exit(opcode);
    }

    private void enter() {
        // 如果是构造函数则跳过
        if (methodName.equals("<init>")) {
            return;
        }

        mv.visitLdcInsn("Chris");
        mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
        mv.visitInsn(DUP);
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
        mv.visitLdcInsn(qualifiedName + ".onCreate.onEnter timestamp = ");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
        mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        mv.visitInsn(POP);

    }

    private void exit(int opcode) {
        if (methodName.equals("<init>")) {
            return;
        }

        mv.visitLdcInsn("Chris");
        mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
        mv.visitInsn(DUP);
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
        mv.visitLdcInsn(qualifiedName+ ".onCreate.onExit timestamp = ");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
        mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        mv.visitInsn(POP);
    }
}

重新编译,查看编译后的 Class 文件是否插入了我们写入的字节码

我们再看看 Kotlin 是否也同样插桩了

如我们所想,Kotlin 也完美插桩,并且,大家仔细看『_$_findCachedViewById』方法,return 前也正确插入了我们的日志代码。

运行APP,查看 Logcat:

七、APP使用AopAsmPlugin

7.1、使用 Gradle + Maven插件 打包与发布

查看《自定义Gradle Plugin远程发布》,了解如何制作本地maven包,以及发布至 JCenter 中央仓库。

7.2、配置顶级build.gradle依赖

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    ext.kotlin_version = '1.3.72'
    repositories {
        google()
        jcenter()
        maven {
            url uri('./repo')
        }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

        classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.5'

        classpath 'com.chris.aop:aop-asm-gradle-plugin:1.0.0' // 添加依赖
    }
}

apply from: rootProject.file('gradle/project-mvn-config.gradle')

allprojects {
    repositories {
        google()
        jcenter()
        maven {
            url uri('./repo')
        }
    }

    tasks.withType(Javadoc) {
        options.addStringOption('Xdoclint:none', '-quiet')
        options.addStringOption('encoding', 'UTF-8')
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

7.3、配置App的build.gradle

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

apply plugin: 'com.chris.aop.asm-plugin' // apply 插件

android {.....}
dependencies {....}

八、总结

本篇内容,只是一个抛砖引玉,让大家了解 Java 字节码的修改,其实在现有的工具下,一切都变的非常的简单和容易;当然,我们在实际修改字节码时,也会遇到各种奇怪的遭遇,这时,我们要多通过修改源码,并用『ASM Bytecode Viewer』来查看,做反复比对,寻找正确的解决方案。

举报

相关推荐

0 条评论