Android字节码插桩实现(Gradle + ASM)

在Android编译过程中,往字节码里插入自定义的字节码,称为字节码插桩或函数插桩。

函数插桩可以帮助我们实现很多手术刀式的代码设计,如无埋点统计上报、轻量级AOP等。应用到在Android中,可以用来做用行为统计、方法耗时统计等功能。

字节码实战

需求分析

需求:在Android应用中,记录每个页面的打开\关闭

一般来说就是记录Activity的创建和销毁(这里以Activity区分页面)。所以只要在Activity的onCreate()onDestroy()中插入对应的代码即可。

如何为Activity插入代码?
一个个写?不可能!毕竟我们是高(懒)效(惰)的程序员
写在BaseActivity中?好像可以,不过项目中如果有第三方的页面就显得有些无力了,而且不通用

我们希望实现一个可以自动在Activity的onCreate()onDestroy()中插入代码的工具,可以在任意工程中使用。

于是,自定义Gradle插件 + ASM(Java字节码操纵框架)便成为一个不错的选择

实现思路

Android打包过程自定义Gradle插件了解后发现,java文件会先转化为class文件,然后在转化为dex文件。而通过Gradle插件提供的Transform API,可以在编译成dex文件之前得到class文件。得到class文件之后,便可以通过ASM对字节码进行修改,即可完成字节码插桩

步骤如下:

(1)了解Android打包过程,找到插入点即class转换成dex过程

(2)了解自定义Gradle插件Transform API,在Transform#transform()中得到class文件

(3)找到FragmentActivityclass文件,通过ASM库,在onCreate()中插入代码(为什么是FragmentActivity而不是Activity后面会说到)

(4)将原文件替换为修改后的class文件

Android打包插桩点

class文件:java源文件经过javac后生成一种紧凑的8位字节的二进制流文件

插入点dex节点,表示将class文件打包到dex文件的过程,其输入包括class文件以及第三方依赖的class文件

关于Transform API:从1.5.0-beta1开始,Gradle插件包含一个Transform API,允许第三方插件在将编译后的类文件转换为dex文件之前对其进行操作

关于混淆:关于混淆可以不用当心。混淆其实是个ProguardTransform,在自定义的Transform之后执行。

动手实现

主要实现以下功能:

  • 自定义Gradle插件
  • 处理class文件
  • 替换

自定义Gradle插件

在Android Studio中自定义Gradle插件
Android Gradle Plugin打包Apk过程中的Transform API

目录结构

目录结构分为两部分:插件部分src/main/groovy中)、ASM部分src/main/java中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
lifecycle-plugin
src
main
groovy
cn.appblog.plugin.lifecycle
LifecyclePlugin.groovy
java
cn.appblog.plugin.lifecycle
LifecycleClassVisitor.java
LifecycleOnCreateMethodVisitor.java
LifecycleOnDestroyMethodVisitor.java
resources
META-INF
gradle-plugins
cn.appblog.gradle.properties
build.gradle
build.gradle

build.gradle内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
apply plugin: 'groovy'
apply plugin: 'maven'

repositories {
mavenCentral()
jcenter()
}

dependencies {
//gradle sdk
implementation gradleApi()
//groovy sdk
implementation localGroovy()

implementation 'com.android.tools.build:gradle:3.5.3'
}

//指定编译的编码
tasks.withType(JavaCompile){
options.encoding = "UTF-8"
}


//group和version在后面使用自定义插件的时候会用到
group='cn.appblog.plugin'
version='0.0.1'

uploadArchives {
repositories {
mavenDeployer {
//提交到远程服务器:
// repository(url: "http://www.xxx.com/repos") {
// authentication(userName: "admin", password: "admin")
// }
//本地的Maven地址:当前工程下
repository(url: uri('D:/repos'))
}
}
}
gradle-plugins

properties文件内容为

