cover_image

Gradle基础与应用(插桩)

移动技术团队 货拉拉技术
2023年09月12日 09:30

图片

Gradle基础

Gradle生命周期

Gradle 构建过程可以分为三个不同的阶段,每个阶段具有特定的功能和任务。

1、初始化阶段:

在这个阶段,Gradle确定要参与构建的项目,并为每个项目创建一个Project实例。这是构建的开始阶段,Gradle根据项目根目录下的 settings.gradle文件来确定哪些项目参与构建。

settings.gradle文件指定了构建所需的项目结构。在这个阶段,您可以配置项目的包含关系和层次结构,以便Gradle知道有哪些子项目需要构建。

图片

2、配置阶段:

在配置阶段,Gradle对每个项目对象进行配置。这意味着执行构建脚本,其中包含了所有参与构建的项目的配置信息。在这个阶段,您可以定义任务(Tasks)、依赖关系、构建规则和其他构建配置。

每个子项目的 build.gradle文件在这个阶段被解析,Gradle会创建所有项目所属的任务(Tasks)以及它们之间的依赖关系,并生成一个任务有向图,这个图形表示了任务执行的顺序和依赖关系。

图片

3、执行阶段:

在执行阶段,Gradle确定要执行的任务子集。这个子集由传递给Gradle命令以及当前目录中指定的任务名称参数来确定。Gradle根据配置阶段创建和配置的任务列表,依次执行每个选定的任务。

在执行阶段,Gradle会执行从根项目到子项目的构建过程,逐个执行任务并满足任务之间的依赖关系。这是构建过程的实际执行阶段。

执行具体的task,如clean。注:gradle同步的时候不会触发执行阶段生命周期

图片

通过这三个阶段,Gradle 可以完成从项目初始化到任务执行的全过程,提供了灵活而强大的构建工具。清晰地理解这些阶段的功能有助于更好地使用和配置 Gradle 构建系统。

Gradle Task

在Gradle中,Task(任务)是构建过程的基本单位。每个Task代表一个构建阶段或操作,它可以执行编译、复制文件、运行测试等各种构建任务。任务是Gradle构建脚本中最重要的组成部分之一。

以下从这几个方面介绍Task:

1、创建Task/定位Task

  1. //创建task的几种方式

  2. tasks.register("taskName"){

  3. }

  4. task taskName{

  5. }

  6. task "taskName"{

  7. }

  8. tasks.create("taskName"){

  9. }

  10. //获取task的几种方式

  11. tasks.findByName("")

  12. tasks.findByPath("")

  13. tasks.named("").get()

2、自定义Task

1、继承DefaultTask。 2、声明task执行方法,方法名随意,必须加上@TaskAction注解。 3、创建自定义task的时候 指定task类名。

  1. /**

  2. * 自定义task

  3. */

  4. class MyTask extends org.gradle.api.DefaultTask {

  5. //方法名随意 只要加上TaskAction注解

  6. @org.gradle.api.tasks.TaskAction

  7. void run() {

  8. println("自定义task 执行")

  9. }

  10. }

  11. task myTask(type: MyTask)

3、Task 顺序

设置依赖的task,只有test1 task执行完后才会执行hello task。 hello.dependsOn(test1)

设置终结者任务,执行完hello task之后会执行test2 task,通常可以用该方法做一些清理操作。 hello.finalizedBy(test2) 如果同时执行hello、test3这2个task,会确保test3执行完之后才执行hello这个task,用这个来保证执行顺序 hello.setMustRunAfter([test3])

4、Task输入输出

Gradle 支持一种叫做 up-to-date 检查的功能,也就是常说的增量构建。Gradle 的 Task 会把每次运行的结果缓存下来,当下次运行时,会检查输出结果有没有变更,如果没有变更则跳过运行,这样可以提高 Gradle 的构建速度。 图中表示一个 java 编译的 task,它的输入有2种,一是 JDK 版本号,一是源文件,它的输出结果为 class 文件,只要 JDK 版本号与源文件有任何变动,最终编译出的 class 文件肯定是不同的。当我们执行过一次编译任务后,再次运行该 task ,如果发现它的输入没有任何改变,那么它编译后的结果肯定也是不会变化的,可以直接从缓存里获取输出,这样 Gradle 会标识该 Task 为 UP-TO-DATE,进而跳过该 Task 的执行。

