Android 插桩之美,全面掌握

这篇具有很好参考价值的文章主要介绍了Android 插桩之美,全面掌握。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

链接:

https://www.yuque.com/docs/share/b49a3274-17d9-4c87-815a-4a13ab72e4e0

1

插桩

插桩是什么?你在开发中有用过插桩的技术吗?

所谓的插桩就是在代码编译期间修改已有的代码或者生成新代码。

插桩具体在编译的哪个流程介入呢?

2

插桩的作用与场景

代码生成

代码监控

代码修改

代码分析

Java 源文件方式

类似 AndroidAnnotation/APT(Annotation Processing Tool),可以在代码编译期解析注解,并且生成新的 Java 文件,减少手动的代码输入。这些代码生成的场景,它们生成的都是 Java 文件,是在编译的最开始介入。典型的有 Greendao、ButterKnife。

Android 插桩之美,全面掌握

上图是我们项目内在用的 ORM 映射数据库 Greendao。可见 build 目录下有很多 *.java 后缀的文件,build一般都是放置编译生成后的产物,很显然这些文件就是在我们 build 时候通过注解处理器产生的 Java 文件。

字节码

对于代码监控、代码修改以及代码分析这三个场景,一般采用操作字节码的方式。可以操作“.class”的 Java 字节码,也可以操作“.dex”的 Dalvik 字节码,这取决于我们使用的插桩方法,相对于 Java 文件方式,字节码操作方式功能更加强大,应用场景也更广,但是它的使用复杂度更高。

Java 字节码

对于 Java 平台,Java 虚拟机运行的是 Class 文件,内部对应的是 Java 字节码。

Dalvik 字节码

Android 这种嵌入式平台,为了优化性能,Android 虚拟机运行的是 Dex 文件。dex 我们可以理解为 Android 为移动设备(受限于早年的手机配置远低于 PC) 研发的 class 的压缩格式。Android SDK 工具包里面有 dx 工具可以将 class 文件打包成 dex。又由 Android 虚拟机的 PathClassLoader 装载到内存中。

身边经历过的案例

火箭兔

之前的项目工程,有 Java + kotlin + Flutter 混编,还大量应用了一些注解框架例如:ButterKnife 、Dagger、Eventbus 导致编译的耗时非常感人。时常可能在代码调试的时候只是想增加一行 Log 日志,但是编译花费的时间可能在 3~5 分钟。

Android 插桩之美,全面掌握

使用火箭兔进行增量编译示例:

Android 插桩之美,全面掌握

我们可以看到上图我们修改了一个 Java 文件和一个资源文件。增量编译重启 run 起 apk 的时间仅仅只花了 10s。当然这种增量编译的实现方式实现起来除了要求插桩的技术以外你还需要了解很多其他的诸如编译原理、脚本语言等综合方面的知识,实现成本不低。但是如果能在团队中推广使用用的人越多 ROI (Return on Investment)也就越高。

全局水印

Android 插桩之美,全面掌握

类似于钉钉、飞书这类办公软件,都有防截图的需求。现在甲方就需求在 APP 的所有 activity 以及 dialog 上加上全局水印,例如带上员工的名称工号等,一个 APP 常规来讲页面加上弹窗可能有上百个。好,我们人多加班加点干,但是应用内的第三方的 Activity ,和 Dialog 你怎么办呢?

Android 插桩之美,全面掌握

通过 AspectJ 的插桩技术我们把几天的工作量可能还容易出错漏的需求,短短几个小时就完成了。掌握了插桩的技术再能想到好的思路(Hook点),能极大的提高我们的工作效率。

3

插桩方案的对比

AspectJ

AspectJ 的作为一个老牌的插桩框架优点是 1 成熟稳定 2 使用简单。但是 AspectJ 的缺点是,由于其基于规则,所以其切入点相对固定,对于字节码文件的操作自由度以及开发的掌控度就大打折扣。还有就是如果我们要实现对所有方法进行插桩,代码注入后的性能也是我们需要关注的一个重要的点,我们希望只插入我们想插入的代码,而AspectJ会额外生成一些包装代码,对性能以及包大小有一定影响。AspectJX:

