{{ v.name }}
{{ v.cls }}类
{{ v.price }} ¥{{ v.price }}
实践出真知笔者有位朋友,每次新学一门语言,都会用来写一个贪吃蛇游戏,以此来检验自己学习的成果。笔者也有类似体会。所谓纸上得来终觉浅,绝知此事要躬行。这一章,笔者将以开发和发布一个gradle插件作为目标,加深学习成果。
官方文档给出了比较详细的实现步骤,本文的脉络会跟官方文档差不了太多,额外增补实际例子和一些实践经验。文中的代码已经托管到了github项目中。需求默认的android打包插件会把apk命名成module-productflavor-buildtype.apk,例如app-official-debug.apk,并且会把包文件发布到固定的位置:module/build/outputs/apk有的时候,这个命名风格并不是你所要的,你也想讲apk输出到别的目录。咱们通过gradle插件来实现自定义。这个插件的需求是:输入一个名为namemap的closure,用来修改apk名字输入一个名为destdir的string,用于输出位置原理简述插件之于gradle根据官方文档定义,插件打包了可重用的构建逻辑,可以适用于不同的项目和构建过程。gradle提供了很多官方插件,用于支持java、groovy等工程的构建和打包。同时也提供了自定义插件的机制,让每个人都可以通过插件来实现特定的构建逻辑,并可以把这些逻辑打包起来,分享给其他人。插件的源码可以使用groovy、scala、java三种语言,笔者不会scala,所以平时只是使用groovy和java。前者用于实现与gradle构建生命周期(如task的依赖)有关的逻辑,后者用于核心逻辑,表现为groovy调用java的代码。另外,还有很多项目使用eclipse或者maven进行开发构建,用java实现核心业务代码,将有利于实现快速迁移。插件打包方式gradle的插件有三种打包方式,主要是按照复杂程度和可见性来划分:buildscript把插件写在build.gradle文件中,一般用于简单的逻辑,只在该build.gradle文件中可见,笔者常用来做原型调试,本文将简要介绍此类。buildsrc项目将插件源代码放在rootprojectdir/buildsrc/src/main/groovy中,只对该项目中可见,适用于逻辑较为复杂,但又不需要外部可见的插件,本文不介绍,有兴趣可以参考此处。独立项目一个独立的groovy和java项目,可以把这个项目打包成jar文件包,一个jar文件包还可以包含多个插件入口,将文件包发布到托管平台上,供其他人使用。本文将着重介绍此类。buildscript插件首先来直接在build.gradle中写一个plugin:classapkdistpluginimplementsplugin{@overridevoidapply(projectproject){project.task('apkdist')<<{println'hello,world!'}}}applyplugin:apkdistplugin命令行运行$./gradlew-papp/apkdist:app:apkdisthello,world!这个插件创建了一个名为apkdist的task,并在task中打印。插件是一个类,继承自org.gradle.api.plugin接口,重载voidapply(projectproject)方法,这个方法将会传入使用这个插件的project的实例,这是一个重要的context。接受外部参数通常情况下,插件使用方需要传入一些配置参数,如bugtags的sdk的插件需要接受两个参数:bugtags{appkey"app_key"//这里是你的appkeyappsecret"app_secret"//这里是你的appsecret,管理员在设置页可以查看}同样,apkdistplugin这个plugin也希望接受两个参数:apkdistconf{namemap{name->println'hello,'+namereturnname}destdir'your-distribution-dir'}参数的内容后面继续完善。那这两个参数怎么传到插件内呢?org.gradle.api.project有一个extensioncontainergetextensions()方法,可以用来实现这个传递。声明参数类声明一个groovy类,有两个默认值为null的成员变量:classapkdistextension{closurenamemap=null;stringdestdir=null;}接受参数project.extensions.create('apkdistconf',apkdistextension);要注意,create方法的第一个参数就是你在build.gradle文件中的进行参数配置的dsl的名字,必须一致;第二个参数,就是参数类的名字。获取和使用参数在create了extension之后,如果传入了参数,则会携带在project实例中,defclosure=project['apkdistconf'].namemap;closure('wow!');printlnproject['apkdistconf'].destdir进化版本一:参数classapkdistextension{closurenamemap=null;stringdestdir=null;}classapkdistpluginimplementsplugin{@overridevoidapply(projectproject){project.extensions.create('apkdistconf',apkdistextension);project.task('apkdist')<<{println'hello,world!'defclosure=project['apkdistconf'].namemap;closure('wow!');printlnproject['apkdistconf'].destdir}}}applyplugin:apkdistpluginapkdistconf{namemap{name->println'hello,'+namereturnname}destdir'your-distribution-directory'}运行结果:$./gradlew-papp/apkdist:app:apkdisthello,world!hello,wow!your-distribution-directory独立项目插件代码写到现在,已经不适合再放在一个build.gradle文件里面了,那也不是我们的目的。建立一个独立项目,把代码搬到对应的地方。理论上,intellijidea开发插件要比androidstudio要方便一点点,因为有对应groovymodule的模板。但其实如果我们了解idea的项目文件结构,就不会受到这个局限,无非就是一个build.gradle构建文件加src源码文件夹。最终项目的文件夹结构是这样:java-library下面我们来一步步讲解。创建项目在androidstudio中新建javalibrarymodule“plugin”。修改build.gradle文件添加groovy插件和对应的两个依赖。//removedjavapluginapplyplugin:'groovy'dependencies{compilegradleapi()//gradlesdkcompilelocalgroovy()//groovysdkcompilefiletree(dir:'libs',include:['*.jar'])}修改项目文件夹src/main项目文件下:移除java文件夹,因为在这个项目中用不到java代码添加groovy文件夹,主要的代码文件放在这里添加resources文件夹,存放用于标识gradle插件的meta-data建立对应文件.├──build.gradle├──libs├──plugin.iml└──src└──main├──groovy│└──com│└──asgradle│└──plugin│├──apkdistextension.groovy│└──apkdistplugin.groovy└──resources└──meta-inf└──gradle-plugins└──com.asgradle.apkdist.properties注意:groovy文件夹中的类,一定要修改成.groovy后缀,ide才会正常识别。resources/meta-inf/gradle-plugins这个文件夹结构是强制要求的,否则不能识别成插件。com.asgradle.apkdist.properties文件如果写过java的同学会知道,这是一个java的properties文件,是key=value的格式。这个文件内容如下:implementation-class=com.asgradle.plugin.apkdistplugin按其语义推断,是指定这个插件的入口类。英文敏感的同学可能会问了,为什么这个文件的承载文件夹是叫做gradle-plugins,使用复数?没错,这里可以指定多个properties文件,定义多个插件,扩展性一流,可以参考linkedin的插件的组织方式。使用这个插件的时候,将会是这样:applyplugin:'com.asgradle.apkdist'因此,com.asgradle.apkdist这个字符串在这里,又称为这个插件的id,不允许跟别的插件重复,取你拥有的域名的反向就不会错。将pluginmodule传到本地maven仓库参考上一篇:拥抱androidstudio之四:maven仓库使用与私有仓库搭建,和对应的demo项目,将包传到本地仓库中进行测试。添加gradle.propertiesproj_name=gradlepluginproj_artifactid=gradlepluginproj_pom_name=localrepositorylocal_repo_url=file:///users/changbinhe/documents/android/repo/proj_group=com.as-gradle.demoproj_version=1.0.0proj_version_code=1proj_websiteurl=http://kvh.ioproj_issuetrackerurl=https://github.com/kevinho/embrace-android-studio-demo/issuesproj_vcsurl=https://github.com/kevinho/embrace-android-studio-demo.gitproj_description=demoappsforembracingandroidstudioproj_licence_name=theapachesoftwarelicense,version2.0proj_licence_url=http://www.apache.org/licenses/license-2.0.txtproj_licence_dest=repodeveloper_id=your-dev-iddeveloper_name=your-dev-namedeveloper_email=your-email@your-mailbox.com在build.gradle添加上传功能applyplugin:'maven'uploadarchives{repositories.mavendeployer{repository(url:local_repo_url)pom.groupid=proj_grouppom.artifactid=proj_artifactidpom.version=proj_version}}上传可以通过运行:$./gradlew-pplugin/cleanbuilduploadarchives在appmodule中使用插件在项目的buildscript添加插件作为classpathbuildscript{repositories{maven{url'file:///users/your-user-name/documents/android/repo/'}jcenter()}dependencies{classpath'com.android.tools.build:gradle:2.1.0-alpha3'classpath'com.as-gradle.demo:gradleplugin:1.0.0'}}在appmodule中使用插件:applyplugin:'com.asgradle.apkdist'命令行运行:$./gradlew-pappapkdist:app:apkdisthello,world!hello,wow!your-distribution-directory可能会遇到问题error:(46,0)cause:com/asgradle/plugin/apkdistplugin:unsupportedmajor.minorversion52.0openfile应该是本机的jdk版本是1.8,默认将pluginmodule的groovy源码编译成了1.8版本的class文件,放在android项目中,无法兼容。需要对pluginmodule的build.gradle文件添加两个参数:sourcecompatibility=1.6targetcompatibility=1.6真正的实现插件需求读者可能会观察到,到目前为止,插件只是跑通了流程,并没有实现本文提出的两个需求,那接下来就具体实现一下。classapkdistpluginimplementsplugin{@overridevoidapply(projectproject){project.extensions.create('apkdistconf',apkdistextension);project.afterevaluate{//只可以在androidapplication或者androidlib项目中使用if(!project.android){thrownewillegalstateexception('mustapply'com.android.application'or'com.android.library'first!')}//配置不能为空if(project.apkdistconf.namemap==null||project.apkdistconf.destdir==null){project.logger.info('apkdistconfshouldbeset!')return}closurenamemap=project['apkdistconf'].namemapstringdestdir=project['apkdistconf'].destdir//枚举每一个buildvariantproject.android.applicationvariants.all{variant->variant.outputs.each{output->filefile=output.outputfileoutput.outputfile=newfile(destdir,namemap(file.getname()))}}}}}必须指出,本文插件实现的需求,其实可以直接在appmodule的build.gradle中写脚本就可以实现。这里做成插件,只是为了做示范。上传到bintray的过程,就不再赘述了,可以参考拥抱androidstudio之四:maven仓库使用与私有仓库搭建。后记至此,这系列开篇的时候挖下的坑,终于填完了。很多人借助这系列的讲解,真正理解了androidstudio和它背后的gradle、groovy,笔者十分高兴。笔者也得到了很多读者的鼓励和支持,心中十分感激。写博客真的是一个很讲究执行力和耐力的事情,但既然挖下了坑,就得填上,对吧?这半年来,个人在android和java平台上也做了更多的事情,也有了更多的体会。as系列,打算扩充几个主题:proguard混淆java&androidtestingmaven私有仓库深入持续集成……待发掘记得有人说,只懂android不懂java,是很可怕的。在这半年以来,笔者在工作中使用java实现了一些后端服务,也认真学习了jvm字节码相关的知识并把它使用到了工作中。在这个过程中,真的很为java平台的活力、丰富的库资源、几乎无止境的可能性所折服。接下来,会写一些跟有关的学习体会,例如:java多线程与锁jvm部分原理字节码操作java8部分特性……待学习随着笔者工作的进展,我也有机会学习使用了别的语言,例如node.js,并实现了一些后端服务。这个语言的活力很强,一些比java现代的地方,很吸引人。有精力会写一写。因为业务所需,笔者所经历的系统,正在处于像面向服务的演化过程中,我们期望建立统一的通讯平台和规范,抽象系统的资源,拆分业务,容器化。这是一个很有趣的过程,也是对我们的挑战。笔者也希望有机会与读者分享。一不小心又挖下了好多明坑和无数暗坑,只是为了激励自己不断往前。在探索事物本质的旅途中,必然十分艰险,又十分有趣,沿途一定风光绚丽,让我们共勉。参考文献官方文档系列导读本文是笔者《拥抱androidstudio》系列第四篇,其他篇请点击:拥抱androidstudio之一:从adt到androidstudio拥抱androidstudio之二:androidstudio与gradle深入拥抱androidstudio之三:溯源,groovy与gradle基础拥抱androidstudio之四:maven公共仓库使用与私有仓库搭建拥抱androidstudio之五:gradle插件使用与开发有问题?在文章下留言或者加qq群:453503476,希望能帮到你。番外笔者kvh在开发和运营bugtags.com,这是一款移动时代首选的bug管理系统,能够极大的提升app开发者的测试效率,欢迎使用、转发推荐。笔者目前关注点在于移动sdk研发,后端服务设计和实现。