文章

04.Gradle三方插件

04.Gradle三方插件

好用的 Gradle Task

Gradle Task 之获取 xml 中的所有 view

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
import groovy.util.slurpersupport.GPathResult
import groovy.util.slurpersupport.NodeChild
import groovy.util.slurpersupport.Node

/**
 * 收集App中自定义view
 */
project.afterEvaluate {
    println("project.afterEvaluate : ${this.name}, android:${this.android}, applicationVariants:${this.android.applicationVariants}")
    this.android.applicationVariants.all { variant ->
        def buildType = variant.buildType.name.capitalize()
        def mergeResourcesTask = project.tasks.findByName("merge${buildType}Resources")
        println("=========project.afterEvaluate find task buildType=${buildType},$mergeResourcesTask: $mergeResourcesTask")
        if (mergeResourcesTask != null) {
            def resParseTask = project.tasks.create("resParse${buildType}Task", ResParseTask.class)
            resParseTask.buildType = buildType
            mergeResourcesTask.finalizedBy(resParseTask)
        }
    }
}

class ResParseTask extends DefaultTask {

    String buildType
    File viewNameListFile

    HashSet<String> viewSet = new HashSet<>()
    // 自己根据输出几个添加
    List<String> ignoreViewNameList = Arrays.asList("include", "fragment", "merge", "view", "DateTimeView")

    ResParseTask() {
        setGroup("resParse")
        setDescription("收集App中xml中的所有View")
    }


    @TaskAction
    void doTask() {
        File distDir = new File(project.buildDir, "tmp_custom_views")
        if (!distDir.exists()) {
            distDir.mkdirs()
        }
        viewNameListFile = new File(distDir, "custom_view_final.txt")
        if (viewNameListFile.exists()) {
            viewNameListFile.delete()
        }
        viewNameListFile.createNewFile()
        viewSet.clear()
        viewSet.addAll(ignoreViewNameList)

        try {
            File resMergeFile = new File(project.buildDir, "/intermediates/incremental/merge${buildType}Resources/merger.xml")
            println("------>>>>>> resMergeFile: ${resMergeFile.getAbsolutePath()} === ${resMergeFile.exists()}")
            if (!resMergeFile.exists()) {
                println("------>>>>>> resMergeFile not exists: ${resMergeFile.getAbsolutePath()} === ${resMergeFile.exists()}")
                return
            }
            XmlSlurper slurper = new XmlSlurper()
            GPathResult result = slurper.parse(resMergeFile)
            if (result.children() != null) {
                println("------>>>>>> resMergeFile parse: ${resMergeFile.getAbsolutePath()}, children=${result.childNodes().size()}")
                result.childNodes().forEachRemaining({ o ->
                    println("------>>>>>> parseNode: ${o.name()}, ${o.getClass().getCanonicalName()} ${o instanceof NodeChild}")
                    if (o instanceof Node) {
                        parseNode(o)
                    }
                })
            }
        } catch (Throwable e) {
            e.printStackTrace()
        }
    }

    void parseNode(Node node) {
        if (node == null) {
            return
        }
        println("------>>>>>> parseNode, name=${node.name()}, type=${node.attributes.get("type")}, path=${node.attributes.get("path")}")
        if (node.name() == "file" && node.attributes.get("type") == "layout") {
            String layoutPath = node.attributes.get("path")
            println("------>>>>>> parseNode: $layoutPath")
            try {
                XmlSlurper slurper = new XmlSlurper()
                GPathResult result = slurper.parse(layoutPath)

                String viewName = result.name()
                println("------>>>>>> parseNode viewName=$viewName")
                if (viewSet.add(viewName)) {
                    viewNameListFile.append("${viewName}\n")
                }
                if (result.children() != null) {
                    result.childNodes().forEachRemaining({ o ->
                        if (o instanceof Node) {
                            parseLayoutNode(o)
                        }
                    })
                }
            } catch (Throwable e) {
                e.printStackTrace()
            }

        } else {
            node.childNodes().forEachRemaining({ o ->
                if (o instanceof Node) {
                    parseNode(o)
                }
            })
        }

    }

    void parseLayoutNode(Node node) {
        if (node == null) {
            return
        }
        String viewName = node.name()
        println("------>>>>>> parseLayoutNode viewName=$viewName")
        if (viewSet.add(viewName)) {
            viewNameListFile.append("${viewName}\n")
        }
        if (node.childNodes().size() <= 0) {
            return
        }
        node.childNodes().forEachRemaining({ o ->
            if (o instanceof Node) {
                parseLayoutNode(o)
            }
        })
    }

}