https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx

Javassist

Javassist 源代码级 API 比 ASM 中实际的字节码操作更容易使用。Javassist 在复杂的字节码级操作上提供了更高级别的抽象层。Javassist 源代码级 API 只需要很少的字节码知识,甚至不需要任何实际字节码知识,因此实现起来更容易、更快。Javassist使用反射机制,这使得它比运行时使用 Classworking 技术的ASM慢。Javassist:

https://github.com/jboss-javassist/javassist

ASM

相比 AspectJ,ASM 更加直接高效。但是对于一些复杂情况,我们可能需要使用另外一种 Tree API 来完成对 Class 文件更直接的修改,因此这时候你要掌握一些必不可少的 Java 字节码知识,ASM 的特点是功能强大操作灵活,但是上手的难度也会比 AspectJ 更难,但是它能获得更好的性能,更适合大面积的插桩场景。ASM:

https://asm.ow2.io/

4

插桩实战

Android 插桩之美,全面掌握

ASM

我们先对 ASM 的三个重要的角色有个了解,他们分别是:

1. ClassReader

2. ClassVisitor

3. ClassWirter

ClassReader

我们通过上文的内容大概了解到,ASM 插桩对字节码进行修改。这肯定有个读取 Class 字节码的过程。那么 ClassReadr 就是这个读取器,他提供了对字节码的读取的方法。

ClassVisitor

ClassVisitor 是 ASM 插桩的核心,因为字节码的插桩修改就是在这一个步骤进行。ClassVisitor 是基于 访问者模式:

https://www.runoob.com/design-pattern/visitor-pattern.html

ClassWirter

顾名思义,这应该是 ASM 提供的对字节码修改完以后,将修改完的内容进行写入的工具。写入的对象包括上面读取的对象都可以是字节码数组或者他们包装了一层的字节码流(Strem)。

了解了上面的 ASM 的三个核心 API 的左右下面我们来进行一个小案例的使用实践:

ASM API 实践

classPrinter{

publicstaticvoidmain( String[] args) {

b;

}

privatestaticvoidb{

longs = System.currentTimeMillis;

try{

Thread.sleep( 2000);

} catch(InterruptedException e) {

e.printStackTrace;

}

longe = System.currentTimeMillis;

System. out.println( "executed time : "+ (e - s) + " :ms");

}

}

我们在排查一些问题的时候,可能需要去根据某个方法的耗时时间来做定位和判断。一个或者几个方法我们可以通过像上文一样手动去编写。但是我需要给数百个方法或者整个应用所有的方法全部添加这个时候靠人力手动是不现实的,这个时候如何去解决?

依赖包引入

implementationgroup: 'org.ow2.asm', name: 'asm-commons', version: '9.2'

读取目标字节流

FileInputStream fis = newFileInputStream( "/Users/macbook/Documents/thunder/ASMDemo/app/build/intermediates/javac/debug/classes/com/thunder/asmdemo/zxm31/Printer.class");

ClassReader classReader = newClassReader(fis);

插入

我们先看一段伪代码或者是未完成的代码:

具体的实现我们放在后面,先了解主干流程。

写出修改后的字节码

FileOutputStream fos = new FileOutputStream( "/Users/macbook/Documents/thunder/ASMDemo/app/build/intermediates/javac/debug/classes/com/thunder/asmdemo/zxm31/Printer.class");

ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES);

fos. write(classWriter.toByteArray);

fos. flush;

fos. close;

Visitor

上文我们先暂时没有实现 accept方法,这里我们完成它细节的实现:

//真正改变字节码的插桩的核心`

classReader.accept( newTimeClassVisitor(ASM9, classWriter), ClassReader.EXPAND_FRAMES);

accept 方法接受一个 ClassVisitor的一个入参:

staticclassTimeClassVisitorextendsClassVisitor{

publicTimeClassVisitor( intapi, ClassVisitor classVisitor) {

super(api, classVisitor);

}

}

ClassVisitor 内部有很多方法可以重写,我们的需求是需要对方法进行插入。这里我们来实现方法相关的函数。

