查看原文
其他

玩转 Gradle,Plugin本质是什么?剖析个知名插件

CoderPig 鸿洋 2021-10-13

本文作者


作者:CoderPig

链接:

https://juejin.cn/post/6956517422606057479

本文由作者授权发布。


关于 Gradle 的学习,我所理解的流程如下:


1. 了解 Gradle 的本质,了解其构建生命周期;

2. 熟悉其内部的核心对象,Projet、Task 等等;

3. 熟悉 Groovy 或者 kotlin,可以自己编写一些简单的 task;

4. 学会自定义 Gradle Plugin;

5. 对Android 打包构建过程中,涉及到的核心 task 分析;


剩下的只有多看了,会在各种开源 plugin 或者博客中看到各种小技巧,然后在不断的「还能这么写?」「我*,这也可以」「可以,学到了」中成长,增加个人解决问题的技巧与方式。


本文大概属于环节4,对于后续更深入的打包源码分析分享也在路上了,好了看文吧~




在这一篇《补齐Android技能树 - 玩转Gradle(二)》提到过插件,有下面这样一段话:
Gradle自身并没有提供编译打包的功能,它只是一个负责定义流程和规则的框架,具体的编译工作都是由 插件 来完成的,比如编译Java用Java插件,编译Kotlin用Kotlin插件。插件的本质就是:定义Task,并具体执行这些Task的模板。
https://juejin.cn/post/6955008819868991495

本节就来了解下:Gradle插件编写的知识储备及插件发布相关姿势,读者亦可自行查阅官方文档Packaging a plugin

https://docs.gradle.org/current/userguide/custom_plugins.html


Gradle插件本质上是一个jar文件,可以用idea创建项目,也可以使用gradle init命令创建,示例如下:



Tips:选择kotlin、Java、Kotlin实现都可,脚本DSL选kotlin、Kotlin皆可,笔者都用的kotlin ~


1Gradle插件分类


1. 构建脚本


严格来说不上插件,就是把构建脚本代码写到一个单独的文件中,复制粘贴到项目目录下引用。示例如下:


外部构建脚本:other.gradle


// 脚本内部访问
def versionName = "v1.0.0"
def versionDesc = "第一个版本"

// 脚本外部访问
ext {
    author = "CoderPig"
}

// Task任务
task printVersionInfo {
    doLast {
        println "$versionName → $versionDesc"
    }
}

build.gradle中引用此脚本:


apply from: 'other.gradle'

task test {
    dependsOn(printVersionInfo)
    doFirst { println(author) }
    doLast { println("build.gradle里的task") }
}

键入gradle test执行任务结果输出如下:




2. buildSrc项目


执行Gradle时会将根目录下的buildSrc目录作为插件源码目录进行编译,并将结果加入到构建脚本的classpath中。


这种插件不需要plugins{}引入,一般适合没有复用性的插件或者新插件开发调试用,还有个缺点:无法使用属性配置DSL,需通过configure<...>{...}配置插件属性。

3. 独立项目


将项目打成jar包,可在多个项目间复用,一般的插件开发都是指的这一类~


2Gradle Plugin Demo 初体验


我们上面通过命令行创建了一个Gradle插件项目,接着来康一康都有哪些核心要素( 顺带吐槽下网上一堆说得不清不楚的文章...)


1. 项目组成


①插件实现类:CpPluginPlugin.kotlin


package cn.coderpig.plugins

import org.gradle.api.Project
import org.gradle.api.Plugin

publicclassCpPluginPluginimplementsPlugin<Project> {
    public void apply(Project project) {
        // 注册一个Task
        project.tasks.register("greeting") {
            doLast {
                println("Hello from plugin 'cn.coderpig.plugins.greeting'")
            }
        }
    }
}

代码比较浅显易懂,实现了Plugin接口,指定泛型类为Project,定义了apply方法,并注册了一个名为greeting带闭包的Task,打印一句话。


② 根目录下的build.gradle


// 引用java gradle插件开发插件、kotlin支持
plugins {
    id 'java-gradle-plugin'
    id 'kotlin'
}

