我正在参加「启航计划」

目录

  • Android 干货共享: 字节码插桩(1)—— 了解 Gradle
  • Android 干货共享: 字节码插桩(2)—— ASM 运用 (待更新)

前语

我在学习这方面内容时也参阅了许多解说插桩的博客,很少对 Gradle Task 相关东西进行解说,就导致看完教程感觉会了可是上手写很困难,单独去全身心学习 Gradle 常识周期又比较长,所以榜首篇博客先来共享下相关的 Gradle 部分内容,但不会深化的解说,由于本系列博客的作用是能赶快上手写代码完成插桩。

Gradle 根底

Gradle是一款基于JVM的构建东西,用于构建、编译和打包各种类型的应用程序和库,在运用 Android Studio 开发时咱们点击运转按钮 app debug 版本就会装在咱们的手机上,这其间的构建流程是由 Gradle 来履行一连串的 Task 完成。

在初学 Gradle 时完全没必要当即掌握 Groovy 语法,由于 Gradle 是基于 JVM 的,能够与 Java 类无缝联接,直接用 Java or Kotlin 来写就好。

结构

在 Android 项目中根目录下的 settings.gradlebuild.gradle 文件再配上各个模块目录下的 build.gradle 文件就构成了最根底的 gradle 项目;

Android 干货分享: 字节码插桩(1)—— 熟悉 Gradle

履行阶段

  1. 装备阶段(Initialization):Gradle 加载构建脚本,装备项目,构建目标模型。在此阶段,Gradle 创立并装备了 Project 目标,初始化了项目的特点、依靠项、插件等信息,但并不履行详细的使命。
  2. 装备履行阶段(Configuration):Gradle 会顺次履行一切的 Task,在履行 Task 前,先履行使命的装备,确认使命的履行办法。在此阶段,Gradle 依据用户界说的 Task 装备信息来创立 Task 目标,并将 Task 参加履行行列。
  3. 履行阶段(Execution):Gradle 顺次履行参加履行行列中的 Task,并依据 Task 之间的依靠联系,确认履行次序。在此阶段,Gradle 履行详细的使命,完成构建进程。履行阶段是整个构建进程中最为关键的阶段。
  4. 完毕阶段(Finalization):Gradle 履行完一切 Task 后,进行资源整理,以及一些收尾作业。在此阶段,Gradle 履行一些整理作业,例如删去一些临时文件,释放占用的资源等。

这么说可能会一脸懵,下面来结合 Android 项目来了解下。

  1. 装备阶段:详细表现为读取 settings.gradle 中的装备,获取项目称号、子模块称号;接着创立 Project 目标;再接着应用项目中界说的插件;依据各个 build.gradle 文件中界说的 Task 创立使命,而且将使命添加到使命图(有向无环图)中;
  2. 装备履行阶段:详细表现为顺次履行 settings.gradle、build.gradle 和各个子模块下的 build.gradle 中的装备代码。留意:装备代码不是履行全部代码。接着一切 Task、Plugin 的装备代码;留意这一步的操作只会履行装备代码,后边的比方中会表现。
  3. 履行阶段:详细表现为依照依靠次序(使命图)履行各个 Task 中的代码;
  4. 完毕阶段:也便是收尾作业,详细表现为输出一些构建相关信息(构建耗时、内存),整理使命(临时文件目录)等;

枯燥的理论常识部分完毕了,关于这些描绘有不明白的不用着急,下面末节中的比方都会表现出来,阅读完回过头再来对照一下就很容易了解了。

闭包

用过 Kotlin 的肯定很容易了解这个闭包,我就直接上代码来简单表达下:

task hello {
    // doLast 就了解为履行一段代码 下面会说到
    doLast { // 闭包
        println("hello world")
    }
    // 承受的是一个 Closure 目标
    doLast(new Closure(this) {
        @Override
        Object call() {
            println("hello world")
            return super.call()
        }
    })
}

两个 doLast 的作用是一样的,都是输出 hello world,能够看作是一种匿名函数。闭包能够有多个参数,也能够没有参数,能够有返回值,也能够没有返回值。

Gradle Task

Gradle 的中心就在于履行一个个的 Task,接下来经过一些实例代码来了解一下常见的操作。

手写 Task

为了便利直接在项目根目录下新建一个 study.gradle 文件并写入以下代码:

study.gradle

class Utils{
    void hello(){
        println("hello world!")
    }
    void goodbye(){
        println("goodbye!")
    }
}
Utils utils = new Utils()
task hello {
    utils.hello()
}
task bye {
    utils.goodbye()
}