@Override

public MethodVisitor visitMethod( intaccess, Stringname, Stringdeor, Stringsignature, String[] exceptions) {

returnsuper.visitMethod(access, name, deor, signature, exceptions);

}

visitMethod 会分析字节码把字节码内所有的方法给我们输出出来,我们来验证一下:

Android 插桩之美,全面掌握

方法的构造、入口方法、b 方法一目了然,但是ClassVisitor 只是提供了对类里面的元素的方法,我想具体的将我们自己的代码插入方法体怎么做呢?

我注意到visitMethod的返回值是 MethodVisitor,没有错就是它:

Android 插桩之美,全面掌握

我们来看下MethodVisitor 内部提供了各种对方法体操作的 API ,但是针对我们这样在方法前后进行代码插入统计的需求,commons 包已经给我们提供了更简便的子类实现 AdviceAdapter,来查看一下它的继承关系:

Android 插桩之美,全面掌握

查看一下里面和method 相关的方法:

Android 插桩之美,全面掌握

onMethodEnter

onMethodExit

正是两个空实现给我们去重写,在方法前后做插入使用 , 我们来做下日志打印的验证。

Android 插桩之美,全面掌握

唉~ 是我们想要的效果。铺垫了这么多,我们现在终于可以开始进行字节码插桩的修改了:

我们在 enter 想要插入的是:

longs = System.currentTimeMillis;

在 exit 想要插入的是:

longe = System.currentTimeMillis;

System. out.println( "executed time : "+ (e - s) + " :ms");

我们来看下完整的字节码指令:

classcom/ thunder/ asmdemo/ zxm31/ Printer{

// access flags 0x0

<init>V

ALOAD 0

INVOKESPECIAL java/lang/Object.<init> V

RETURN

MAXSTACK = 1

MAXLOCALS = 1

// access flags 0x9

publicstaticmain([Ljava/lang/String;)V

// parameter args

INVOKESTATIC com/thunder/asmdemo/zxm31/Printer.b V

RETURN

MAXSTACK = 0

MAXLOCALS = 1

// access flags 0xA

privatestaticbV

TRYCATCHBLOCK L0 L1 L2 java/lang/InterruptedException

INVOKESTATIC java/lang/System.currentTimeMillis J

LSTORE 0

L0

LDC 2000

INVOKESTATIC java/lang/Thread.sleep (J)V

L1

GOTOL3

L2

ASTORE 2

ALOAD 2

INVOKEVIRTUAL java/lang/InterruptedException.printStackTrace V

L3

INVOKESTATIC java/lang/System.currentTimeMillis J

LSTORE 2

GETSTATIC java/lang/System.out : Ljava/io/PrintStream;

NEWjava/lang/StringBuilder

DUP

INVOKESPECIAL java/lang/StringBuilder.<init> V

LDC "executed time : "

INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;

LLOAD 2

LLOAD 0

LSUB

INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;

LDC " :ms"

INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;

INVOKEVIRTUAL java/lang/StringBuilder.toString Ljava/lang/String;

INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V

RETURN

MAXSTACK = 6

MAXLOCALS = 4

}

看着上面一堆的字节码指令,我们依稀的可以看到 b 方法里面的内容:

似乎有一句

INVOKESTATIC java/lang/System.currentTimeMillis J

大概能看懂是调用一个静态方法,对应的呢我们也在 visit 中找到了一个:

Android 插桩之美,全面掌握

我们简单的分析下第一行语句的字节码。

JVM 字节码对照语句

methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "J", false);

methodVisitor.visitVarInsn(LSTORE, 2);

Android 插桩之美,全面掌握

如果每一行指令集我们都要去这样的处理。对我们的字节码知识的要求以及对 ASM 的 API 的掌握程度有很高的要求,还慢容易出现错漏上手要求高,有没有更简便的办法呢?有:

ASM Bytecode Viewer

后文会介绍安装方式,我们先来看下使用:

Android 插桩之美,全面掌握

通过这个插件,我们能够直接将 Java&Kotlin 的源代码转化成 ASM 需要的 API,如上图:

