货拉拉技术

Gradle基础与应用(插桩)

Image

Gradle基础

Gradle生命周期

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

1、初始化阶段:

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

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

Image

2、配置阶段:

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

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

Image

3、执行阶段:

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

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

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

Image

通过这三个阶段,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. classMyTaskextends 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 的执行。

Image

5、Task的一些监听方法

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

小案例

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

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

  2. def map =newHashMap<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. }

ImageImage

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<?extendsTask> 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. }

Image

Gradle插件

Gradle有三种创建插件的方式

1、build.gradle

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

  1. classMyPluginimplementsPlugin<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、使用插件

Image

Image

  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类似 只是要发布之后才能使用。

Image

Image

Image

小案例

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

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

Image

插件的工作

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

Image

Gradle应用(asm插桩)

插桩简介

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

Image

浅析编译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

Image

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

Image

4、选择debug 任务

Image

5、执行debug

Image

Image

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

Image

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文件

Image

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

修改前

Image

修改后

Image

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

背景:

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

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

技术实现:

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

1、创建TransformPlugin

  1. publicclassTransformPluginimplementsPlugin<Project>{

  2. @Override

  3. publicvoid apply(Project target){

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

  5. //注册 registerTransform

  6. android.registerTransform(newTransformTemplate("MyTransform",false,newMyTransform()));

  7. }

  8. }

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

  1. publicclassMyTransformimplementsBiConsumer<InputStream,OutputStream>{

  2. @Override

  3. publicvoid accept(InputStream inputStream,OutputStream outputStream){

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

  5. ClassVisitor visitor = writer;

  6. visitor =newOkHttpAdapter(visitor);

  7. try{

  8. ClassReader cr =newClassReader(inputStream);

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

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

  11. }catch(IOException e){

  12. thrownewUncheckedIOException(e);

  13. }

  14. }

  15. @NotNull

  16. @Override

  17. publicBiConsumer<InputStream,OutputStream> andThen(@NotNullBiConsumer<?superInputStream,?superOutputStream> after){

  18. returnnull;

  19. }

  20. }

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

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

  1. privatestaticfinalclassMethodAdapterextendsMethodVisitorimplementsOpcodes{

  2. publicMethodAdapter(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. publicvoid 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. }elseif(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. privatestaticboolean isConstructor(int opcode,String name,String desc){

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

  26. }

  27. privatestaticboolean 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. privatevoid 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 isOkHttpClient.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 isOkHttpClient){

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

  19. }

  20. }

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

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

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