gradlePlugin {
    plugins {
        greeting {
            // 插件id
            id = 'cn.coderpig.plugins.greeting'
            // 插件实现类
            implementationClass = 'cn.coderpig.plugins.CpPluginPlugin'
        }
    }
}


Tips:网上很多Gradle插件开发教程还要另外配置一个properties文件,如:

src/main/resources/META-INF/gradle-plugins/cn.coderpig.plugins.greeting.properties

文件内容如下:

implementation-class=cn.coderpig.plugins.CpPluginPlugin

实际上,在build.gradle中声明了gradlePlugin就可以了,无需另外再配置一遍!

2. 插件本地发布


使用插件可以使用上面的buildSrc方式引入,也可以先打成jar包,自用就发布到本地,分享给别人用就发布到Maven或者JCenter仓库。先试试发布到本地吧,需要添加Maven相关的配置:


plugins {
    // 添加maven插件
    id 'maven'
}

uploadArchives {
    repositories.mavenDeployer {
        repository(url: uri('C:\\Users\\用户名\\Maven\\repo'))   // 本地仓库路径
        pom.groupId = "cn.coderpig.plugins"// 唯一标识(通常为模块包名,也可以任意)
        pom.artifactId = "CpPluginPlugin"// 项目名称(通常为类库模块名称,也可以任意)
        pom.version = "0.0.1"// 版本号
    }
}

配置完Sync Now重新构建下项目,在Gradle窗口就会多出一个uploda目录,里面的uploadArchives就是将插件类库发布到仓库的Task:



双击执行此task,在 C:\Users\用户名\Maven\repo 下生成下述文件:



发布到本地Maven后,就可以在另一个项目中引用验证插件效果,先修改根目录的build.gradle