@Override

protectedvoidonMethodEnter{

System.out.println( "start : ");

visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "J", false);

visitVarInsn(LSTORE, 0);

super.onMethodEnter;

}

@Override

protectedvoidonMethodExit( intopcode) {

System.out.println( "end : ");

visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "J", false);

visitVarInsn(LSTORE, 2);

visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");

visitTypeInsn(NEW, "java/lang/StringBuilder");

visitInsn(DUP);

visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "V", false);

visitLdcInsn( "executed time : ");

visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

visitVarInsn(LLOAD, 2);

visitVarInsn(LLOAD, 0);

visitInsn(LSUB);

visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);

visitLdcInsn( " :ms");

visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "Ljava/lang/String;", false);

visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

super.onMethodExit(opcode);

}

执行,查看Printer.class。

Android 插桩之美,全面掌握

我们的插桩算是成功了,但是有两个小问题。

1 每个方法都给插桩了,其中还存在一些没有意义我们不需要去检测的构造方法,我们是完全不需要插桩的。

2 方法打印这么多没有标识类名和方法名,到时候也分不清楚是哪个类的哪个方法耗费的多少时长。

我们利用注解来解决,我们先来定义一个注解如下:

@Retention(RetentionPolicy.CLASS)

@Target(ElementType.METHOD)

@interfaceStatisticTime {

}

将其标记在我们需要的方法上,然后我们在MethondVisitor 中有没有和注解相关的方法,还真有:

/**

* Visits an annotation of this method.

*

* @paramdeor the class deor of the annotation class.

* @paramvisible { @literaltrue} if the annotation is visible at runtime.

* @returna visitor to visit the annotation values, or { @literalnull} if this visitor is not

* interested in visiting this annotation.

*/

publicAnnotationVisitor visitAnnotation( finalString deor, finalbooleanvisible) {

if(mv != null) {

returnmv.visitAnnotation(deor, visible);

}

returnnull;

}

将它的描述打印:

Android 插桩之美,全面掌握

定义一个成员变量来标识是否对该方法进行注入;

Android 插桩之美,全面掌握

现在只有 b 方法被注入了,我们特地在main 方法上加个@derprecated 注解作为测试,也正确判断了。现在第一个问题得以解决。

解决第二个问题也简单,我们再次对 ASM 的字节码信息进行改造 (onMethidExit) 。方法名好说,直接就在本类 AdviceAdapter中可以获取,类名我们也可以通过ClassVisitor的 visit 方法拿到然后通过构造间接传进来,我们看改造后的字节码以及输出:

Android 插桩之美,全面掌握

ASM Bytecode Outline 失效与解决

很多插桩的文章都提及到 ASM Bytecode Outline 工具。但是实测已经在 AS(4.2.1) 上失效,提示如下:

具体原因可能是 Android Studio 升级版本的兼容性问题,这边亲测一个可用的字节码插桩工具,推荐如下:

Android 插桩之美,全面掌握

Displays bytecode for Java or Kotlin classes and ASMified code which will help you in your class generation. 该工具除了 Java 之外还额外支持 kotlin 的字节码查看与生成。

用法举例:

1 Preference -> Plugins -> Marketplace 中找到该插件 install 重启 AS。

2 右键 Dialog -> ASM Bytecode Viewer。

Android 插桩之美,全面掌握

3 稍等工具的转化时间,就可以再 AS 的右侧面板上找到如下:

Android 插桩之美,全面掌握

ASMified 下方展示的就是我们需要的插桩用的改变字节码文件的片段以及 ASM API 。这个工具能够简化我们插桩开发需要的知识难度,只需要了解少量或者无需了解字节码的知识我们就能完成 ASM 的插桩工作。

GradlePlugin & Transform

通过上文我们已经了解到 ASM 的用法以及字节码插桩的原理,但是似乎还有一个问题。

我们都是通过 Main 方法去做的插桩执行,如何在 Android 中自动使用呢,并且插桩的目标字节码文件都是我们自己去手动读取写入?

