一、前言
在 Android 开发中,要想使用 ASM 库来开发自己的字节码插桩库,需要 Hook Android 的编译流程,基于 Gradle(Gradle 是基于 Groovy 语言来开发的) 的API 来实现 class / lib 文件的遍历与操作。
Hook Android 的 Transform 阶段:
transformClassesWithDexBuilderForXXX(XXX 即是 buildTypes 对应的环境)
(不用翻墙) 查阅 Google 官方的 gradle-api form android
二、模块 & 配置
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
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 使用插件与不使用插件的输出内容 } }
- 自定义 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 这个对象来传递的。
A Transform that processes intermediary build artifacts. Transform负责处理中间内容
For each added transform, a new task is created. The action of adding a transform takes care of handling dependencies between the tasks.This is done based on what the transform processes. The output of the transform becomes consumable by other transforms and these tasks get automatically linked together. 对于每个被加入进来的 transform,都会对应一个 gradle task 的创建。 一个 trasform 的输出,将会被下一个 transform 来消费,这些都是自动链接在一起的。
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』。注:
- 第二个其实不用安装,因为 Kotlin 自带『Show Kotlin Bytecode』;
- 第二个插件我也尝试过,Kotlin 代码下,结果是一样的;
- 千万不要将上图中的两个插件都同时安装,否则 AS(我的是3.5)无法启动(只能手动去目录中删除一个才解决);
- ASM Bytecode Viewer使用
- 打开任意 Java 文件;
- IDE的编辑器右键 -> 选择『ASM Bytecode Viewer』;
- 此时插件会判断:
- 如果该文件已经有 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 路径在:
./app/build/intermediates/javac/环境变体/classes 目录下
- 对于 Kotlin 文件,其 Class 路径在:
./app/build/tmp/kotlin-classes/环境变体/ 目录下
- 之后 Android Gradle Plugin 会将所有的 classes 文件,合并成不同的 dex,统一放在:
./app/build/intermediates/transforms/环境变体/ 目录下
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 最下面有个提示:
Session 'app': Installation did not succeed. The application could not be installed: INSTALL_FAILED_INVALID_APK
我们切换到 Run 面版,以及查看 transforms 路径
dexBuilder 目录下是空的,没有 dex 文件,也就没有 apk 文件。
还记得我前面说的吗?\color{red}{还记得我前面说的吗?}还记得我前面说的吗?
Transform 是中间产物链中的一环,它的输出,会影响到下一个 Transform 的输入,而 transform 方法是空函数,如果我们没有正确设置 TrannsformOutputProvider,那么,下一个环节『Task :app:transformClassesWithDexBuilderForDebug』无法将 classes 转成 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 数据结构),在这里只需让大家了解一下流程:
- IO读取 Class 文件;
- 基于IO流,创建 ClassReader 实例;
- 创建 ClassWriter 实例,用于修改字节码;
- 基于 ClassWriter 创建 ClassVisitor 实例;
- 触发 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源文件:我们只有一个方法,编译生成时,有两个方法,其中一个是默认构造函数 ;
- 对于Kotlin源文件:我们只有一个方法,编译生成时,有四个方法,一个是默认构造函数 ,还有两个是 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.0' 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』来查看,做反复比对,寻找正确的解决方案。
Github源码地址:GitHub - qingye/AndroidAOPDemo