1
implementation-class=cn.appblog.plugin.lifecycle.LifecyclePlugin
LifecyclePlugin.groovy

继承Transform,实现Plugin接口,通过Transform#transform()得到Collection<TransformInput> inputs,里面有我们想要的class文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package cn.appblog.plugin.lifecycle

import com.android.annotations.NonNull
import com.android.build.api.transform.*
import com.android.build.gradle.AppExtension
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter

import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry

import static org.objectweb.asm.ClassReader.EXPAND_FRAMES

class LifecyclePlugin extends Transform implements Plugin<Project> {

@Override
void apply(Project project) {
//registerTransform
def android = project.extensions.getByType(AppExtension)
android.registerTransform(this)
}

@Override
String getName() {
return "LifecyclePlugin"
}

@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}

@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}

@Override
boolean isIncremental() {
return false
}

@Override
void transform(@NonNull TransformInvocation transformInvocation) {
println '--------------- LifecyclePlugin visit start --------------- '
def startTime = System.currentTimeMillis()
Collection<TransformInput> inputs = transformInvocation.inputs
TransformOutputProvider outputProvider = transformInvocation.outputProvider
//删除之前的输出
if (outputProvider != null)
outputProvider.deleteAll()
//遍历inputs
inputs.each { TransformInput input ->
//遍历directoryInputs
input.directoryInputs.each { DirectoryInput directoryInput ->
handleDirectoryInputs(directoryInput, outputProvider)
}

//遍历jarInputs
input.jarInputs.each { JarInput jarInput ->
handleJarInputs(jarInput, outputProvider)
}
}
def cost = (System.currentTimeMillis() - startTime) / 1000
println '--------------- LifecyclePlugin visit end --------------- '
println "LifecyclePlugin cost : $cost s"
}

/**
* 处理文件目录下的class文件
*/
static void handleDirectoryInputs(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
//是否是目录
if (directoryInput.file.isDirectory()) {
//列出目录所有文件(包含子文件夹,子文件夹内文件)
directoryInput.file.eachFileRecurse { File file ->
def name = file.name
if (checkClassFile(name)) {
println '----------- deal with "class" file <' + name + '> -----------'
ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new LifecycleClassVisitor(classWriter)
classReader.accept(cv, EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
FileOutputStream fos = new FileOutputStream(
file.parentFile.absolutePath + File.separator + name)
fos.write(code)
fos.close()
} else {
println '----------- directory "class" file <' + name + '> need not deal -----------'
}
}
}
//处理完输入文件之后,要把输出给下一个任务
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}

/**
* 处理Jar中的class文件
*/
static void handleJarInputs(JarInput jarInput, TransformOutputProvider outputProvider) {
if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
//重名名输出文件,因为可能同名,会覆盖
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
JarFile jarFile = new JarFile(jarInput.file)
Enumeration enumeration = jarFile.entries()
File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
//避免上次的缓存被重复插入
if (tmpFile.exists()) {
tmpFile.delete()
}
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
//用于保存
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
String entryName = jarEntry.getName()
ZipEntry zipEntry = new ZipEntry(entryName)
InputStream inputStream = jarFile.getInputStream(jarEntry)
//插桩class
if (checkClassFile(entryName)) {
//class文件处理
println '----------- deal with "jar" class file <' + entryName + '> -----------'
jarOutputStream.putNextEntry(zipEntry)
ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new LifecycleClassVisitor(classWriter)
classReader.accept(cv, EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
jarOutputStream.write(code)
} else {
println '----------- jar "class" file <' + entryName + '> need not deal -----------'
jarOutputStream.putNextEntry(zipEntry)
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
jarOutputStream.closeEntry()
}
//结束
jarOutputStream.close()
jarFile.close()
def dest = outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(tmpFile, dest)
tmpFile.delete()
}
}

/**
* 检查class文件是否需要处理
* @param fileName
* @return
*/
static boolean checkClassFile(String name) {
//只处理需要的class文件
return (name.endsWith(".class") && !name.startsWith("R\$")
&& !"R.class".equals(name) && !"BuildConfig.class".equals(name)
&& ("android/support/v4/app/FragmentActivity.class".equals(name) || "androidx/appcompat/app/AppCompatActivity.class".equals(name)))
}

}

