根据android打包流程可知,java类会先编译成.class类,此时的.class类就是以java字节码形式存储的(8位字节的二进制流文件,android studio可以直接将其转化成可读的文件),.class类会进一步通过dx工具转化成dex文件(一种优化和压缩后的字节码流文件),dex文件需要在Dalvik(android运行时环境,与java的jvm类似)上运行。
是一个 java字节码操作与分析框架。
是将一段代码通过某种方法插入到另外一段代码中,或者修改一段代码。
可实现在被调用方法的第一行插入代码,在方法结束时插入代码,根据此可以做很多事情:
(1)、在某个方法调用和结束时打印日志、计算方法执行时间;
(2)、在对使用了自定义注解的方法插入对应的逻辑代码(权限控制);
(3)、处理所有按钮连续点击重复打开页面;
(4)、实现辅助热更新等;
本文采用的方法:
通过Gradle插件提供的Transform API,可以在编译成dex文件之前得到class文件,得到class文件之后,便可以通过ASM对字节码进行修改,即可完成字节码插桩。
有什么效果呢:
当导入了我们自定义的这个Gradle插件,就可以将我们需要变更的代码注入到最终的apk中,主项目代码无需关心Gradle插件的任何信息,主项目几乎是无感知的。
实现对Test类下的test()方法,在调用开始和退出调用时注入打印日志的代码,案例如下:
保留java目录,新建resources资源文件夹,在resources添加META_INF/gradle-plugins路径,在此路径下添加com.hb.test.pluginams.properties 属性文件。
com.hb.test.pluginams.properties内容如下:
implementation-class=com.hb.test.pluginams.TestMethodPlugin
配置插件pluginams的build.gradle文件,引入对应的asm框架包:
apply plugin: 'kotlin'
apply plugin: 'groovy'
apply plugin: 'maven'
repositories {
mavenCentral()
}
dependencies {
//gradle sdk
implementation gradleApi()
//groovy sdk
implementation localGroovy()
implementation 'org.ow2.asm:asm:7.2'
implementation 'org.ow2.asm:asm-commons:7.2'
implementation 'org.ow2.asm:asm-analysis:7.2'
implementation 'org.ow2.asm:asm-util:7.2'
implementation 'org.ow2.asm:asm-tree:7.2'
implementation 'com.android.tools.build:gradle:4.1.2', {
exclude group:'org.ow2.asm'
}
}
//group和version在后面引用自定义插件的时候会用到
group='com.hb.test.pluginams'
version='1.0.0'
//上传到本地maven仓库
uploadArchives {
repositories {
mavenDeployer {
//本地的Maven地址:当前工程下
repository(url: uri('./ams_plugin'))
}
}
}
TestMethodPlugin.kt
package com.hb.test.pluginams
import com.android.build.api.transform.Format
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformInvocation
import com.android.build.gradle.AppExtension
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import java.io.FileOutputStream
class TestMethodPlugin : Transform(), Plugin<Project> {
override fun getName(): String {
return "TestMethodPlugin"
}
override fun apply(target: Project) {
val appExtension = target.extensions.getByType(AppExtension::class.java)
appExtension.registerTransform(this)
}
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
return TransformManager.CONTENT_CLASS
}
override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
return TransformManager.SCOPE_FULL_PROJECT
}
override fun isIncremental(): Boolean {
return false
}
override fun transform(transformInvocation: TransformInvocation?) {
val inputs = transformInvocation?.inputs
val out = transformInvocation?.outputProvider
inputs?.forEach { transformInput ->
transformInput.directoryInputs.forEach { directoryInput ->
if (directoryInput.file.isDirectory) {
FileUtils.getAllFiles(directoryInput.file).forEach {
val file = it
val name = file.name
if (name.endsWith(".class") && name != "R.class"
&& !name.startsWith("R$") && name != "BuildConfig.class"
) {
val classPath = file.absolutePath
val cr = ClassReader(file.readBytes())
val cw = ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
val visitor = TestClassVisitor(cw)
cr.accept(visitor, ClassReader.SKIP_FRAMES)
val byte = cw.toByteArray()
val fos = FileOutputStream(classPath)
fos.write(byte)
fos.close()
}
}
}
val dest = out?.getContentLocation(
directoryInput.name,
directoryInput.contentTypes,
directoryInput.scopes,
Format.DIRECTORY
)
FileUtils.copyDirectoryToDirectory(directoryInput.file, dest)
}
transformInput.jarInputs.forEach {
val dest = out?.getContentLocation(
it.name, it.contentTypes,
it.scopes, Format.JAR
)
FileUtils.copyFile(it.file, dest)
}
}
}
}
TestClassVisitor.kt
package com.hb.test.pluginams
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
class TestClassVisitor(classVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM7, classVisitor) {
private val TAG = "PluginAmsTag:"
private var className: String? = null
override fun visit(
version: Int,
access: Int,
name: String?,
signature: String?,
superName: String?,
interfaces: Array<out String>?
) {
super.visit(version, access, name, signature, superName, interfaces)
className = name
}
override fun visitMethod(
methodAccess: Int,
methodName: String?,
methodDescriptor: String?,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
val methodVisitor =
super.visitMethod(methodAccess, methodName, methodDescriptor, signature, exceptions)
println("$TAG method=$methodName")
println("$TAG className=$className")
if (className == "com/hb/test/amstest/Test" && methodName == "test") {
return CustomizeMethodVisitor(
api,
methodVisitor,
className,
methodAccess,
methodName,
methodDescriptor
)
}
return methodVisitor
}
}
CustomizeMethodVisitor.kt
package com.hb.test.pluginams
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.commons.AdviceAdapter
class CustomizeMethodVisitor(
api: Int,
methodVisitor: MethodVisitor,
className: String?,
access: Int,
name: String?,
descriptor: String?
) : AdviceAdapter(api, methodVisitor, access, name, descriptor) {
private val TAG = "${this.javaClass.simpleName}"
override fun onMethodEnter() {
println("$TAG onMethodEnter")
super.onMethodEnter()
mv.visitLdcInsn("Test.class")
mv.visitLdcInsn("aaa start")
mv.visitMethodInsn(
INVOKESTATIC, "android/util/Log", "d",
"(Ljava/lang/String;Ljava/lang/String;)I", false
)
mv.visitInsn(POP)
}
override fun onMethodExit(opcode: Int) {
mv.visitLdcInsn("Test.class")
mv.visitLdcInsn("aaa end")
mv.visitMethodInsn(
INVOKESTATIC, "android/util/Log", "d",
"(Ljava/lang/String;Ljava/lang/String;)I", false
)
mv.visitInsn(POP)
println("$TAG onMethodExit")
super.onMethodExit(opcode)
}
}
上传成功之后会看到生成了对应的maven库:
然后,在主项目中引用此maven库。
MyAmsTest/app/build_gradle
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'com.hb.test.pluginams'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.hb.test.amstest"
minSdkVersion 16
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
MyAmsTest/build_gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.5.10"
repositories {
google()
mavenCentral()
maven {
url uri('./pluginams/ams_plugin')
}
}
dependencies {
classpath "com.android.tools.build:gradle:4.2.2"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
//这里
classpath "com.hb.test.pluginams:pluginams:1.0.0"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
mavenCentral()
jcenter() // Warning: this repository is going to shut down soon
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Test.java
package com.hb.test.amstest;
public class Test {
public static final String TAG = "Test.class";
void test() {
System.out.println("test fun content");
}
}
MainActivity.kt
package com.hb.test.amstest
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val test=Test()
test.test()
}
}
app一运行就会输出我们插入的Log日志:
1、在新建模块的时候,配置插件模块的build_gradle文件容易出错,建议直接拷贝;
2、要熟悉android打包apk的流程java->class->dex,.class文件是以java字节码形式存储的。
3、Gradle插件提供的Transform API,可以直接利用此API进行字节码修改和插入操作。
4、还可以通过ASM Bytecode viewer插件可以直接查看比较java或是kt文件生成的class文件内容;
5、知识点很多,笔者也是看了很多资料之后实践成功的,希望多操作。
Java字节码类文件(.class)是Java编译器编译Java源文件(.java)产生的“目标文件”。它是一种8位字节的二进制流文件, 各个数据项按顺序紧密的从前向后排列, 相邻的项之间没有间隙, 这样可以使得class文件非常紧凑, 体积轻巧, 可以被JVM快速的加载至内存, 并且占据较少的内存空间(方便于网络的传输)。
Java源文件在被Java编译器编译之后, 每个类(或者接口)都单独占据一个class文件, 并且类中的所有信息都会在class文件中有相应的描述, 由于class文件很灵活, 它甚至比Java源文件有着更强的描述能力。
class文件中的信息是一项一项排列的, 每项数据都有它的固定长度, 有的占一个字节, 有的占两个字节, 还有的占四个字节或8个字节, 数据项的不同长度分别用u1, u2, u4, u8表示, 分别表示一种数据项在class文件中占据一个字节, 两个字节, 4个字节和8个字节。 可以把u1, u2, u3, u4看做class文件数据项的“类型” 。