一、前言
在 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』。注:
- 第二个其实不用安装,因为 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 路径在:
- 对于 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 数据结构),在这里只需让大家了解一下流程:
- 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源文件:我们只有一个方法,编译生成时,有两个方法,其中一个是默认构造函数 <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』来查看,做反复比对,寻找正确的解决方案。