主要看方法transform(),通过参数inputs可以拿到所有的class文件。inputs中包括directoryInputsjarInputsdirectoryInputs为文件夹中的class文件,而jarInputs为jar包中的class文件。

对应两个处理方法LifecyclePlugin#handleDirectoryInput()LifecyclePlugin#handleJarInputs()

这两个方法都在做同一件事,就是遍历directoryInputsjarInputs,得到对应的class文件,然后交给ASM处理,最后覆盖原文件。

发现:在input.jarInputs中并没有android.jar,因为android.jar不在编译期,操作不了。本想在Activity中做处理,因为找不到android.jar,只好退而求其次选择android.support.v4.app中的FragmentActivity

处理class文件

handleDirectoryInputshandleJarInputs中,可以看到ASM的部分代码。这里以handleDirectoryInputs为例。

handleDirectoryInputs中ASM代码:

1
2
3
4
ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new LifecycleClassVisitor(classWriter)
classReader.accept(cv, EXPAND_FRAMES)

其中,关键处理类LifecycleClassVisitor

LifecycleClassVisitor

用于访问class的工具,在visitMethod()里对类名方法名进行判断是否需要处理。若需要,则交给MethodVisitor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package cn.appblog.plugin.lifecycle;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class LifecycleClassVisitor extends ClassVisitor implements Opcodes {

private String mClassName;

public LifecycleClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}

@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
//System.out.println("LifecycleClassVisitor : visit -----> started :" + name);
this.mClassName = name;
super.visit(version, access, name, signature, superName, interfaces);
}

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
//System.out.println("LifecycleClassVisitor : visitMethod : " + name);
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
//匹配FragmentActivity
if ("android/support/v4/app/FragmentActivity".equals(this.mClassName) || "androidx/appcompat/app/AppCompatActivity".equals(this.mClassName)) {
if ("onCreate".equals(name) ) {
//处理onCreate
System.out.println("LifecycleClassVisitor : change method ----> " + name);
return new LifecycleOnCreateMethodVisitor(mv);
} else if ("onDestroy".equals(name)) {
//处理onDestroy
System.out.println("LifecycleClassVisitor : change method ----> " + name);
return new LifecycleOnDestroyMethodVisitor(mv);
}
}
return mv;
}

@Override
public void visitEnd() {
//System.out.println("LifecycleClassVisitor : visit -----> end");
super.visitEnd();
}
}

visitMethod()中判断是否为FragmentActivity,且为方法onCreateonDestroy,然后交给LifecycleOnDestroyMethodVisitorLifecycleOnCreateMethodVisitor处理。

提示:ClassVisitor#visitMethod()只能访问当前类定义的method(一开始想访问父类的方法,陷入误区)
如,在MainActivity中只重写了onCreate(),没有重写onDestroy()。那么在visitMethod()中只会出现onCreate(),不会有onDestroy()

回到需求,我们希望在onCreate()中插入对应的代码,来记录页面被打开(这里通过Log代替)

1
Log.i("TAG", "-------> onCreate : " + this.getClass().getSimpleName());

于是,在LifecycleOnCreateMethodVisitor中如下处理(LifecycleOnDestroyMethodVisitorLifecycleOnCreateMethodVisitor相似)

LifecycleOnCreateMethodVisitor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package cn.appblog.plugin.lifecycle;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class LifecycleOnCreateMethodVisitor extends MethodVisitor {

public LifecycleOnCreateMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM4, mv);
}

@Override
public void visitCode() {
super.visitCode();
//方法执行前插入
mv.visitLdcInsn("yezhou");
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitLdcInsn("-------> onCreate : ");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "()Ljava/lang/String;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(Opcodes.POP);
}

