我是靠谱客的博主 淡然树叶,这篇文章主要介绍Android Gradle插件开发基础实例,现在分享给大家,希望可以做个参考。

    作为一名Android码农,相信大家也有跟我一样的感觉,很多框架或者第三方的SDK我们是只会用,但是很少去了解如何实现和它的原理是什么。主要也不是我比较懒,而是工作环境的影响很少有时间去研究。不过想成为一名技术资深的码农,了解原理并且学会自己造轮子是必走的路,我也开始反思了。

    好了言归正传,  相信很多同僚都知道build.gradle的作用,负责构建我们的App,语言用的是Groovy,但是其实里面真正起作用的是Plugin插件,如下图

导入了一个id为xxx的插件,其中android{}都是这个插件的Task,gradle作用就是把一个一个Plugin一次执行。

    我这次要演示的批量给每个Activity的进入和退出插入Toast(第三方统计的方案有类似实现)。

    一般我们如果需要App每个页面的进入和退出加入埋点,我们初级版本会每个类的onCreate和onDestroy加入自己的埋点代码,高级版本可以自己封装一个BaseActivity,所有Activity继承它。而我今天要使用的是一个便于维护在各个App的方案,后续还可以看自己需要放到maven库。

    我们需要在java文件生成Class到Dex文件之间去修改类的Class文件,这样就能够不修改源码的情况下无感知的加入了我们的代码。

 

  • 首先使用AndroidStudio新建一个自己作为Demo的工程

我这个叫

  • 然后创建一个Module

删除掉所有文件,只保留main目录(目录下文件清空)和build.gradle

maingroovy目录下创建自己的包路径,我这里是cn.berfy.gradle.plugin。

在main目录下创建resources文件夹,新建META-INFgradle-plugins目录,这里是存放最终给其他build.gradle调用apply的路径名。

新建cn.berfy.gradle.plugin.properties文件,编写

复制代码
1
implementation-class=cn.berfy.gradle.plugin.TestPlugi

创建自己的包路径,我这里是cn.berfy.gradle.plugin

 

  • 再然后就是一步步写代码,有点长,耐心看完。

编写build.gradle文件

开头导入groovy,如果想用kotlin开发插件也可以导入。

apply plugin: 'maven-publish'

加入maven发布功能

dependencies 下引入gradleApi(gradleApi)和localGroovy(groovy Api)

引入com.android.tools.build:gradle支持

引入org.ow2.asm:asm:7.1(这个是真正修改字节码的Api)

groupId 你的maven库要显示的包名

artifactId  你的库:后面的名字

version 版本

repositories 是maven生成的repo文件路径,可以自定义,这里稍加修改可以自动上传到maven仓库

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
apply plugin: 'groovy' apply plugin: 'kotlin' //apply plugin: 'java-gradle-plugin' //apply plugin: 'cn.berfy.gradle.plugin.TestPlugin' dependencies { //gradle sdk implementation gradleApi() //groovy sdk implementation localGroovy() implementation 'com.android.tools.build:gradle:3.4.1' implementation 'org.ow2.asm:asm:7.1' } apply plugin: 'maven-publish' sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 publishing { publications { mavenJava(MavenPublication) { groupId 'cn.berfy.gradle.plugin' artifactId 'berfy' version '1.0.0' from components.java } } } publishing { repositories { maven { // change to point to your repo, e.g. http://my.org/repo url uri('src/main/repos') } } }

----------------------------------------------------------------

groovy目录下你的包名根路径新建一个TestPlugin.groovy文件,内容为

AppExtension就是apply plugin: 'com.android.application'这个插件的扩展。