在 app/build.gradle 中引用

1
apply from: "$rootDir/config/gradle/view_opt.gradle"

官方插件

JVM Plugins

The Java Library Plugin

https://docs.gradle.org/nightly/userguide/java_library_plugin.html#java_library_plugin

The Groovy Plugin

https://docs.gradle.org/nightly/userguide/groovy_plugin.html

java-gradle-plugin

java-gradle-plugin 介绍

使用 java-gradle-plugin 插件发布 jar 省略写 resource/META-INF

官方文档:

Gradle Plugin Development Plugin

之前的写法:

  1. 创建 main/resources/META-INF/gradle-plugins 文件夹,并在该文件夹下新建 first-plugin.properties 文件 (这里的 first-plugin 就是插件的 id),
  2. first-plugin.properties 文件中声明实现插件的类 implementation-class=me.hacket.plugin.FirstPlugin

java-gradle-plugin 插件可用来帮助我们开发 Gradle 插件

  1. Gradle 6.4 后自动 apply 了 [[04.Gradle三方插件#The Java Library Plugin]]java-library 插件
  2. 自动在在 api configuration 添加了 gradleApi() 依赖
  3. jar task 执行时,校验 plugin metadata 信息
  4. 集成了 TestKit,可用来测试 plugin 代码;对于 ` testImplementation ` 自动添加了 gradleTestKit() 依赖;通过 GradleRunner 自动生成了 plugin classpath,具体可见 Automatic classpath injection with the Plugin Development Plugin

引入

kotlin 引入:

1
2
3
4
// build.gradle.kts
plugins {
    `java-gradle-plugin`
}

groovy 引入:

1
2
3
4
// build.gradle
plugins {
    id 'java-gradle-plugin'
}

作用

作用一:可用来替代需要写 resource/META-INF

  • groovy
1
2
3
4
5
6
7
8
gradlePlugin {
    plugins {
        simplePlugin {
            id = 'first-plugin'
            implementationClass = 'me.hacket.plugin.FirstPlugin'
        }
    }
}
  • kotlin
1
2
3
4
5
6
7
8
gradlePlugin {
    plugins {
        create("simplePlugin") {
            id = "org.gradle.sample.simple-plugin"
            implementationClass = "org.gradle.sample.SimplePlugin"
        }
    }
}
  • 之前的写法
    创建 main/resources/META-INF/gradle-plugins 文件夹,并在该文件夹下新建 first-plugin.properties 文件 (这里的 first-plugin 就是插件的 id),在该文件中声明实现插件的类 implementation-class=me.hacket.plugin.FirstPlugin

可在 gradlePlugin{}block 中定义多个插件

作用二:为每个 plugin 生成 Plugin Marker Artifact 信息 Plugin Marker Artifact 用来标记插件库的真实路径,用于 plugins 找到插件库用的

作用三:Publish each plugin to the Gradle Plugin Portal (see Publishing Plugins to Gradle Plugin Portal for details), but only if the Plugin Publishing Plugin has also been applied.

Plugin Marker Artifacts

自从可以通过 plugins{}DSL 来全局声明插件的 idversion 属性,Gradle 需要一种方式来找到该插件的实现 artifact(简单说就是需要一种方法把 plugins 方式声明的和已有的 GAV 映射起来)。

Gradle 会寻找 Plugin Marker Artifact 找到 marker:plugin.id:plugin.id.gradle.plugin:plugin.version,该 marker 有具体的依赖实现,该 marker 的 publish 可通过自动 java-gradle-plugin 发布

和其他插件的配合

maven-publish

java-gradle-plugin 检测到 maven-publish 插件,会自动配置 MavenPublications:

1
2
3
4
5
6
7
publishing {
  publications {
    myPublicationName(MavenPublication) {
      // Configure the publication here
    }
  }
}

默认添加以下属性:

  • groupId - project.group
  • artifactId - project.name
  • version - project.version

一般自定义最好。

### Plugin Publish Plugin

Starting from version 1.0.0, the Plugin Publish Plugin always auto-applies the Java Gradle Plugin (java-gradle-plugin) and the Maven Publish Plugin (maven-publish).

三方插件

maven 发布相关插件

[[Gradle 和 Maven]]

Java Gradle 插件

Java Gradle 插件的使用

1
2
// java 是Java Gradle插件的plugin id
apply plugin:'java'

使用 Java 插件之后会为当前工程添加默认设置和约定,如源代码的位置、单元测试代码的位置、资源文件的位置等,一般使用默认设置即可。

Java 插件约定的项目结构

Java 插件设置一些默认的设置和约定,下面来看一看 Java 项目的默认工程目录,目录结构基本如下:

1
2
3
4
5
6
7
8
JavaGradle
└─src
    ├─main
    │  ├─java
    │  └─resources
    └─test
        ├─java
        └─resources
  • src/main/java 默认是源代码存放目录
  • src/main/resources 是资源文件、配置文件等目录
  • src/test 下面的目录当然是与其相对应的单元测试相关文件的存储目录
  • main 和 test 是 Java Gradle 插件内置的两个源代码集合,当然除此之外可以自己定义其他源代码集合
1
2
3
4
5
6
7
apply plugin : 'java'
sourceSets{
    // 指定新的源代码集合
    vip{
        // ...
    }
}

上述目录结构都是 Java Gradle 插件默认实现的,当然还可以修改具体目录的位置,配置方式如下:

1
2
3
4
5
6
7
8
9
10
11
sourceSets {
    // 修改默认目录,下面还是和默认位置一样,如需配置其他目录修改即可
    main {
        java {
            srcDir 'src/java'
        }
        resources {
            srcDir 'src/resources'
        }
    }
}

SourceSet 源集

SourceSet 是 Java Gradle 插件用来描述和管理源代码及其资源的一个抽象概念,是一个 Java 源代码文件和资源文件的集合。可以通过源代码集合配置源代码文件的位置、设置源代码集合的属性等,源集可以针对不同的业务将源代码分组管理,如 Java Gradle 插件默认提供的 main 和 test 源代码目录,一个用于业务代码,另一个用于单元测试,非常方便。

Java Gradle 插件在 Project 下提供一个 sourceSet 属性以及 sourceSet{} 闭包来访问和配置源集,sourceSet 是一个 SourceSetContainer, 源集的常用属性如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 比如main、test等表示源集的名称
name(String)

// 表示源集编译后的class文件目录
output.classDir(File)

// 表示编译后生成的资源文件目录
output.resourcesDir(File)

// 表示编译后源集所需的classpath
compileClasspath(FileCollection)

// 表示该源集的Java源文件
java(SourceDirectorySet)

// 表示该源集的Java源文件所在目录
java.srcDirs(Set)

// 表示源集的资源文件
resources(SourceDirectorySet)

// 表示该源集的资源文件所在目录
resources.srcDirs(Set)

Java 插件常用 Task

Task 名称(默认源集通用任务)类型描述
compileJavaJavaCompile表示使用 javac 编译 java 源文件
processResourcesCopy表示把资源文件复制到生成的资源文件目录中
classesTask表示组装产生的类和资源文件目录
compileTestJavaJavaCompile表示使用 javac 编译测试 java 源文件
processTestResourcesCopy表示把资源文件复制到生成的资源文件目录中
testClassesTask表示组装产生的测试类和相关资源文件
jarJar表示组装 jar 文件
javadocJavadoc表示使用 javadoc 生成 Java API 文档
uploadArchivesUpload表示上传包含 Jar 的构建,使用 archives{}闭包进行配置
cleanDelete表示清理构建生成的目录文件
cleanTaskNameDelete表示删除指定任务生成的文件,如 cleanJar 是删除 jar 任务生成的文件
Task 名称(自定义源集任务,SourceSet 是具体的源集名称)类型描述
compileSourceSetJavaJavaCompile表示使用 javac 编译指定源集的源代码
processSouceSetResourcesCopy表示把指定源集的资源文件复制到生成文件中的资源目录中
sourcesSetClassesTask表示组装给定源集的类和资源文件目录

Gradle 插件之蒲公英 pgyer

蒲公英应用上传插件
https://github.com/dodocat/pgyer

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
apply plugin: 'org.quanqi.pgyer'

// ==================== 蒲公英 ====================

pgyer {
    _api_key = "70885395bcdfd5ebdb72a5856c95596c"
    uKey = "d670780e218b698082c7dd096078659c"
}

def findReleaseApk() {
    String uploadFilePath
    File file = file("${rootDir}/build/apk/release")
    if (file.exists()) {
        file.eachFileMatch(~/.*release.apk/) { it ->
            uploadFilePath = it.absolutePath
        }
    }
    return uploadFilePath
}

def findDebugApk() {
    String uploadFilePath
    File file = file("${rootDir}/build/apk/debug")
    if (file.exists()) {
        file.eachFileMatch(~/.*debug.apk/) { it ->
            uploadFilePath = it.absolutePath
        }
    }
    return uploadFilePath
}

apks {
    release {
//        sourceFile = file(findReleaseApk())
        sourceFile = file("${findDebugApk()}")
        println("开始上传apk到蒲公英:" + findDebugApk())
    }
}

Gradle 插件 webp 转换

https://zhuanlan.zhihu.com/p/307222777
https://github.com/smallSohoSolo/McImage

Gradle 插件之 walle

什么是 walle?

https://github.com/Meituan-Dianping/walle
Walle 是 Android Signature V2 Scheme 签名下的新一代渠道包打包神器:通过在 Apk 中的 APK Signature Block 区块添加自定义的渠道信息来生成渠道包,从而提高了渠道包生成效率,可以作为单机工具来使用,也可以部署在 HTTP 服务器上来实时处理渠道包 Apk 的升级网络请求。

使用

配置依赖

在位于项目的根目录 build.gradle 文件中添加 Walle Gradle 插件的依赖, 如下:

1
2
3
4
5
6
root build.gradle
buildscript {
    dependencies {
        classpath 'com.meituan.android.walle:plugin:1.1.7'
    }
}

引入 walle 插件

1
2
3
4
5
6
// app build.gradle
apply plugin: 'walle'

dependencies {
    compile 'com.meituan.android.walle:library:1.1.7'
}

配置 walle 插件

1
2
3
4
5
6
7
8
walle {
    // 指定渠道包的输出路径
    apkOutputFolder = new File("${project.buildDir}/outputs/channels");
    // 定制渠道包的APK的文件名称
    apkFileNameFormat = '${appName}-${packageName}-${channel}-${buildType}-v${versionName}-${versionCode}-${buildTime}.apk';
    // 渠道配置文件
    channelFile = new File("${project.getProjectDir()}/channel")
}
  1. apkOutputFolder 指定渠道包的输出路径, 默认值为 new File("${project.buildDir}/outputs/apk")
  2. apkFileNameFormat:定制渠道包的 APK 的文件名称, 默认值为 ‘appName-{buildType}-${channel}.apk
    可使用以下变量:
1
2
3
4
5
6
7
8
9
10
 projectName - 项目名字
 appName - App模块名字
 packageName - applicationId (App包名packageName)
 buildType - buildType (release/debug等)
 channel - channel名称 (对应渠道打包中的渠道名字)
 versionName - versionName (显示用的版本号)
 versionCode - versionCode (内部版本号)
 buildTime - buildTime (编译构建日期时间)
 fileSHA1 - fileSHA1 (最终APK文件的SHA1哈希值)
 flavorName - 编译构建 productFlavors 名
  1. channelFile :包含渠道配置信息的文件路径。具体内容格式详见:github 渠道配置文件示例,支持使用#号添加注释

如何获取渠道信息

1
String channel = WalleChannelReader.getChannel(this.getApplicationContext());

如何生成渠道包

生成渠道包的方式是和 assemble${variantName}Channels 指令结合,渠道包的生成目录默认存放在 build/outputs/apk/,也可以通过 walle 闭包中的 apkOutputFolder 参数来指定输出目录

1
2
3
4
// 生成渠道包 
./gradlew clean assembleReleaseChannels
// 支持 productFlavors 
./gradlew clean assembleMeituanReleaseChannels

walle 原理

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

开源 Gradle 插件

TinyPngPlugin

https://github.com/waynell/TinyPngPlugin
TinyPng gradle plugin for android

ApiInspect

https://github.com/Sunzxyong/ApiInspect
An api compatibility inspect gradle plugin.(一个 Api 兼容性检测的 Gradle 插件)

ScratchPaper

https://github.com/2BAB/ScratchPaper
ScatchPaper 可以在你的 App icon 上加一个蒙层用以区分出各个 BuildType 的 App,并且承载了版本信息等附加文字。

  1. 支持 常规 和 圆形 的图标
  2. 支持 adaptive-icon
  3. 支持 AAPT2

类似项目:
https://github.com/akonior/icon-version

AabResGuard

https://github.com/bytedance/AabResGuard
About
The tool of obfuscated aab resources.(Android app bundle 资源混淆工具)

McImage

Android Gradle Plugin – Auto Check big image and compress image in building.
https://github.com/smallSohoSolo/McImage

android-snapshot-publisher

https://github.com/xmartlabs/android-snapshot-publisher
Gradle plugin to deploy Android Snapshot Versions

AndroidLocalizePlugin

https://github.com/Airsaid/AndroidLocalizePlugin
Android/IDEA 本地化插件。 支持多种语言,无需申请 KEY。

1
2
3
4
支持 104 种语言。
无需申请 KEY。
一键生成所有翻译文件。
支持对指定的文本不参与翻译。
本文由作者按照 CC BY 4.0 进行授权