Android gradle编译时字节码处理

简介: Android gradle编译时字节码处理

android app的构建是使用gradle 工具,它提供给了开发者自定义编译期行为的能力。一般情况下,我们在transform阶段进行字节码的修改,插入,删除等操作。通过字节码处理,我们可以完成很多cool的事情,比如根据编译时注解,完成一些特定的操作等。

实现修改字节码的工具有:

javassist (如库 ‘org.javassist:javassist:3.27.0-GA’)

ASM ( 如库’com.android.tools.build:gradle:3.6.4’)

插件开发的一般步骤

  • 继承Plugin
class TestPlugin implements Plugin<Project> {
 
    @Override
    void apply(Project project) {
        System.out.println("========================");
        System.out.println("hello TestPlugin")
        System.out.println("========================")
 
        project.extensions.findByType(AppExtension.class).registerTransform(new MyTransform(project));
  • 自定义Transfrom
class MyTransform extends Transform {
    private Project project
    MyTransform(Project project) {
        this.project = project
    }
    @Override
    String getName() {
        return "MyTransform"
    }
 
    @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(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
    // do something when transform...下面只是一个例子
    transformInvocation.inputs.each { input ->
            // 包含我们手写的 Class 类及 R.class、BuildConfig.class 等
            input.directoryInputs.each { directoryInput ->
                String path = directoryInput.file.absolutePath
                println("[MyTransform] Begin to inject: $path")
 
                // 执行注入逻辑 ==========
                // inject code ...
                InjectByJavassit.inject(path, project)
                // 获取输出目录
                def dest = transformInvocation.outputProvider.getContentLocation(directoryInput.name,
                        directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                println("[MyTransform] Directory output dest: $dest.absolutePath")
 
                // 将input的目录复制到output指定目录
                FileUtils.copyDirectory(directoryInput.file, dest)
            }
 
            // jar文件,如第三方依赖
            input.jarInputs.each { jarInput ->
                def dest = transformInvocation.outputProvider.getContentLocation(jarInput.name,
                        jarInput.contentTypes, jarInput.scopes, Format.JAR)
                //
                FileUtils.copyFile(jarInput.file, dest)
            }
        }
  }
}

修改字节码的时候,我们可以使用javassist或者ASM。

  • 注册transform
project.extensions.findByType(AppExtension.class).registerTransform(new MyTransform(project));

在buildSrc中,开发并发布插件

插件的功能是,在Activity的onCreate中,编译时统一加上弹toast的功能。

  • build.gradle
apply plugin: 'groovy'
apply plugin: 'maven-publish'
println "debug, buildSrc ..."
repositories {
    google()
    jcenter()
    mavenCentral()
}
 
allprojects {
    repositories {
        google()
        jcenter()
        mavenCentral()
    }
}
dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation 'com.android.tools.build:gradle:3.5.0'
    implementation 'org.javassist:javassist:3.27.0-GA'
}
  • 在META-INF.gradle-plugins下新建文件com.test.testplugin.properties
    内容为implementation-class=com.test.testplugin.TestPlugin
  • 在src/main底下,建groovy目录,并建com.test.testplugin包
    新建以下类
  • TestPlugin.groovy
// 
package com.test.testplugin
 
import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.gradle.AppExtension
 
class TestPlugin implements Plugin<Project> {
 
    @Override
    void apply(Project project) {
        System.out.println("========================");
        System.out.println("hello TestPlugin")
        System.out.println("========================")
 
        project.extensions.findByType(AppExtension.class).registerTransform(new MyTransform(project))
        project.extensions.create("myExtension", MyExtension)
        project.task("myExtensionTask").doLast {
            System.out.println("in myExtensionTask " + project["myExtension"].name + ": " + project["myExtension"].version)
        }
    }
}
  • MyTransform.groovy
package com.test.testplugin
 
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.io.FileUtils
import org.gradle.api.Project
 
class MyTransform extends Transform {
    private Project project
    MyTransform(Project project) {
        this.project = project
    }
    @Override
    String getName() {
        return "MyTransform"
    }
 
    @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(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        transformInvocation.inputs.each { input ->
            // 包含我们手写的 Class 类及 R.class、BuildConfig.class 等
            input.directoryInputs.each { directoryInput ->
                String path = directoryInput.file.absolutePath
                println("[MyTransform] Begin to inject: $path")
 
                // 执行注入逻辑 ==========
                // inject code ...
                InjectByJavassit.inject(path, project)
                // 获取输出目录
                def dest = transformInvocation.outputProvider.getContentLocation(directoryInput.name,
                        directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                println("[MyTransform] Directory output dest: $dest.absolutePath")
 
                // 将input的目录复制到output指定目录
                FileUtils.copyDirectory(directoryInput.file, dest)
            }
 
            // jar文件,如第三方依赖
            input.jarInputs.each { jarInput ->
                def dest = transformInvocation.outputProvider.getContentLocation(jarInput.name,
                        jarInput.contentTypes, jarInput.scopes, Format.JAR)
                //
                FileUtils.copyFile(jarInput.file, dest)
            }
        }
    }
}
  • MyExtension.groovy
package com.test.testplugin
class MyExtension {
    String name = null
    String version = null
}
  • InjectByJavassit.groovy
package com.test.testplugin
 
import javassist.ClassPool
import javassist.CtClass
import javassist.CtMethod
import org.gradle.api.Project
 
class InjectByJavassit {
    static void inject(String path, Project project) {
        try {
            File dir = new File(path)
            if (dir.isDirectory()) {
                dir.eachFileRecurse { File file ->
                    if (file.name.endsWith('Activity.class')) {
                        doInject(project, file, path)
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace()
        }
    }
 
    private static void doInjectKai(Project project, File clsFile, String originPath) {
        println("[Inject] DoInject: $clsFile.absolutePath")
        String cls = new File(originPath).relativePath(clsFile).replace('/', '.')
        cls = cls.substring(0, cls.lastIndexOf('.class'))
        println("[Inject] Cls: $cls")
 
        ClassPool pool = ClassPool.getDefault()
        CtClass ctClass = pool.getCtClass(cls)
        // 解冻
        if (ctClass.isFrozen()) {
            ctClass.defrost()
        }
        // 获取方法
        CtMethod ctMethod = ctClass.getDeclaredMethod('splitString')
        String addLog = 'android.util.Log.e("kaidebug", "This is add by injecting");'
        ctMethod.insertAfter(addLog)
        ctClass.writeFile(originPath)
 
        ctClass.detach()
    }
 
    private static void doInject(Project project, File clsFile, String originPath) {
        println("[Inject] DoInject: $clsFile.absolutePath")
        String cls = new File(originPath).relativePath(clsFile).replace('/', '.')
        cls = cls.substring(0, cls.lastIndexOf('.class'))
        println("[Inject] Cls: $cls")
 
        ClassPool pool = ClassPool.getDefault()
        // 加入当前路径
        pool.appendClassPath(originPath)
        // project.android.bootClasspath 加入android.jar,不然找不到android相关的所有类
        pool.appendClassPath(project.android.bootClasspath[0].toString())
        // 引入android.os.Bundle包,因为onCreate方法参数有Bundle
        pool.importPackage('android.os.Bundle')
 
        CtClass ctClass = pool.getCtClass(cls)
        // 解冻
        if (ctClass.isFrozen()) {
            ctClass.defrost()
        }
        // 获取方法
        CtMethod ctMethod = ctClass.getDeclaredMethod('onCreate')
 
        String toastStr = 'android.widget.Toast.makeText(this, "This is ' + cls + '", android.widget.Toast.LENGTH_SHORT).show();'
 
        // 方法尾插入
        ctMethod.insertAfter(toastStr)
        ctClass.writeFile(originPath)
 
        // 释放
        ctClass.detach()
    }
}

App 模块中使用插件

在app module中的build.gradle中,使用上面开发的插件

......
apply plugin: 'com.test.testplugin'
println "kaidebug, app build.gradle"
// 这里为插件传入控制数据,方便插件使用者向插件注入控制量,是灵活性的一种体现
myExtension {
    name "zhuyunkai"
    version "1.2.3"
}
...


相关文章
|
30天前
|
Java Android开发 C++
Android Studio JNI 使用模板:c/cpp源文件的集成编译,快速上手
本文提供了一个Android Studio中JNI使用的模板,包括创建C/C++源文件、编辑CMakeLists.txt、编写JNI接口代码、配置build.gradle以及编译生成.so库的详细步骤,以帮助开发者快速上手Android平台的JNI开发和编译过程。
82 1
|
1月前
|
Android开发 Docker 容器
docker中编译android aosp源码,出现Build sandboxing disabled due to nsjail error
在使用Docker编译Android AOSP源码时,如果遇到"Build sandboxing disabled due to nsjail error"的错误,可以通过在docker run命令中添加`--privileged`参数来解决权限不足的问题。
136 1
|
30天前
|
Android开发
Android Studio: 解决Gradle sync failed 错误
本文介绍了解决Android Studio中出现的Gradle同步失败错误的步骤,包括从`gradle-wrapper.properties`文件中获取Gradle的下载链接,手动下载Gradle压缩包,并替换默认下载路径中的临时文件,然后重新触发Android Studio的"Try Again"来完成同步。
343 0
Android Studio: 解决Gradle sync failed 错误
|
30天前
|
Java Android开发 芯片
使用Android Studio导入Android源码:基于全志H713 AOSP,方便解决编译、编码问题
本文介绍了如何将基于全志H713芯片的AOSP Android源码导入Android Studio以解决编译和编码问题,通过操作步骤的详细说明,展示了在Android Studio中利用代码提示和补全功能快速定位并修复编译错误的方法。
43 0
使用Android Studio导入Android源码:基于全志H713 AOSP,方便解决编译、编码问题
|
30天前
|
API 开发工具 Android开发
Android Studio:解决AOSP自编译framework.jar引用不到的问题
在Android Studio中解决AOSP自编译framework.jar引用问题的几种方法,包括使用相对路径、绝对路径和通过`${project.rootDir}`动态获取路径的方法,以避免硬编码路径带来的配置问题。
43 0
Android Studio:解决AOSP自编译framework.jar引用不到的问题
|
1月前
|
搜索推荐 Android开发
学习AOSP安卓系统源代码,需要什么样的电脑?不同配置的电脑,其编译时间有多大差距?
本文分享了不同价位电脑配置对于编译AOSP安卓系统源代码的影响,提供了从6000元到更高价位的电脑配置实例,并比较了它们的编译时间,以供学习AOSP源代码时电脑配置选择的参考。
59 0
学习AOSP安卓系统源代码,需要什么样的电脑?不同配置的电脑,其编译时间有多大差距?
|
30天前
|
Ubuntu 开发工具 Android开发
Repo下载、编译AOSP源码:基于Ubuntu 21.04,android-12.1.0_r27
文章记录了作者在Ubuntu 21.04服务器上配置环境、下载并编译基于Android 12.1.0_r27版本的AOSP源码的过程,包括解决编译过程中遇到的问题和错误处理方法。
44 0
|
30天前
|
Java 开发工具 Android开发
Android Studio利用Build.gradle导入Git commit ID、Git Branch、User等版本信息
本文介绍了在Android Studio项目中通过修改`build.gradle`脚本来自动获取并添加Git的commit ID、branch名称和用户信息到BuildConfig类中,从而实现在编译时将这些版本信息加入到APK中的方法。
40 0
|
存储 Java 编译器
Android逆向之--------常见Davlik字节码解释
Android逆向之--------常见Davlik字节码解释
145 0
Android逆向之--------常见Davlik字节码解释
|
13天前
|
Android开发 开发者 Kotlin
探索安卓开发中的新特性
【9月更文挑战第14天】本文将引导你深入理解安卓开发领域的一些最新特性,并为你提供实用的代码示例。无论你是初学者还是经验丰富的开发者,这篇文章都会给你带来新的启示和灵感。让我们一起探索吧!