针对上述的问题,这个时候 GradlePlugin & Transform 就登场了。

GradlePlugin 的定义有三种方式:

1 脚本 task 方式:

最简单的定义一个 task app build.gradle。

task( "hello"){

doLast {

println( "--------")

}

}

sync now 一下,拉开右侧面板

Android 插桩之美,全面掌握

这种方式一般用来定义一些简单的不需要复用的任务。

2 buildSrc 方式:

这种方式必须要求 module 的命名也是 buildSrc,应该是介于第一种和第三种的方式中间。适合定义本工程目录下使用的方式 buildSrc方式实现自定义Gradle插件。

3 Gradle 插件方式,比第一种复杂。更适合对外发布使用,更适合需要复用的场景。

Gradle 插件自定义

我们来学习一下最复杂的但是应用最广泛的 GradlePlugin 的定义:

Step 1 : 创建一个 module Java 或者 Libray 都可以,最后都得大改。

Android 插桩之美,全面掌握

定义 gradle 目录,添加 groovy 语法需要的相关依赖这样后面写 groovy 文件代码才有提示。这点 intelli IDEA 支持得更好,gradle 结构如下,脚本很简单。就是提供了对 groovy 的支持,记得 sync。

plugins {

id 'groovy'

id 'maven'

id 'java'

}

repositories {

mavenCentral

jcenter

}

dependencies {

implementation gradleApi

implementation localGroovy

}

java {

sourceCompatibility = JavaVersion.VERSION_1_8

targetCompatibility = JavaVersion.VERSION_1_8

}

Step 2 : main 文件下(与 Java 同级) java 文件夹后续没有用到可以删除,创建 groovy 和 resource 资源文件。

groovy 文件夹下就是我们写插件代码的地方,resource提供对外的调用。

需要注意的是,properties 的文件命名是最终在 apply 应用的名字,这个点后文还会再讲到。另外需要注意的是上图中 TimePlugin 是可以正常跳转到 groovy 文件夹下的 TimePlugin.groovy 中,这里容易出错的就是一些包名的细节没有对应上。

我们在 groovy 下的 TimePlugin 做一个简单的 Task 构建:

classTimePluginimplementsPlugin< Project> {

@Override

voidapply(Project project){

project.task( "customPlugin") {

doLast {

println( "I am plugin ~ !")

}

}

}

}

这样其实一个最简单的 gradle 插件已经开发完毕了,这个时候我们怎么运用到我们的应用中呢?

先把插件进行上传,有两种方式一种本地方式、还有一种是将本地的插件产物上传到远程公有仓库或者自己的私有 maven 仓库。其实两者依赖的东西都是一样的,我们这里采用本地的方式:

uploadArchives {

repositories {

mavenDeployer {

//deploy到maven仓库

// 调用方式就是 'com.thunder.plugin:timeplugin:1.0-SNAPSHOT'

pom.groupId = 'com.thunder.plugin'

pom.version = '1.0-SNAPSHOT'

pom.artifactId = 'timeplugin'

repository(url: uri( '../../repo')) //deploy到本地仓库

}

}

}

在 gradle 脚本中我们添加上传到本地的 Task 任务,Sync 后执行这个任务。找到对应的目录我们去看一下插件产物。

Android 插桩之美,全面掌握

好了,所有的准备工作完毕了。终于我们可以在 APP 中应用这个插件了:

自定义插件的应用与验证

首先在整个工程的根build.gradle 下(注意此处是根build.gradle)添加:

Android 插桩之美,全面掌握

然后 Sync,这个时候很有可能成功也有可能遇到如下的情况:

这个报错就是没有找到本地的插件产物,切记检查。

maven{ urluri( '../../repo')}

这个路径的层级需要比 uploadArchives 中的少一级,因为根 gradle 和插件的 gradle 的相对层级就是少一级。

根 gradle 依赖配置完毕然后就是 app 的 gradle 中的应用了。

plugins{

id'com.android.application'

id 'time-plugin'

}

time-plugin 这个 id 刚好和插件中的 resouce 的名称对应上:

Sync 后我们来查看一下 APP 的右侧 task 中有没有我们 plugin 插件中定义的 task。

Android 插桩之美,全面掌握

可以找到,我们执行一下:

Android 插桩之美,全面掌握

至此,我们一个简单的自定义插件完毕。

Transform

完成了插件的自定义和应用后我们来看一下 Transform。

Android 插桩之美,全面掌握

Gradle Transform 是 Android 官方提供给开发者在项目构建阶段(.class -> .dex转换期间)用来修改.class 文件的一套标准API。

Plugin gradle 中添加 :

implementation'com.android.tools.build:gradle:4.2.2', {

excludegroup: 'org.ow2.asm'

}

否则会找不到目标的 Transform,注意它的包名:

时间关系,这边 transform 的处理基本都是模板代码,就是对自身文件目录和第三方的处理。直接上代码了。

importcom.android.build.api.transform.*

importcom.android.build.gradle.internal.pipeline.TransformManager

importorg.apache.commons.io.FileUtils

importorg.objectweb.asm.ClassReader

importorg.objectweb.asm.ClassVisitor

importorg.objectweb.asm.ClassWriter

importorg.objectweb.asm.Opcodes

classTimeTransformextendsTransform{

@Override

String getName{

return"TimeTransform"

}

/**

* 需要处理的数据类型,有两种枚举类型

* CLASS->处理的java的class文件

* RESOURCES->处理java的资源

* @return

*/

@Override

Set<QualifiedContent.ContentType> getInputTypes {

returnTransformManager.CONTENT_CLASS

}

/**

* 指 Transform 要操作内容的范围,官方文档 Scope 有 7 种类型:

* 1. EXTERNAL_LIBRARIES 只有外部库

* 2. PROJECT 只有项目内容

* 3. PROJECT_LOCAL_DEPS 只有项目的本地依赖(本地jar)

* 4. PROVIDED_ONLY 只提供本地或远程依赖项

* 5. SUB_PROJECTS 只有子项目。

* 6. SUB_PROJECTS_LOCAL_DEPS 只有子项目的本地依赖项(本地jar)。

* 7. TESTED_CODE 由当前变量(包括依赖项)测试的代码

* @return

*/

@Override

Set<? superQualifiedContent.Scope> getScopes {

returnTransformManager.SCOPE_FULL_PROJECT

}

/**

* 是否增量编译

* @return

*/

@Override

booleanisIncremental{

returnfalse

}

/**

*

* @paramcontext

* @paraminputs 有两种类型,一种是目录,一种是 jar 包,要分开遍历

* @paramoutputProvider 输出路径

*/

@Override

voidtransform(Context context,

Collection<TransformInput> inputs,

Collection<TransformInput> referencedInputs,

TransformOutputProvider outputProvider,

booleanisIncremental) throwsIOException, TransformException, InterruptedException {

if(!incremental) {

//不是增量更新删除所有的outputProvider

outputProvider.deleteAll

}

inputs.each { TransformInput input ->

//遍历目录

input.directoryInputs.each { DirectoryInput directoryInput ->

handleDirectoryInput(directoryInput, outputProvider)

}

// 遍历jar 第三方引入的 class (第三方的不是我们的插桩目标暂不遍历)

// input.jarInputs.each { JarInput jarInput ->

// handleJarInput(jarInput, outputProvider)

// }

}

}

/**

* 处理文件目录下的class文件

*/

staticvoidhandleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider){

//是否是目录

if(directoryInput.file.isDirectory) {

//列出目录所有文件(包含子文件夹,子文件夹内文件)

directoryInput.file.eachFileRecurse { File file ->

def name = file. name

if(filterClass(name)) {

ClassReader classReader = newClassReader(file.bytes)

ClassWriter classWriter = newClassWriter(classReader, ClassWriter.COMPUTE_MAXS)

ClassVisitor classVisitor = newTimeClassVisitor(Opcodes.ASM9, classWriter)

classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)

byte[] code = classWriter.toByteArray

FileOutputStream fos = newFileOutputStream(file.parentFile.absolutePath + File.separator + name)

fos.write(code)

fos.close

}

}

}