图片

5、Task的一些监听方法

Task执行前后回调方法 voidbeforeTask(Action<Task>action); voidbeforeTask(Closureclosure); voidafterTask(Action<Task>action); voidafterTask(Closureclosure);

小案例

1、打印编译过程中Task的执行时长

  1. //临时存储task对应的时间

  2. def map = new HashMap<String, Long>()

  3. tasks.beforeTask {

  4. Task task ->

  5. map.put(task.getName(), System.currentTimeMillis())

  6. }

  7. tasks.afterTask {

  8. Task task ->

  9. def time = map.get(task.getName())

  10. map.put(task.getName(), System.currentTimeMillis() - time)

  11. }

  12. //构建结束

  13. gradle.buildFinished {

  14. println("buildFinished")

  15. map.forEach {

  16. k, v ->

  17. println("taskName:" + k + " time:" + v)

  18. }

  19. }

图片图片

2、打印编译过程中task的依赖关系

  1. gradle.projectsEvaluated {

  2. println("projectsEvaluated")

  3. //配置完成后 生成task 有向图

  4. TaskExecutionGraph tasks = gradle.getTaskGraph()

  5. tasks.whenReady {

  6. println("TaskExecutionGraph whenReady")

  7. //打印依赖图

  8. TaskExecutionGraph tasks2 = gradle.getTaskGraph()

  9. println("digraph pic { ")

  10. List<Task> taska = tasks2.getAllTasks()

  11. for (i in (taska.size())..<0) {

  12. Task t = taska.get(i - 1)

  13. try {

  14. Set<? extends Task> dependsOn = t.getTaskDependencies().getDependencies()

  15. dependsOn.forEach {

  16. Task dt = it

  17. println(dt.name + " -> " + t.name)

  18. }

  19. } catch (Exception e) {

  20. println("异常了" + e.toString())

  21. }

  22. }

  23. println("}")

  24. }

  25. }

图片

Gradle插件

Gradle有三种创建插件的方式

1、build.gradle

直接在build.gradle中包含插件的源代码。这样做的好处是插件会自动编译并包含在构建脚本的类路径中,而无需执行任何操作。但是,该插件在构建脚本之外是不可见的,因此不能在定义它的构建脚本之外重用该插件。 1、在app下的build.gradle定义自定义插件MyPlugin 并实现Plugin接口 2、在app下的build.gradle使用该插件 apply plugin:MyPlugin

  1. class MyPlugin implements Plugin<Project>{

  2. @Override

  3. void apply(Project project) {

  4. println("应用插件")

  5. project.task('pluginTask') {

  6. doLast {

  7. println '执行插件创建的任务'

  8. }

  9. }

  10. }

  11. }

  12. apply plugin:MyPlugin

2、buildSrc项目

可以将插件的源代码放在rootProjectDir/buildSrc/src/main/java目录中(rootProjectDir/buildSrc/src/main/groovy或rootProjectDir/buildSrc/src/main/kotlin根据喜欢的语言)。Gradle 将负责编译和测试插件,并使其在构建脚本的类路径中可用。该插件对构建使用的每个构建脚本都是可见的。但是,它在构建之外是不可见的,因此不能在定义它的构建之外重用插件。

步骤

1、新建一个buildSrc目录,特殊目录 2、编译后会自动生成build、.gradle相关文件夹 3、创建一个build.gradle文件,从其他地方拷贝过来就好 4、依赖gradle api 5、创建存放源码的目录 6、创建插件类 7、创建resources目录 ,创建properties文件配置插件索引 8、使用插件

图片

图片

  1. plugins {

  2. id 'com.android.application'

  3. id 'kotlin-android'

  4. id 'com.weizhenbin.handleapk'

  5. }

注意:META-INF.gradle-plugins 这是两个文件夹META-INF/gradle-plugins

3、独立项目

