插件的价值是什么
不是装逼,也不只是减少一些不太好理解和可能冗余的代码.
核心在于控制编译过程,帮助管理项目,让项目代码更少的实现细节,关注于业务逻辑解耦,管理代码边界.
写这个笔记的原因
我有一句mmp不知……网上要么是进阶教程,要么是漏东西的”入门教程”,找不到一个可以一步步完成的傻瓜教程,让平时几乎不改gradle脚本的我情何以堪,自己写一篇,让找到的人可以省些时间,也作备忘之用.
开撸
Hello World
Android Studio 似乎不能直接创建groovy项目,whatever,创建一个java lib module,把全部文件删完就好了~~
build.gradle里面的内容也是没用的,全部删掉,添加以下代码:
apply plugin: 'groovy' apply plugin: 'maven' dependencies { compile gradleApi() compile localGroovy() } repositories { mavenCentral() } //group和version是提供给Maven使用的,在使用maven导入插件的时候用 group='ml.xuexin.plugins' version='1.0.0' uploadArchives { repositories { mavenDeployer { //提交到远程服务器: // repository(url: "http://www.xxx.com/repos") { // authentication(userName: "admin", password: "admin") // } //本地的Maven地址设置为..:/repos repository(url: uri('../repos')) } } }
很简单,groovy相关的就是必须的,maven相关的就是上传到服务器/本地的东西
跟Java代码类似,新建src/main/groovy/包名/类名.groovy,实现Plugin接口:
package ml.xuexin import org.gradle.api.Plugin import org.gradle.api.Project class MyPlugin implements Plugin<Project> { @Override void apply(Project project) { println 'Hello World' } }
以上代码作用就是在开始build项目的时候,打印一个Hello World而已
新建resources/META-INF/gradle-plugins/插件名.properties文件
implementation-class=ml.xuexin.MyPlugin
这个文件名才是别的地方实际使用的名字:
apply plugin: '插件名'
ok,该上传maven仓库了,用上面的build.gradle,已经可以上传了,Android Studio右边的Gradle窗口,双击upload->uploadArchives就好
引用插件所在的maven库:
根目录的build.gradle的buildscript的repositories加上(这是本地的,后续改服务器仓库)
maven { url uri('./repos') }
dependencies加上(前面有配置group,version,myplugin为module名)
classpath 'ml.xuexin.plugins:myplugin:1.0.0'
在想要使用插件的module应用咯,上面有写
然后clean,就可以看到效果了,另外Android Studio显示gradle log的窗口有点坑,太不显眼了,名字叫Gradle Console,目前版本在右下角,如果找不到就用双shift搜索吧
稍微有一点点功能的插件,记录编译时间
首先,来个找了半天的傻逼问题,这个插件应该应用到哪儿?
根bulid.gradle对不对,我真是脑抽……
groovy本质是Java,所以实在不知道语法的时候,就直接打Java代码吧^_^
写这么一个类:
class TimeListener implements TaskExecutionListener, BuildListener { private long now private times = [] @Override void beforeExecute(Task task) { now = System.currentTimeMillis() } @Override void afterExecute(Task task, TaskState taskState) { def ms = System.currentTimeMillis() - now times.add([ms, task.path]) } void printTime() { long totalTime = 0 println "耗时超过100ms的项目:" for (time in times) { totalTime += time[0] if (time[0] >= 100) { printf "%7sms %s\n", time } } printf "总耗时:%.2fs\n", totalTime / 1000.0 } @Override void buildStarted(Gradle gradle) { } @Override void settingsEvaluated(Settings settings) { } @Override void projectsLoaded(Gradle gradle) { } @Override void projectsEvaluated(Gradle gradle) { } @Override void buildFinished(BuildResult buildResult) { printTime() } }
ok,在插件类添加下Listener一下就完了:
class MyPlugin implements Plugin<Project> { @Override void apply(Project project) { project.gradle.addListener(new TimeListener()) } }
大功告成,完成的时候会打印这样的信息:
耗时超过100ms的项目: 225ms :app:processDebugManifest 177ms :mylibrary:processDebugResources 163ms :mylibrary2:processDebugResources 1127ms :app:processDebugResources 247ms :mylibrary:compileDebugJavaWithJavac 171ms :mylibrary2:compileDebugJavaWithJavac 2815ms :app:compileDebugKotlin 476ms :app:compileDebugJavaWithJavac 19868ms :app:transformClassesWithDexBuilderForDebug 3263ms :app:transformDexArchiveWithExternalLibsDexMergerForDebug 742ms :app:transformDexArchiveWithDexMergerForDebug 531ms :app:transformNativeLibsWithMergeJniLibsForDebug 712ms :app:transformResourcesWithMergeJavaResForDebug 444ms :app:packageDebug 总耗时:31.41s
插入代码(最直接的应用场景:AOP)
Transform
从
偷来的图上可以看到,编译的过程就是不断in/out的过程.transform就是中间一环啦在插件build.gradle添加依赖:
compile 'com.android.tools.build:gradle:3.0.1'
继承Transform:
class MyTransform extends Transform { private Project project MyTransform(Project project) { super() this.project = project } //transform的名称 //transformClassesWithMyClassTransformForDebug 运行时的名字 //transformClassesWith + getName() + For + Debug或Release @Override String getName() { return this.class.simpleName } //需要处理的数据类型,有两种枚举类型 //CLASSES和RESOURCES,CLASSES代表处理的java的class文件,RESOURCES代表要处理java的资源 @Override 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 Set<? super QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT } //当前Transform是否支持增量编译 @Override boolean isIncremental() { return false } // Transform中的核心方法, // inputs中是传过来的输入流,其中有两种格式,一种是jar包格式一种是目录格式。 // outputProvider 获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错 @Override void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException { } }
在插件中注册这个transform:
project.android.registerTransform(new MyTransform(project))
这就完成了一个只要apply就编译不过,但是很难找原因的插件233
修复它:
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException { // Transform的inputs有两种类型,一种是目录,一种是jar包,要分开遍历 inputs.each { TransformInput input -> //对类型为“文件夹”的input进行遍历 input.directoryInputs.each { DirectoryInput directoryInput -> //文件夹里面包含的是我们手写的类以及R.class、BuildConfig.class以及R$XXX.class等 // 获取output目录 def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) // 将input的目录复制到output指定目录 FileUtils.copyDirectory(directoryInput.file, dest) } //对类型为jar文件的input进行遍历 input.jarInputs.each { JarInput jarInput -> //jar文件一般是第三方依赖库jar文件 // 重命名输出文件(同目录copyFile会冲突) def jarName = jarInput.name def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath()) if (jarName.endsWith(".jar")) { jarName = jarName.substring(0, jarName.length() - 4) } //生成输出路径 def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR) //将输入内容复制到输出 FileUtils.copyFile(jarInput.file, dest) } } }
这里用了apache的开源IO库,DigestUtils,FileUtils又需要添加依赖:
compile group: 'commons-codec', name: 'commons-codec', version: '1.4'
不知道代码提示什么鬼,反正我没提示,得自己手动添加import:
import org.apache.commons.codec.digest.DigestUtils import org.apache.commons.io.FileUtils
ok,绕了这么大弯,终于在插入IDE黑盒子的情况下能正确编译了,马上就是见证奇迹的时候了
Javassist
这是什么呢,简单的说,就是提供傻瓜化修改字节码的开源库,有了他,我们就可以不用看着字节码一脸懵逼了,说这个做不了了,世界多美好
添加依赖:
compile 'org.javassist:javassist:3.22.0-GA'
新建一个类来专门处理注入:
package ml.xuexin.insertcode import javassist.ClassPool import javassist.CtClass import javassist.CtConstructor class MyInject { private static ClassPool pool = ClassPool.getDefault() private static String injectStr = "System.out.println(\"插入的代码\" ); " static void injectDir(String path, String packageName) { pool.appendClassPath(path) File dir = new File(path) if (dir.isDirectory()) { dir.eachFileRecurse { File file -> String filePath = file.absolutePath //确保当前文件是class文件,并且不是系统自动生成的class文件 if (filePath.endsWith(".class") && !filePath.contains('R$') && !filePath.contains('R.class') && !filePath.contains("BuildConfig.class")) { // 判断当前目录是否是在我们的应用包里面 int index = filePath.indexOf(packageName) boolean isMyPackage = index != -1 if (isMyPackage) { int end = filePath.length() - 6 // .class = 6 String className = filePath.substring(index, end) .replace('\\', '.').replace('/', '.') //开始修改class文件 CtClass c = pool.getCtClass(className) if (c.isFrozen()) { c.defrost() } CtConstructor[] cts = c.getDeclaredConstructors() if (cts == null || cts.length == 0) { //手动创建一个构造函数 CtConstructor constructor = new CtConstructor(new CtClass[0], c) constructor.insertBeforeBody(injectStr) c.addConstructor(constructor) } else { cts[0].insertBeforeBody(injectStr) } c.writeFile(path) c.detach() } } } } } }
Transform中调用:
@Override void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException { // Transform的inputs有两种类型,一种是目录,一种是jar包,要分开遍历 inputs.each { TransformInput input -> //对类型为“文件夹”的input进行遍历 input.directoryInputs.each { DirectoryInput directoryInput -> //文件夹里面包含的是我们手写的类以及R.class、BuildConfig.class以及R$XXX.class等 MyInject.injectDir(directoryInput.file.absolutePath,"ml/xuexin/learnplugin") // 获取output目录 def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) // 将input的目录复制到output指定目录 FileUtils.copyDirectory(directoryInput.file, dest) } //对类型为jar文件的input进行遍历 input.jarInputs.each { JarInput jarInput -> //jar文件一般是第三方依赖库jar文件 // 重命名输出文件(同目录copyFile会冲突) def jarName = jarInput.name def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath()) if (jarName.endsWith(".jar")) { jarName = jarName.substring(0, jarName.length() - 4) } //生成输出路径 def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR) //将输入内容复制到输出 FileUtils.copyFile(jarInput.file, dest) } } }
跟之前其实只差了一句话:
MyInject.injectDir(directoryInput.file.absolutePath,"ml/xuexin/learnplugin")
其中包名,看的文章用的windows,是”\\”,我是mac,换成了”/”,印象中在windows中用”/”也是可以的,待测试
MainActivity:
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Log.e("--->", "===================") Test() Log.e("--->", "===================") } } class Test
见证奇迹的时刻到了:
12-21 08:52:22.465 28669-28669/ml.xuexin.learnplugin E/--->: =================== 12-21 08:52:22.465 28669-28669/ml.xuexin.learnplugin I/System.out: 插入的代码 12-21 08:52:22.465 28669-28669/ml.xuexin.learnplugin E/--->: ===================