buildscript {
    repositories {
        ...
        // 本地Maven地址
        maven { url 'C:\\Users\\用户名\\Maven\\repo' }
    }
    dependencies {
        ...
        // 插件依赖
        classpath "cn.coderpig.plugins:cpplugin:0.0.1"
    }


app目录或module目录的build.gradle引用此插件:


plugins {
    ...
    id 'cn.coderpig.plugins.greeting'
}

接着写一个Task来验证下:


task("testPlugin") {
    group("custom"// 分组,方便找到Task
    dependsOn('greeting')   // 调用插件里的greeting Task,在执行testPlugin
    doLast { println '任务执行完毕' }
}

运行结果如下:



3. 插件远程发布


① 废弃的JCenter


说到插件远程发布,网上十有八九的教程都是传到JFlog Bintray

https://bintray.com/


打开官网却是大红提示:



其实早在今年的2.3,官方就发布了一则通告:


包括 GoCenter、Bintray、JCenter 在内的多项软件包管理和分发服务都将停止运营。

自3.31后就不在接受任何新的提交,在2022.2.1前,你还是可以正常拉取2021.3.31前提交的库。


不能提交的话就只能找找JCenter的替代品咯,因为这里编写的是Gradle插件,可以试下提交到Gradle的远程仓库。


② Gradle Plugin仓库


完整流程介绍可参见官方文档:How do I add my plugin to the plugin portal?

https://plugins.gradle.org/docs/submit


这里简述下步骤:


先注册个账号 (Github授权登录亦可),登录后点击生成API Keys:



复制粘贴到本地gradle配置文件中:HOME_DIR/.gradle/gradle.properties (~/.gradle/gradle.properties)


接着照着:How do I use the Plugin Publishing Plugin?

https://plugins.gradle.org/docs/publish-plugin


配置下com.gradle.plugin-publish上传插件需要的一些参数:


plugins {
    ...
    id "com.gradle.plugin-publish" version "0.14.0"// 上传插件
}

version = "0.0.1"// 自定义插件版本
group = "cn.coderpig.plugins"// 自定义插件分组

// 自定义插件id及实现类
gradlePlugin {
    plugins {
        greeting {
            id = 'cn.coderpig.plugins.greeting'
            implementationClass = 'cn.coderpig.plugins.CpPluginPlugin'
        }
    }
}

// 插件附加信息
pluginBundle {
    website = 'https://github.com/coderpig/cpplugin'
    vcsUrl = 'https://github.com/coderpig/cpplugin'
    description = 'cpplugin gradle plugin'//插件描述
    tags = ['cp'//搜索关键词
    plugins {
        greeting {
            // id会从插件java-gradle-plugin处自动获取
            displayName = 'cpplugin gradle plugin'
        }
    }
}

配置完Sync Now重新构建下项目,在Gradle窗口就会多出一个plugin portal目录,点击publishPlugins即可发布插件到Gradle:



发布成功后需要等待官方审核,审核通过的话就可以在官方搜索到自己的插件了,当然我这种乱写和信息乱填的Demo肯定是过不了审的,2333,只是演示下流程~


③ JitPack仓库


Gradle Plugin仓库只适合Gradle插件发布,日常用第三方库可不支持,顺带提提另外两个方案,先说说JitPack

https://jitpack.io/


基于Github仓库的发布仓库,发布方式也不复杂,照着官方用户指南走:使用 Maven Publish 插件
https://developer.android.com/studio/build/maven-publish-plugin


在你的库的build.gradle文件中增加对应配置信息,接着push到Github上,点击Releases面板 →create new release,依次输入版本号、标题和描述,然后点击Publish release即可。


接着回到JitPack,定位到自己写的库,然后四个tab,选中Releases → 对应版本点下Get it,接着静待片刻:



这里的红色代表编译失败,失败的原因是这个库没有添加相关配置,正常编译通过是绿色的,然后下方可以看到如何在项目中依赖这个库,Tag替换成对应版本,比如这里的1.0.2 :



④ mavenCentral仓库


上传库到MavenCentral前需要注册登录:Sonatype

https://issues.sonatype.org/secure/Signup%21default.jspa


进入网页后点击Sign up进入注册页面注册:



拥有Sonatype账号后,点开管理后台

https://s01.oss.sonatype.org/


Log In下,会弹下述错误提示框:



需要申请一波Sonatype上传权限,回到https://issues.sonatype.org/页,点击新建,填写项目信息:



提交完等审核吧,一般会让你证明域名真的是你自己的:



第一个种解决方式最简单,域名DNS坐下解析,添加一个TXT类型的记录即可,如:



接着在官网那里回复下他这个评论,又是静待审核,然后是Gradle的配置,GPG签名等,更多具体详细内容可以参考:Android库发布至MavenCentral流程详解

https://juejin.cn/post/6953598441817636900


3常用API补漏


创建配置dsl,先定义dsl结构,定义里面的属性,然后在plugin apply方法中添加以下:


TestExtension extension = project.getExtensions().create("testExt", TestExtension)
project.extensions.add("testExt", TestExtension)

project.task("TestTask") {
    doLast {
        //2.获取外界配置的 TestExtension
        TestExtension extension = project.testExt
        //3.输出插件扩展属性
        println ">>>>>>" + extension.message
    }
}

testExt {
    //给插件扩展的属性赋值
    message  "helloworld"
}


4插件源码探索——美团渠道包生成插件Walle


没有啥idea,强行写个没啥用的插件没啥意思,刚好群里有人谈到了打包插件:valle

https://github.com/Meituan-Dianping/walle


直接看人家插件是怎么实现的~



比起Android自带打包要快上许多,在美团技术团队的博客上有介绍这个工具的大概实现原理:新一代开源Android渠道包生成工具Walle

https://tech.meituan.com/2017/01/13/android-apk-v2-signature-scheme.html


简单说说,等下慢慢跟一波源码,先是v2签名前后的APK包差异:



多了个APK Signing Block区块,除它之外其它三个区块都是受保护的,签名后对这些区块的修改都逃不过应用签名方案的检查。美团的打包插件就是从APK Signing Block区块入手的,区块2格式描述如下:



然后就是从ID-value入手的,v2签名信息是以ID(0x7109871a)的ID-value来保存在这个区块中,Android系统对于其它的ID-value选择忽略,打包插件就是定义了自定义的ID-value把渠道信息写入到这个区域,App运行时读取渠道信息,再去完成特定渠道初始化。整套插件主要由四个部分组成:


① 用于写入ID-value信息的Java类库;


② Gradle构建插件用来和Android的打包流程进行结合 ;


③ 用于读取ID-value信息的Java类库;


④ 用于供 com.android.application 使用的读取渠道信息的AAR;


行吧,大概原理就了解到这里,接着跟一波插件具体的实现源码,直接定位到插件配置文件:



打开看下接口实现类是哪个:



1. GradlePlugin


定位到GradlePlugin.kotlin,逻辑不算复杂:



定位到Extension.kotlin,就是DSL传递的参数:



对应文档这里:



看完applyExtension()接着看看applyTask()



跟下ChannelMakerTask类~


2. ChannelMaker


集成DefaultTask@TaskAction注解标识Task本身要执行的方法:



然后判断。


根据下述几种情况调用对应生成渠道APK的方法:


  • PROPERTY_CHANNEL_LIST  ↓

    channelList.each {generateChannelApk(...) }


  • PROPERTY_CONFIG_FILE  ↓

    generateChannelApkByConfigFile(...)


  • PROPERTY_CHANNEL_FILE  ↓

    generateChannelApkByChannelFile(...)


  • configFile instanceof File  ↓

    generateChannelApkByConfigFile(...)


  • channelFile instanceof File  ↓

    generateChannelApkByChannelFile(...)


  • variantConfigFileName != null

    && variantConfigFileName.length() > 0 ↓

    generateChannelApkByConfigFile(...)


上述方法各自有不同的处理,但最终调用的都是:generateChannelApk



呕吼,接着看看ChannelWriter是怎么写入渠道信息的~

3. ChannelWriter


跟下put()方法,最后调用的都是:



跟下putRaw()方法:



定位下APK_CHANNEL_BLOCK_ID



哈,这个插件把渠道信息写到APK Signing Block里的 ID,跟下PayloadWriter.put()


4. PayloadWriter



一步步跟,跟到putAll,看上面的代码似乎很复杂的样子?其实不然先是ApkSigningBlockHandler回调接口,定义了一个handle方法:



传入了一个originIdValues,其实就是apk本身带的ID-value,然后遍历新的idValues,写入其中,最后通过addPayload()将数据塞到ApkSigningBlock实例中返回。



接着是v3签名的一些处理,然后把渠道信息写到apk里,这里就真的是技术活,太强了!!!


byte级别精细化的文件操作,这功底...我的确是个假安卓,跟到ApkSigningBlock → writeApkSigningBlock()



就是对应上表,把区块2的内容写回apk中:



把渠道包信息打入到apk的大概流程就这样,任务的执行时机也在assemble后。


写弄懂了,接着看下读,官方文档中写道:



APP还要另外依赖这个aar,在运行时读取对应的渠道信息。



跟下WalleChannelReader,在项目的library目录下:



5. WalleChannelReader



传入comtext,获取apk的路径,接着传入ChannelReader.get()


6. ChannelReader



跟下getMap()



跟下PayloadReader.getString()


7. PayloadReader


getString() → get() → getAll()



RandomAccessFile.getChannel() 获得文件通道对象,然后传入ApkUtil.findApkSigningBlock()



就是找到APK Signing Block区块,返回ByteBuffer实例,然后ApkUtil.findIdValues(apkSigningBlock2)获取Id-Values们,此时再回到ChannelReader → get()处,就懂了吧。


这是调用getChannel()的实现,如果根据key获取则是走getChannelInfoMap(),流程比较相似,就不再复述了。


以上就是此插件实现的完整讲解,当然核心难点Byte级别的文件操作,后面解完apk构建过程再去研究研究~


参考文献

你确定会在 JitPack 上创建依赖库吗?
https://juejin.cn/post/6946377616400220191




最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读

JakeWharton:Android 为支持这些特性,做了哪些努力?
玩转Gradle ,动不动就Build Running,这玩意到底在干嘛?
新技术又又又又又又叒来了?


点击 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存