Android字节码ASM插桩

Android字节码ASM插桩,第1张

目录 1、字节码ASM插桩到底什么意思?1.1、字节码1.2、ASM1.3、插桩 2、插桩到底有什么用?3、怎么才能实现插桩呢?第一步:新建moudle第二步,配置属性文件和插件模块的build.gradle:第三步,编写对应的插件注入类,及类和方法访问器:第四步,上传插件库到本地maven仓库,方便主项目引用:第五步,配置主项目的MyAmsTest/app/build_gradle及MyAmsTest/build_gradle文件第六步,在主项目中编写被用于插桩的测试类:第七步,在启动Activity中正常使用此测试类,然后运行app,查看日志: 4、总结5、附加知识点5.1、什么是Class文件?

1、字节码ASM插桩到底什么意思? 1.1、字节码

根据android打包流程可知,java类会先编译成.class类,此时的.class类就是以java字节码形式存储的(8位字节的二进制流文件,android studio可以直接将其转化成可读的文件),.class类会进一步通过dx工具转化成dex文件(一种优化和压缩后的字节码流文件),dex文件需要在Dalvik(android运行时环境,与java的jvm类似)上运行。

1.2、ASM

是一个 java字节码 *** 作与分析框架。

1.3、插桩

是将一段代码通过某种方法插入到另外一段代码中,或者修改一段代码。

2、插桩到底有什么用?

可实现在被调用方法的第一行插入代码,在方法结束时插入代码,根据此可以做很多事情:

(1)、在某个方法调用和结束时打印日志、计算方法执行时间;
(2)、在对使用了自定义注解的方法插入对应的逻辑代码(权限控制);
(3)、处理所有按钮连续点击重复打开页面;
(4)、实现辅助热更新等;

3、怎么才能实现插桩呢?

本文采用的方法:
通过Gradle插件提供的Transform API,可以在编译成dex文件之前得到class文件,得到class文件之后,便可以通过ASM对字节码进行修改,即可完成字节码插桩。

有什么效果呢:
当导入了我们自定义的这个Gradle插件,就可以将我们需要变更的代码注入到最终的apk中,主项目代码无需关心Gradle插件的任何信息,主项目几乎是无感知的。

实现对Test类下的test()方法,在调用开始和退出调用时注入打印日志的代码,案例如下:

项目结构图如下:

第一步:新建moudle



保留java目录,新建resources资源文件夹,在resources添加META_INF/gradle-plugins路径,在此路径下添加com.hb.test.pluginams.properties 属性文件。

com.hb.test.pluginams.properties内容如下:

implementation-class=com.hb.test.pluginams.TestMethodPlugin
第二步,配置属性文件和插件模块的build.gradle:

配置插件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库:

然后,在主项目中引用此maven库。

第五步,配置主项目的MyAmsTest/app/build_gradle及MyAmsTest/build_gradle文件

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");
    }
}

第七步,在启动Activity中正常使用此测试类,然后运行app,查看日志:

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日志:

4、总结

1、在新建模块的时候,配置插件模块的build_gradle文件容易出错,建议直接拷贝;
2、要熟悉android打包apk的流程java->class->dex,.class文件是以java字节码形式存储的。
3、Gradle插件提供的Transform API,可以直接利用此API进行字节码修改和插入 *** 作。
4、还可以通过ASM Bytecode viewer插件可以直接查看比较java或是kt文件生成的class文件内容;
5、知识点很多,笔者也是看了很多资料之后实践成功的,希望多 *** 作。

5、附加知识点 5.1、什么是Class文件?

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文件数据项的“类型” 。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/992864.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存