其他形式

上一末节中界说了 hello,bye 两个 Task,界说的办法是运用闭包,除了闭包还有其他的界说办法:

class MyTask extends DefaultTask{
    @TaskAction
    void doSomethings(){
        Utils utils = new Utils()
        utils.hello()
    }
}
task hello(type: MyTask)
// or
Task myTask = tasks.create("hello"){
    // 装备使命其他特点
    dependsOn(otherTask) // 使命依靠
    group("study") // 使命分组
    description("invoke Utils.hello method to print") // 使命描绘
    doLast{
        Utils utils = new Utils()
        utils.hello()
    }
}

运转作用是一样的。

Task 履行

gradle 文件能够经过以下指令履行:

./gradlew -b study.gradle

输出成果:

> Configure project :
hello world!
goodbye!

嗯,看着一切正常,接着来履行下单个 Task:

./gradlew -b study.gradle hello

输出成果:

> Configure project :                                                                      
hello world!                                                                               
goodbye!     

履行单个 Task 和履行整个文件的成果居然一样?这儿就表现出来前面末节中的履行装备阶段,留意输出的榜首行内容 >Configure project : ,这显然是装备项目的意思。

修正一下 hello 这个 Task 代码:

task hello {
    doLast{
        utils.hello()
    }
}

再次履行./gradlew -b study.gradle hello后输出成果:

> Configure project :
goodbye!
> Task :hello
hello world!

能够看到多了一些输出,Configure project 中没有了 hello world 输出,而是移动到了 > Task :hello 之后输出。而 > Task :hello 正是前面说到的履行阶段。

再次修正代码来体会下:

task hello {
    // 装备阶段
    utils.hello()
    // 履行阶段
    doLast{
        utils.hello()
    }
}

再次履行./gradlew -b study.gradle hello后输出成果:

> Configure project :
hello world!
goodbye!
> Task :hello
hello world!

相信看到这儿就能了解装备阶段、履行阶段的意义了,在装备阶段会加载一切的 Task 而且履行其装备代码,履行阶段会履行 doLast、doFirst 等等闭包中的代码;

doFirst

字面意思就很容易了解,在使命之前履行一些操作,直接上代码来解说:

task hello {
    doFirst {
        println("hello3")
    }
    doFirst {
        println("hello2")
    }
    doFirst {
        println("hello1")
    }
}

输出成果:

> Task :hello
hello1
hello2
hello3

每次调用 doFirst 都会将闭包中的逻辑插到使命履行阶段的最前面。

doLast

和 doFrist 相反,doLast 是将逻辑插到使命履行阶段的终究,示例:

task hello {
    doLast {
        println("hello1")
    }
    doLast {
        println("hello2")
    }
    doLast {
        println("hello3")
    }
}

输出成果:

> Task :hello
hello1
hello2
hello3

afterEvaluate

afterEvaluate 是在装备阶段完成后的回调,示例:

task hello {
    // 装备阶段
    utils.hello()
    // 履行阶段
    doLast{
        utils.hello()
    }
}
task bye {
    utils.goodbye()
}
// 在装备阶段之后履行
afterEvaluate {
    println("After evaluate")
    // 能够在这儿履行进一步的操作,如注册使命、修正特点等
}

运转 ./gradlew -b study.gradle 输出成果:

> Configure project :
hello world!
goodbye!
After evaluate

在使命装备阶段都履行完成后触发了 afterEvaluate 闭包中的逻辑。

Task 依靠

Android Studio 在构建进程中最常见的 Task 便是 assembleDebug ,点击运转按钮后能够看到控制台输出的可不是只要一个 assembleDebug 使命:

Android 干货分享: 字节码插桩(1)—— 熟悉 Gradle

比较长没有截图完好,不过不影响,能够看得出在履行 assembleDebug 时先履行了一系列与打包有关的使命,这就需求用到使命依靠。示例:

task hello {
    doLast{
        utils.hello()
    }
}
// 创立使命时设置依靠
// bye 使命依靠于 hello 使命
task bye (dependsOn: hello) {
    doLast{
        utils.goodbye()
    }
}

履行 ./gradlew -b study.gradle bye 来运转 bye 使命,输出成果:

> Configure project :

> Task :hello
hello world!

> Task :bye
goodbye!

能够看出先履行了依靠的 hello 使命后才会履行 bye 使命。

finalizedBy

dependsOn 是在当前使命之前履行,finalizedBy 则是在当前使命完成后履行,示例代码:

task hello {
    doLast{
        utils.hello()
    }
}
task bye{
    doLast{
        utils.goodbye()
    }
}
hello.finalizedBy(bye)