可以为插件创建一个单独的项目。该项目生成并发布一个 JAR,然后您可以在多个构建中使用它并与他人共享。通常,这个 JAR 可能包含一些插件,或者将几个相关的任务类捆绑到一个库中。或者两者的某种组合。独立项目和buildSrc类似 只是要发布之后才能使用。

图片

图片

图片

小案例

1、模拟apk自动打包上传服务器

1、创建一个插件 2、在gradle配置完毕之后(这里主要是可增加一些扩展配置信息)创建Task 3、依赖task,执行我们自己的task之前先执行assembleDebug task 先把包打出来,打包之前先clean 最后的依赖顺序 clean ->assembleDebug->handleApkTask

图片

插件的工作

1、遍历编译变体,取得输出文件apk 2、接下来就是对apk做文件操作,修改、加固、上传,随意操作

图片

Gradle应用(asm插桩)

插桩简介

Asm是一种字节码插桩技术,在android编译过程中,在class 打包带dex文件之前,对class进行操作,而这个过程需要借助transform这个task来完成,大致如图:

图片

浅析编译apk的gradle执行过程查看gradle源码 ,可以依赖在我们的appmudule里 就能看到源码了 这里以 implementation 'com.android.tools.build:gradle:4.2.2' 为例,代码入口:

  1. plugins {

  2. id 'com.android.application'

  3. }

配置gradle的debug模式,有助于分析流程

1、Edit Configurations 2、添加 remote

图片

3、将复制的指令 配置在gradle.properties中

图片

4、选择debug 任务

图片

5、执行debug

图片

图片

关于Transform的主要逻辑在TaskManager.createPostCompilationTasks

图片

1、创建transform Task 2、与其他task一样 在相应顺序执行

一个小知识点 Gradle自定义了一个transform ,CustomClassTransform 几乎就是asm插桩的模版代码了,这里主要是用来让Androidstudio通过插桩来实现profiler相关监控用的 https://android.googlesource.com/platform/tools/base/+/studio-master-dev/profiler/transform/src/main/java/com/android/tools/profiler/ProfilerTransform.java

一个小案例 (hook隐私 api 

1、定一个类 继承 Transform 

2、实现getName() 、getInputTypes()、getScopes()、isIncremental()、transform() 

3、遍历jar和项目里的class文件 

4、修改class文件 输出修改后的class文件

图片

1、读取类 ClassReader cr = new ClassReader(inputStream); 

2、遍历类的每个方法 visitMethod 

3、扫描每个方法里的所有指令 MethodVisitor.visitMethodInsn 

4、找到目标方法 if (owner.equals("android/telephony/TelephonyManager")&&name.equals("getDeviceId")) 

5、修改方法 mv.visitMethodInsn(INVOKESTATIC,"com/example/gradledemo/MyTelephonyManager","getDeviceId","(Landroid/telephony/TelephonyManager;)Ljava/lang/String;",isInterface);

修改前

图片

修改后

图片

最后以一个基于okhttp网络监控功能插件的实现思路作为总结

背景:

移动互联网时代,极大部分的APP都需要依赖Server来获取数据,如果因为网络请求慢或者请求失败,导致用户无法顺畅的使用业务功能,会对用户体验造成极大影响。所以我们应该尽可能的监控网络请求的各个阶段,以准对不同问题进行优化,我们以hook okhttp网络库来实现一个简单的网络监控。

hook okhttp网络库是通过插桩的方式对所有使用okhttp的本地代码或者第三方库加入我们的 Interceptor来达到监控网络的能力

技术实现:

插件实现:这里以独立项目的方式创建插件module

1、创建TransformPlugin

  1. public class TransformPlugin implements Plugin<Project> {

  2. @Override

  3. public void apply(Project target) {

  4. BaseExtension android = target.getExtensions().getByType(BaseExtension.class);

  5. //注册 registerTransform

  6. android.registerTransform(new TransformTemplate("MyTransform",false,new MyTransform()));

  7. }

  8. }

2、在MyTransform中对项目的代码和jar包做遍历扫描,根据逻辑识别目标类,进行字节码修改

  1. public class MyTransform implements BiConsumer<InputStream, OutputStream> {

  2. @Override

  3. public void accept(InputStream inputStream, OutputStream outputStream) {

  4. ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);

  5. ClassVisitor visitor = writer;

  6. visitor = new OkHttpAdapter(visitor);

  7. try {

  8. ClassReader cr = new ClassReader(inputStream);

  9. cr.accept(visitor, ClassReader.EXPAND_FRAMES);

  10. outputStream.write(writer.toByteArray());

  11. } catch (IOException e) {

  12. throw new UncheckedIOException(e);

  13. }

  14. }

  15. @NotNull

  16. @Override

  17. public BiConsumer<InputStream, OutputStream> andThen(@NotNull BiConsumer<? super InputStream, ? super OutputStream> after) {

  18. return null;

  19. }

  20. }