//文件夹里面包含的是我们手写的类以及R.class、BuildConfig.class以及R$XXX.class等

// 获取output目录

def dest = outputProvider.getContentLocation(

directoryInput.name,

directoryInput.contentTypes,

directoryInput.scopes,

Format.DIRECTORY)

//这里执行字节码的注入,不操作字节码的话也要将输入路径拷贝到输出路径

FileUtils.copyDirectory(directoryInput.file, dest)

}

/**

* 检查class文件是否需要处理

* @paramfileName

* @return

*/

staticbooleanfilterClass(String name){

return(name.endsWith( ".class")

&& !name.startsWith( "R\$")

&& "R.class"!= name

&& "BuildConfig.class"!= name)

}

}

注意看到我们需要重写的transform方法就在遍历 APP 中的 class , 其他的代码就是上文 ASM 中照搬过来改下包名基本不需要做改动。

Android 插桩之美,全面掌握

就不一一贴出来了。

最终如何对定义好的transform 做使用呢?

classTimePluginimplementsPlugin< Project> {

@Override

voidapply(Project project){

def android = project.extensions.findByType(AppExtension)

android.registerTransform( newTimeTransform)

}

}

两行代码注册即可,注册完毕后我们更新插件产物,Sync。然后去 APP 里面找 Android 环境试一下:

Android 插桩之美,全面掌握

观察日志:

大功告成,我们从零开始,熟悉 ASM 、GradlePlugin、Transform 实现了一个简单的字节码插桩,虽然简单。但是再复杂的 ASM + GradlePlugin + Transform 的插桩流程和核心就是如此,只是对 API 的掌握程度不一样,未来我们也一定能够利用插桩的技术和 AOP 的思想来更好的帮助我们编程开发。

5

Transform API 的废弃与插桩技术未来

技术一直在迭代变化,有消息称在Gradle 7.0 Transform API 被废弃,这是不是我们之前的学习没有用了呢。我们完全不用担心,目前我们工程里面(Thunder)的 Gradle 版本才 4.8。我们工作开发多年 JDK 也才从 5 ~ 8 的版本。

https://johnsonlee.io/2021/08/02/the-deprecation-of-agp-transform-api/

但是 Oracle 的官网已经更新到了 17,另外一点不管插桩技术怎么变迁那些底下核心的例如:注解、反射、ClassLoader、字节码等核心技术应该还是不会变的、只要我们把这些核心技术掌握好,新的技术出来我们也能够很快去掌握。

结语

插桩技术能就像孙悟空领悟了七十二变。你的脑洞有多大,背后的世界就有多大。

参考资料

https://www.wanandroid.com/blog/show/3054

https://www.jianshu.com/p/16ed4d233fd1

https://github.com/AndroidAdvanceWithGeektime/Chapter-ASM

https://www.jianshu.com/p/e8433c1eb581

https://www.jianshu.com/p/9039a3e46dbc

https://www.jianshu.com/p/dca3e2c8608a

https://mp.weixin.qq.com/s/9vbt73d7nIbTbXVyyRAH3A

https://github.com/JetBrains/kotlin/blob/master/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/K2JVMCompiler.kt

https://www.jianshu.com/p/6785ddb4383f

https://tech.youzan.com/you-zan-android-bian-yi-jin-jie-zhi-lu-zeng-liang-bian-yi-ti-xiao-fang-an-savitar/文章来源地址https://www.toymoban.com/news/detail-410114.html