运转 ./gradlew -b study.gradle hello 后输出成果:

> Task :hello
hello world!

> Task :bye
goodbye!

能够看到 bye 在 hello 之后履行了。

文件读写

读写相对简单,直接上代码:

task studyIO {
    doLast{
        String path = "D:/info.txt"
        File file = new File(path)
        // 创立文件
        file.createNewFile()
        //写入
        file.withPrintWriter {printWriter ->
            printWriter.println("time --> " + System.currentTimeMillis())
        }
        //读取
        println("read file start")
        println(file.text)
        println("read file end")
    }
}

运转成果,在 D 盘根目录下创立 info.txt 文件一起写入时间戳信息,并输出以下成果:

> Task :studyIO
read file start
time --> 1684317062511
read file end

当然也能够一行一行读取文件:

file.eachLine {
    println(it)
}

实战:Task 输出 Android 项目依靠信息

结合前面末节的示例来完成一个稍微复杂点的 Task,在项目根目录的 build.gradle 文件中写入以下代码:

// 界说 Task
task writeDependencyTreeToFile {
    group("CustomTask")
    description("Query dependency information and write to a file")
    doLast {
        // 创立文件 位置在项目根目录
        File file = new File("${rootProject.projectDir}/dependency-tree.txt")
        file.createNewFile()
        file.withPrintWriter { printWriter ->
            // 遍历项目模块
            rootProject.allprojects { Project project ->
                // 获取项目装备信息
                project.configurations.each { Configuration configuration ->
                    // 遍历依靠
                    configurations.named('implementation').get().allDependencies.each { dependency ->
                        String info = "${dependency.group}:${dependency.name}:${dependency.version}"
                        // 打印依靠信息
                        println(info)
                        // 将以来信息写入文件中
                        printWriter.println(info)
                    }
                }
            }
        }
    }
}

运转 Task 后会在项目的根目录下生成 dependency-tree.txt 文件并写入依靠信息,控制台输出如下:

> Task :writeDependencyTreeToFile
androidx.core:core-ktx:1.7.0
com.google.android.material:material:1.5.0
androidx.constraintlayout:constraintlayout:2.1.3
androidx.lifecycle:lifecycle-process:2.4.0
androidx.appcompat:appcompat:1.0.0
androidx.activity:activity-ktx:1.0.0
androidx.fragment:fragment-ktx:1.1.0
androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1
androidx.lifecycle:lifecycle-livedata-ktx:2.5.1
androidx.lifecycle:lifecycle-runtime-ktx:2.5.1
androidx.core:core-ktx:1.7.0
// 太多了就省掉了 ...

当然,这仅仅个简单完成,里边有许多重复依靠而且也没有依靠结构,仅仅巩固一下 Task 相关常识。自界说 Task 的需求比较常见,随意举个比方,比方之前适配 AndroidS 时要求清单文件中 Activity 等组件都参加 android:exported="false" 特点,关于页面较多的老项目来说手动添加不太实际(一些第三方库的清单文件也无法直接修正),假如运用自界说 Task,完成一个解析清单文件而且依照需求添加 android:exported 特点的 Task,而且运用 finalizedBy 将其挂载到构建流程之后就能轻松完成。

Gradle Plugin

了解完 Task 接着再来看看 Gradle 中的 Plugin(插件)。

手写 Plugin

在 Android 项目中,buildSrc 目录是 Gradle 的一个特殊目录,用于编写插件和构建脚本相关的自界说代码。当 Gradle 构建时,会自动检测并处理 buildSrc 目录,将其作为构建脚本的一部分来履行。

首要,在项目中新建一个 Java or Kotlin Library Moudle 命名为 buildSrc,而且在其目录下的 build.gradle 文件中写入以下代码:

//apply plugin: 'groovy' // 运用 groovy 编写需求引进该插件
apply plugin: 'java'
apply plugin: 'java-library'
dependencies {
    // 运用 gradle 提供的 api
    implementation gradleApi()
    // implementation localGroovy() // 运用 groovy 编写需求引进该插件
    // 在这儿也能够引进一些其他库,比方需求插件进行网络请求能够引进 okhttp 等等
}

界说插件

插件相关代码和一般模块一样就放在 java 目录下,假如运用 groovy 来写就需求新建 groovy 目录,当然,用 kotlin 编写也是能够的。后边的示例代码都用 java 编写便利了解。

新建 MyPlugin.java 文件:

public class MyPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) { // project 参数能够获取项目的一些信息
        System.out.println("This is my first plugin");
    }
}