3、寻找到okhttp的okhttp2的 com/squareup/okhttp/OkHttpClient 或者okhttp3的 okhttp3/OkHttpClient$Builder

的构造函数,插入我们的代码,插入一个静态方法 addInterceptorToBuilder 或者 addInterceptorToClient

  1. private static final class MethodAdapter extends MethodVisitor implements Opcodes {

  2. public MethodAdapter(MethodVisitor mv) {

  3. super(ASM7, mv);

  4. }

  5. /**

  6. * Looks for a callsite to the non-parameter constructor ("<init>") of two specific classes,

  7. * When one is found, inserts a call into our interceptor after the constructor call.

  8. */

  9. @Override

  10. public void visitMethodInsn(

  11. int opcode, String owner, String name, String desc, boolean itf) {

  12. if (owner.equals(OKHTTP3_BUILDER_CLASS) && isConstructor(opcode, name, desc) && !itf) {

  13. super.visitInsn(DUP);

  14. super.visitMethodInsn(opcode, owner, name, desc, itf);

  15. invoke(OKHTTP3_WRAPPER, "addInterceptorToBuilder", "(Ljava/lang/Object;)V");

  16. } else if (owner.equals(OKHTTP2_CLIENT_CLASS)

  17. && isConstructor(opcode, name, desc)

  18. && !itf) {

  19. super.visitInsn(DUP);

  20. super.visitMethodInsn(opcode, owner, name, desc, itf);

  21. invoke(OKHTTP2_WRAPPER, "addInterceptorToClient", "(Ljava/lang/Object;)V");

  22. }

  23. }

  24. private static boolean isConstructor(int opcode, String name, String desc) {

  25. return (opcode == INVOKESPECIAL && name.equals("<init>") && desc.equals("()V"));

  26. }

  27. private static boolean isConstructor2(int opcode, String name, String desc) {

  28. return (opcode == INVOKESPECIAL && name.equals("<init>") && desc.equals("(Ljava/util/List;)V"));

  29. }

  30. /** Invokes a static method on our wrapper class. */

  31. private void invoke(String wrapper, String method, String desc) {

  32. super.visitMethodInsn(INVOKESTATIC, wrapper, method, desc, false);

  33. }

  34. }

4、接下来只要实现我们自己的方法,添加拦截器

  1. @JvmStatic

  2. fun addInterceptorToBuilder(builder: Any?) {

  3. OkHttp3TrackInterceptor.addToBuilder(builder)

  4. }

  5. @JvmStatic

  6. fun addToBuilder(builder: Any?) {

  7. if (builder is OkHttpClient.Builder) {

  8. builder.addInterceptor(OkHttp3MockInterceptor())

  9. }

  10. }

  11. @JvmStatic

  12. fun addInterceptorToBuilder(client: Any) {

  13. addToClient(client)

  14. }

  15. @JvmStatic

  16. fun addToClient(client: Any?) {

  17. if (client is OkHttpClient) {

  18. client.networkInterceptors().add(OkHttp2Interceptor())

  19. }

  20. }

这样我们就能在自己的拦截器上实现自己的逻辑,网络监控,日志打印等。

最后,只要按照上述的插件篇的发布流程,发布之后就能在其他项目上用起来了。

以上从gradle的基础知识点到gradle插件的应用,赶紧学起来吧。


继续滑动看下一个
货拉拉技术
向上滑动看下一个