到了这里,关于Android 插桩之美,全面掌握的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 掌握大数据--Hive全面指南

    1.Hive简介 2.Hive部署方式 3.Hive的架构图 4.Hive初体验 5.Hive SQL语法--DDL操作数据库 Apache Hive是建立在Hadoop之上的一个数据仓库工具,它提供了一种类似于SQL的查询语言,称为HiveQL,用于查询和分析存储在Hadoop分布式文件系统(HDFS)中的大规模结构化数据。以下是Hive的一些主要特

    2024年02月02日
    浏览(44)
  • 掌握大数据--Hadoop全面指南

    在讲Hadoop之前我们需要知道一个问题:为什么我们学习大数据要学习Hadoop 不卖关子 原因如下: 1. 分布式存储:Hadoop提供了分布式存储系统,最著名的是Hadoop Distributed File System(HDFS)。这使得大规模数据能够分布式存储在集群中,提高了数据的容错性和可靠性。 2. 分布式计

    2024年01月22日
    浏览(40)
  • [学习笔记]全面掌握Django ORM

    参考资料:全面掌握Django ORM 学习目标:独立使用django完成orm的开发 学习内容:Django ORM所有知识点 ORM:Object-Relational Mapping Django的ORM详解 在django中,应用的文件夹下面会自动建一个models.py文件。 模型类的优势与劣势 优势:让开发人员专注于业务逻辑处理,提高开发效率。不

    2024年02月15日
    浏览(39)
  • 掌握Spring缓存-全面指南与最佳实践

    第1章:引言 大家好,我是小黑,咱们今天来聊聊缓存,在Java和Spring里,缓存可是个大角色。咱们在网上购物,每次查看商品详情时,如果服务器都要去数据库里翻箱倒柜,那速度得慢成什么样?这就是缓存发光发热的时刻。缓存就像是服务器的“小抽屉”,把经常用到的数

    2024年01月17日
    浏览(40)
  • Linux 统计文件数量:多种方法全面掌握

    你是否想过在 Linux 系统中如何快速地统计某个目录下文件的数量?作为 Linux 用户,这是一个很常见的需求。无论是进行系统维护,还是管理文件,我们都需要掌握这个技能。在本文中,我们将向你介绍多种不同的方法来实现这个目标。 统计目录中的文件数量 统计目录中文件

    2024年02月16日
    浏览(45)
  • 全面掌握胶囊网络:从基础理论到PyTorch实战

    本文全面深入地探讨了胶囊网络(Capsule Networks)的原理、构建块、数学模型以及在PyTorch中的实现。通过本文,读者不仅能够理解胶囊网络的基础概念和高级数学原理,还能掌握其在实际问题中的应用方法。 关注TechLead,分享AI与云服务技术的全维度知识。作者拥有10+年互联网

    2024年02月08日
    浏览(38)
  • JavaWeb图书管理系统课设:全面掌握CRUD操作

    详细介绍JavaWeb图书管理系统的开发过程,包括后端Servlet服务、前端Ajax交互以及MyBatis的增删改查实现,适合初学者巩固JavaWeb知识。

    2024年02月08日
    浏览(48)
  • 04 - Vue3语法系统进阶 - 全面掌握Vue3特性

    Vue是基于MVVM设计模式进行实现的,视图与数据不直接进行通信,但是Vue并没有完全遵循这一原则,而是允许开发者直接进行原生DOM操作。 在Vue中可通过 ref 属性来完成这一行为,通过给标签添加 ref 属性,然后再通过 vm.$refs 来获取DOM 把 ref 属性添加给组件,可以获取到组件的

    2024年02月12日
    浏览(41)
  • Flutter 空安全的糖果罐,带你全面掌握高级知识点

    -h, --[no-]help 显示帮助信息 -p, --path Flutter 项目的根路径 (默认 “.”) -f, --folder assets 文件夹的名字 (默认 “assets”) -w, --[no-]watch 是否继续监听 assets 的变化 (默认 开启) -t, --type pubsepec.yaml 生成配置的类型 “d” 代表以文件夹方式生成 “- assets/images/” “f” 代表以文件方式生成

    2024年04月15日
    浏览(42)
  • HTTPS(面试高频&必须掌握)

    目录 一、HTTPS背景 二、HTTPS 的工作过程 1. 对称加密 2.非对称加密 3. HTTPS 基本工作过程 3.1 使用对称密钥 3.2 引入非对称密钥(面试高频问题) 3.3 黑客的手段 3.4 引入证书 3.5 捋一捋 3.6 SSL/TLS 三、HTTP 与 HTTPS 区别(高频面试题) 四、Tomcat 1.目录介绍 2.服务器的启动 3.部署静态

    2024年02月06日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包