@Override
public void visitInsn(int opcode) {
//方法执行后插入
/*
if (opcode == Opcodes.RETURN) {
mv.visitLdcInsn("TAG");
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitLdcInsn("-------> onCreate : end :");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "()Ljava/lang/String;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(Opcodes.POP);
}
*/
super.visitInsn(opcode);
}
}

只需要在visitCode()中插入上面的代码,即可实现onCreate()内容执行之前,先执行我们插入的代码。

如果想在onCreate()内容执行之后插入代码,该怎么做?和上面相似,只要在visitInsn()方法中插入对应的代码即可。代码如下:

1
2
3
4
5
6
7
8
9
@Override
public void visitInsn(int opcode) {
//判断RETURN
if (opcode == Opcodes.RETURN) {
//在这里插入代码
...
}
super.visitInsn(opcode);
}

如果对字节码不是很了解,看到上面visitCode()中的代码可能会觉得既熟悉又陌生,那是ASM插入字节码的用法。
如果写不来,没关系,这里介绍一个插件ASM Bytecode Outline

LifecycleOnDestroyMethodVisitor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package cn.appblog.plugin.lifecycle;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class LifecycleOnDestroyMethodVisitor extends MethodVisitor {

public LifecycleOnDestroyMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM4, mv);
}

@Override
public void visitCode() {
super.visitCode();
//方法执行前插入
mv.visitLdcInsn("yezhou");
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitLdcInsn("-------> onDestroy : ");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "()Ljava/lang/String;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(Opcodes.POP);
}

@Override
public void visitInsn(int opcode) {
super.visitInsn(opcode);
}
}
替换

class文件的插桩已经说完,剩下最后一步替换。眼尖的同学应该发现,代码上面已经出现过了。还是以LifecyclePlugin#handleDirectoryInputs()中的代码为例:

1
2
3
4
5
byte[] code = classWriter.toByteArray()
FileOutputStream fos = new FileOutputStream(
file.parentFile.absolutePath + File.separator + name)
fos.write(code)
fos.close()

classWriter得到class修改后的byte流,然后通过流的写入覆盖原来的class文件(Jar包的覆盖会稍微复杂一点,这里就不细说了)

File.separator:文件的分隔符。不同系统分隔符可能不一样。如:同样一个文件,Windows下是C:\tmp\test.txt;Linux 下却是/tmp/test.txt

插件使用

工程目录下的build.gradle配置插件

1
2
3
4
5
6
7
8
9
10
buildscript {
repositories {
maven { //本地Maven仓库地址
url uri('D:/repos')
}
}
dependencies {
classpath 'cn.appblog.plugin:lifecycle-plugin:0.0.1'
}
}

创建一个Android项目app,在app.gradle中引用插件

1
apply plugin: 'cn.appblog.gradle'

运行后,按步骤操作:打开MainActivity —> 打开SecondActivity —> 返回MainActivity

查看效果:

1
2
3
cn.appblog.asmdemo I/yezhou: -------> onCreate : MainActivity
cn.appblog.asmdemo I/yezhou: -------> onCreate : SecondActivity
cn.appblog.asmdemo I/yezhou: -------> onDestroy : SecondActivity

可以发现,页面打开\关闭都会打印对应的log。说明我们插入的代码被执行了,而且,使用时对项目没有任何“入侵”。

结语

本文内容涉及知识较多,在熟悉Android打包过程字节码Gradle Transform APIASM等之前,阅读起来会很困难。不过,在了解并学习这些知识的之后,相信你对Android会有新的认识。

本文转载至:https://www.jianshu.com/p/16ed4d233fd1

Powered by AppBlog.CN     浙ICP备14037229号

Copyright © 2012 - 2020 APP开发技术博客 All Rights Reserved.

访客数 : | 访问量 :