插件需求承继自 org.gradle.api.Plugin 不要搞错了,这就完成了一个插件,引证该插件时会输出一条日志。

引证插件

一般插件都是供其他模块运用,那么需求先能够被其他模块辨认,在 main 目录下新建 META-INF/gradle-plugins/my-plugin.properties 文件,留意目录不要错:

Android 干货分享: 字节码插桩(1)—— 熟悉 Gradle

在其间写入:

implementation-class=top.sunhy.app.plugin.MyPlugin // 插件详细完成类

接着就能够在其他模块中运用了,回到 app 模块的 build.gradle 中引证该插件:

apply plugin: 'my-plugin'

这时再次运转项目时留意控制台的输出:

Android 干货分享: 字节码插桩(1)—— 熟悉 Gradle

能够看到插件正常运转了。

插件扩展

插件也支撑扩展,如插件需求建议网络请求,请求的基地址想经过其他模块自界说装备能够这么写。

界说扩展信息,新建一个 Java 类:

public class MyExtension {
    String baseUrl = "http://localhost:8080/api";
    String user = "admin";
}

回到 MyPlugin 中添加以下代码:

public class MyPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        // 创立
        MyExtension ext = project.getExtensions().create("network", MyExtension.class);
        // 装备阶段完成后获取
        project.afterEvaluate(p -> {
            // 打印
            System.out.println("Plugin baseUrl: " + ext.baseUrl);
            System.out.println("Plugin user: " + ext.user);
        });
    }
}

先不进行自界说装备,直接运转看成果:

Android 干货分享: 字节码插桩(1)—— 熟悉 Gradle

输出的是默认值,接着进行自界说装备,在 app 目录下的 build.gradle 中添加以下代码:

apply plugin: 'my-plugin'
// 自界说
network {
    baseUrl = "http://sunhy.com/api"
    user = "sunhy"
}

运转成果:

Android 干货分享: 字节码插桩(1)—— 熟悉 Gradle

实战:扫描项目中一切的 Activity

先来捋一下思路,在构建 apk 的一系列使命中有一个使命肯定是兼并一切模块的 AndroidManifest.xml 文件,那么咱们就在这个使命之后获取到终究 AndroidManifest.xml 所在的目录,用 xml 解析器解析出里边的标签即可。

那么榜首个问题就来了,哪个使命会得到终究的 AndroidManifest.xml 呢?点一下运转按钮控制台就会输出一系列的 Task:

Android 干货分享: 字节码插桩(1)—— 熟悉 Gradle

看名字也知道这个使命肯定是处理 AndroidManifest 文件,那么咱们就运用之前 Task 末节说到的 doLast 来给这个 Task 添加一项操作,留意自界说插件 apply 办法的 project 参数,能够获取一些项目信息,其间就包含了 Task,接下来随意找个 xml 解析库解析一下就完事了,代码如下:

public class MyPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
            // 留意:要在 afterEvaluate 里进行操作,装备阶段完成后才会形成使命图
            // 否则会找不到对应的 Task
            project.afterEvaluate(p -> p.getTasks().forEach(task -> { // 遍历一切task
            // 依据称号找到 task
            if (task.getName().equals("processDebugMainManifest")) {
                // doLast 添加解析操作
                task.doLast(t -> {
                    // 获取 Task 输出的文件
                    t.getOutputs().getFiles().getFiles().forEach(file -> {
                        // 输出的不止有一个文件,依据文件名判断
                        if (file.getName().equals("AndroidManifest.xml")) {
                            // 解析的进程就不多说了
                            Document doc = Jsoup.parse(file, "UTF-8");
                            Elements activityElements = doc.select("manifest > application > activity");
                            activityElements.forEach(element -> {
                                // 打印成果
                                System.out.println("Activity: " + element.attr("android:name"));
                            });
                        }
                    });
                });
            }
        }));
    }
}

运转看下作用:

Android 干货分享: 字节码插桩(1)—— 熟悉 Gradle

能够看到包括其他模块(libs)中的 Activity 也都打印了出来。

参考资料

Gradle 官方文档

终究

关于 Gradle 的 Task 和 Plugin 了解了之后下一篇博客将共享下详细的字节码插桩操作。本篇博客的示例代码均以 Java 为主,关于 groovy 语法来说仍是有必要去学习一下,由于不是一切人都会用 Java 来写 Gradle 脚本模块,相比于 Java,Groovy 和 Kotlin 关于 Gradle 脚本的编写提供了许多语法糖,能够大大提高开发效率。

假如我的博客共享对你有点协助,不妨点个赞支撑下