我们检测到这个Plugin就传递给MyClassTransform负责拦截App编译后的操作。

 

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package cn.berfy.gradle.plugin import org.gradle.api.Plugin import org.gradle.api.Project import com.android.build.gradle.AppExtension class TestPlugin implements Plugin<Project> { void apply(Project project) { def android = project.extensions.getByType(AppExtension) //注册一个Transform def logTransform = new MyClassTransform(project) android.registerTransform(logTransform) } }

新建MyClassTransform.groovy,负责class文件和jar文件的拦截,便于我们队class字节码修改。
getInputType这里用的是Class的枚举,拦截编译到class这步。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
package cn.berfy.gradle.plugin; import com.android.build.api.transform.* import com.android.build.gradle.internal.pipeline.TransformManager import org.apache.commons.codec.digest.DigestUtils import org.apache.commons.io.FileUtils import org.gradle.api.Project import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassWriter import java.io.IOException import java.lang.reflect.Field; import java.util.Collection; import java.util.Set; /** * Created by 刘镓旗 on 2017/8/30. */ public class MyClassTransform extends Transform { private Project mProject; public MyClassTransform(Project p) { this.mProject = p; } //transform的名称 //transformClassesWithMyClassTransformForDebug 运行时的名字 //transformClassesWith + getName() + For + Debug或Release @Override public String getName() { return "MyClassTransform"; } //需要处理的数据类型,有两种枚举类型 //CLASSES和RESOURCES,CLASSES代表处理的java的class文件,RESOURCES代表要处理java的资源 @Override public Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS; } // 指Transform要操作内容的范围,官方文档Scope有7种类型: // // EXTERNAL_LIBRARIES 只有外部库 // PROJECT 只有项目内容 // PROJECT_LOCAL_DEPS 只有项目的本地依赖(本地jar) // PROVIDED_ONLY 只提供本地或远程依赖项 // SUB_PROJECTS 只有子项目。 // SUB_PROJECTS_LOCAL_DEPS 只有子项目的本地依赖项(本地jar)。 // TESTED_CODE 由当前变量(包括依赖项)测试的代码 @Override public Set<QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT; } //指明当前Transform是否支持增量编译 @Override public boolean isIncremental() { return false; } // Transform中的核心方法, // inputs中是传过来的输入流,其中有两种格式,一种是jar包格式一种是目录格式。 // outputProvider 获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错 // @Override // public void transform(Context context, // Collection<TransformInput> inputs, // Collection<TransformInput> referencedInputs, // TransformOutputProvider outputProvider, // boolean isIncremental) throws IOException, TransformException, InterruptedException { // super.transform(context, inputs, referencedInputs, outputProvider, isIncremental) // println("字节码插桩的地方 哈哈哈哈") // } @Override void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { super.transform(transformInvocation) println("==============================TracePlugin visit start========================================") def isIncremental = transformInvocation.isIncremental() //OutputProvider管理输出路径,如果消费型输入为空,你会发现OutputProvider == null def outputProvider = transformInvocation.outputProvider if (!isIncremental) { //不需要增量编译,先清除全部 outputProvider.deleteAll() } transformInvocation.inputs.forEach { input -> input.jarInputs.forEach { jarInput -> //处理Jar println("fix Jar name=" + jarInput.name) processJarInput(jarInput, outputProvider, isIncremental) } input.directoryInputs.forEach { directoryInput -> //处理文件 println("fix file name=" + directoryInput.name) processDirectoryInput(directoryInput, outputProvider, isIncremental) } } println("==============================TracePlugin visit end========================================") } //============================================jar文件修改总入口======================================================================= //jar输入文件 修改 void processJarInput(JarInput jarInput, TransformOutputProvider outputProvider, boolean isIncremental) { def dest = outputProvider.getContentLocation( jarInput.file.absolutePath, jarInput.contentTypes, jarInput.scopes, Format.JAR) if (isIncremental) { //处理增量编译 processJarInputIncremental(jarInput, dest) } else { //不处理增量编译 processJarInputNoIncremental(jarInput, dest) } } //jar 没有增量的修改 void processJarInputNoIncremental(JarInput jarInput, File dest) { transformJarInput(jarInput, dest) } //jar 增量的修改 void processJarInputIncremental(JarInput jarInput, File dest) { switch (jarInput.status) { case Status.NOTCHANGED: break; case Status.ADDED: //真正transform的地方 transformJarInput(jarInput, dest) break; case Status.CHANGED: //Changed的状态需要先删除之前的 if (dest.exists()) { FileUtils.forceDelete(dest) } //真正transform的地方 transformJarInput(jarInput, dest) break; case Status.REMOVED: //移除Removed if (dest.exists()) { FileUtils.forceDelete(dest) } break; } } //真正执行jar修改的函数 void transformJarInput(JarInput jarInput, File dest) { FileUtils.copyFile(jarInput.file, dest) } //============================================================文件及文件夹修改总入口====================================================================== void processDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider, boolean isIncremental) { def dest = outputProvider.getContentLocation( directoryInput.file.absolutePath, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) println("fix processDirectoryInput isIncremental=" + isIncremental) if (isIncremental) { //处理增量编译 processDirectoryInputIncremental(directoryInput, dest) } else { processDirectoryInputNoIncremental(directoryInput, dest) } } //文件无增量修改 void processDirectoryInputNoIncremental(DirectoryInput directoryInput, File dest) { println("fix processDirectoryInputNoIncremental ") transformDirectoryInput(directoryInput, dest) } //文件增量修改 void processDirectoryInputIncremental(DirectoryInput directoryInput, File dest) { println("fix processDirectoryInputIncremental ") FileUtils.forceMkdir(dest) def srcDirPath = directoryInput.file.absolutePath def destDirPath = dest.absolutePath def fileStatusMap = directoryInput.changedFiles fileStatusMap.forEach { entry -> val inputFile = entry.key val status = entry.value val destFilePath = inputFile.absolutePath.replace(srcDirPath, destDirPath) val destFile = File(destFilePath) switch (status) { case Status.NOTCHANGED: break; case Status.ADDED: //真正transform的地方 transformDirectoryInput(directoryInput, dest) break; case Status.CHANGED: //处理有变化的 FileUtils.touch(destFile) //Changed的状态需要先删除之前的 if (dest.exists()) { FileUtils.forceDelete(dest) } //真正transform的地方 transformDirectoryInput(directoryInput, dest) break; case Status.REMOVED: //移除Removed if (destFile.exists()) { FileUtils.forceDelete(destFile) } break; } } } //真正执行文件修改的地方 void transformDirectoryInput(DirectoryInput directoryInput, File dest) { println("fix transformDirectoryInput ") // directoryInput.forEach { directoryInput: DirectoryInput? -> //是否是目录 if (directoryInput.file.isDirectory()) { println("fix transformDirectoryInput isDirectory ") List<File> files = new ArrayList<>() findAllFiles(directoryInput.file.listFiles(), files) for (File file : files) { def name = file.name //在这里进行代码处理 if (name.endsWith(".class") && !name.startsWith("R$") && "R.class" != name && "BuildConfig.class" != name) { ClassReader classReader = new ClassReader(file.readBytes()) ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS) def className = name.split(".class")[0] println("class fix chazhuang " + className) def classVisitor = new TraceVisitor(className, classWriter) classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES) def code = classWriter.toByteArray() FileOutputStream fos = new FileOutputStream(file.parentFile.absoluteFile.toString() + File.separator + name) fos.write(code) fos.close() } } } else { println("fix transformDirectoryInput isFile ") def name = directoryInput.file.name //在这里进行代码处理 if (name.endsWith(".class") && !name.startsWith("R$") && "R.class" != name && "BuildConfig.class" != name) { // ClassReader classReader = ClassReader(file.readBytes()) // ClassWriter classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS) // def className = name.split(".class")[0] println("class fix hahahaha " + name) } } //将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了 FileUtils.copyDirectory(directoryInput.file, dest) } void findAllFiles(File[] files, List<File> outFiles) { if (null == outFiles) { return } for (File out : files) { if (out.isDirectory()) { findAllFiles(out.listFiles(), outFiles) } else { outFiles.add(out) } } } }

transform(TransformInvocation transformInvocation)方法就是拦截到的class文件和jar包。

通过获取到class文件,修改,然后修改后的文件复制到输出路径中完成class的替换。这其中字节码的修改就是ASM的操作了,操作类是TraceVisitor.java。

创建TraceVisitor.java

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package cn.berfy.gradle.plugin; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.commons.AdviceAdapter; /** * 对继承自AppCompatActivity的Activity进行插桩 */ public class TraceVisitor extends ClassVisitor { /** * 类名 */ private String className; /** * 父类名 */ private String superName; /** * 该类实现的接口 */ private String[] interfaces; public TraceVisitor(String className, ClassVisitor classVisitor) { super(Opcodes.ASM7, classVisitor); } /** * ASM进入到类的方法时进行回调 * * @param access * @param name 方法名 * @param desc * @param signature * @param exceptions * @return */ @Override public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, String[] exceptions) { MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions); methodVisitor = new AdviceAdapter(Opcodes.ASM5, methodVisitor, access, name, desc) { private boolean isInject() { //如果父类名是AppCompatActivity则拦截这个方法,实际应用中可以换成自己的父类例如BaseActivity if (superName.contains("AppCompatActivity")) { return true; } return false; } @Override public void visitCode() { super.visitCode(); } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { return super.visitAnnotation(desc, visible); } @Override public void visitFieldInsn(int opcode, String owner, String name, String desc) { super.visitFieldInsn(opcode, owner, name, desc); } /** * 方法开始之前回调 */ @Override protected void onMethodEnter() { if (isInject()) { if ("onCreate".equals(name)) { mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESTATIC, "cn/berfy/demo/gradleplugin/asm/TraceUtil", "onActivityCreate", "(Landroid/app/Activity;)V", false); } else if ("onDestroy".equals(name)) { mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESTATIC, "com/xuexuan/androidaop/traceutils/TraceUtil" , "onActivityDestroy", "(Landroid/app/Activity;)V", false); } } } /** * 方法结束时回调 * @param i */ @Override protected void onMethodExit(int i) { super.onMethodExit(i); } }; return methodVisitor; } /** * 当ASM进入类时回调 * * @param version * @param access * @param name 类名 * @param signature * @param superName 父类名 * @param interfaces 实现的接口名 */ @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, interfaces); this.className = name; this.superName = superName; this.interfaces = interfaces; } }

ClassVisitor是对类的字节码操作,MethodVisitor是方法。

isInject()拦截类的类型,这里我们只处理AppCompatActivity。

onMethodEnter()方法执行的开始。我们在这里增加自己的代码。

具体的代码意思就是在onCreate方法开始执行的时候,插入cn.berfy.demo.gradleplugin/asm/TraceUtil.class类中的onActivityCreate的方法中的内容。具体可以学习下ASM语法和JVM字节码说明。

TraceUtil.class类可以写在module或者App module中,只要保证运行的class包名正确即可。

这样每个Acitivity就会在onCreate生命周期是调用我们自己类的方法(也不是调用,是插桩,可以叫代码复制)。

我的App module下的TraceUtil.class

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package cn.berfy.demo.gradleplugin.asm; import android.app.Activity; import android.widget.Toast; import cn.berfy.demo.gradleplugin.MainActivity; /** * Created by will on 2018/3/9. */ public class TraceUtil { private final String TAG = "TraceUtil"; /** * 当Activity执行了onCreate时触发 * * @param activity */ public static void onActivityCreate(Activity activity) { Toast.makeText(activity, MainActivity.class.getName() + " 我是插桩的检测代码", Toast.LENGTH_LONG).show(); // System.out.println(MainActivity.class); } /** * 当Activity执行了onDestroy时触发 * * @param activity */ public static void onActivityDestroy(Activity activity) { Toast.makeText(activity, activity.getClass().getName() + "call onDestroy", Toast.LENGTH_LONG).show(); } }

插件到这里就编写好了

我们到terminal中执行gradlew publish等待成功。如果失败看一眼log,很好理解。

成功之后mainrepo目录就会生成对应你plugin下的build.gradle中的groupIdartifactIdversion的pom源文件。

 

  • 最后

关键的关键

项目根目录build.gradle加入maven库和classPath

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() jcenter() maven {//local maven repo path url uri('plugin/src/main/repos') } } dependencies { classpath "com.android.tools.build:gradle:4.1.3" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72" classpath 'cn.berfy.gradle.plugin:berfy:1.0.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() jcenter() } } task clean(type: Delete) { delete rootProject.buildDir }

App module的build.gradle

复制代码
1
2
3
4
5
6
plugins { id 'com.android.application' } apply plugin: 'cn.berfy.gradle.plugin.TestPlugin'

最终运行

 

最后

以上就是淡然树叶最近收集整理的关于Android Gradle插件开发基础实例的全部内容,更多相关Android内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(81)

评论列表共有 0 条评论

立即
投稿
返回
顶部