要了解变体的作用,必须首先了解 flavor、dimension、variant 这三者之间的关系,AGP3.X版本以后,每一个 falvor 必须对应一个 dimension,然后不同 dimension 里的 flavor 会组合成一个 variant
// app\build.gradle
android{
flavorDimensions "size","color"
productFlavors{
monk{
dimension "size"
}
small{
dimension "size"
}
blue{
dimension "color"
}
}
}
在Android对 Gradle 插件的扩展支持中,其中最常见的便是利用 variant 来对构建过程中各默认 task 进行 hook,关于 variants的类型,一共可分为3种类型
这里我们以applicationVariants来举个例子
// app\build.gradle
android{
productFlavors{
flavorA{}
flavorB{}
flavorC{}
}
// 配置阶段之后获取所有variant 的 name 和 baseName
afterEvaluate {
applicationVariants.all {variant->
println "name: ${variant.name}"
println "---baseName: ${variant.baseName}"
}
}
}
/*
这样配置后,执行gradlew clean 会报错,所以可以通过 sync方式查看输出
> Configure project :app
name: flavorADebug
---baseName: flavorA-debug
name: flavorBDebug
---baseName: flavorB-debug
name: flavorCDebug
---baseName: flavorC-debug
name: flavorARelease
---baseName: flavorA-release
name: flavorBRelease
---baseName: flavorB-release
name: flavorCRelease
---baseName: flavorC-release
*/
另外一个常见的场景是利用 applicationVariants 修改输出的apk名称
// app\build.gradle
android{
applicationVariants.all { variant ->
variant.outputs.each{
outputFileName ="app-${variant.baseName}-${variant.versionName}.apk"
println outputFileName
}
}
}
/**
执行 gradlew clean
> Configure project:app
app-debug-1.0.apk
app-release-1.0.apk
*/
对 applicationVariants 中的 Task 进行 Hook
// app\build.gradle
afterEvaluate {
android.applicationVariants.all {variant->
println "checkManifest --: ${variant.checkManifest.name}"
}
}
/*
> Configure project :app
checkManifest --: checkDebugManifest
checkManifest --: checkReleaseManifest
*/
根据上述例子,我们可以利用 variants 去解决插件化开发中的痛点:编写一个对插件化项目中的各个插件自动更新的脚本,示例代码如下
// app\build.gradle
afterEvaluate {
android.applicationVariants.all {variant->
def checkManifest = variant.checkManifest
checkManifest.doFirst{
def bt = variant.buildType.name
if(bt == 'release' || bt == 'preview'){
//...
}
}
}
}
大家都知道,Google 在 AGP1.5 以后提供了 transform API,允许 plugin 在打包成 .dex 文件之前的编译过程中操作 .class 文件,我们需要做的就是利用该机制修改 class文件方法替换原文件即可;即: Gradle Transform 的功能就是把输入的 .class 文件转换为目标字节码文件
实现步骤
配置 Android DSL
plugins {
id 'java-library'
id 'groovy' // groovy 语言
id 'java-gradle-plugin'
}
// 由于 buildSrc 执行时机是最早的,因此需要首先添加库
repositories {
google()
mavenCentral()
}
sourceSets {
main{
groovy{
srcDir 'src/main/groovy'
}
resources {
srcDir 'src/main/resource'
}
}
}
dependencies {
implementation localGroovy()
implementation gradleApi()
// Android DSL
implementation "com.android.tools.build:gradle:7.0.2"
// ASM V9.1
implementation 'org.ow2.asm:asm:9.1'
implementation 'org.ow2.asm:asm-commons:9.1'
创建 Transform 的子类
// transfomr 虽然已经过时,但过往的插件化方案还是比较成熟,可以作为借鉴
class MyTransform extends Transform {
/**
* 一、重写getName:返回对于的 Task 名称
* 每个 Transform 都有一个与之对应的 Transform task,这里返回的是 taskName
* app\build\intermediates\transforms下
*/
@Override
String getName() {
return "MyTransform"
}
/**
* 二、重写 getInputTypes:确定对那些类型的结果进行转换
* 确定需要对那些类型结果进行转换,比如字节码、资源文件
* ContentType类型有6种,前2用的较为频繁
* 1. CONTENT_CLASS:表示需要处理的.class文件
* 2. CONTENT_JARS:表示小的 class 和资源文件
* 3. CONTENT_RESOURCES:表示需要处理 java 的资源文件
* 4. CONTENT_NATIVE_LIBS:表示需要处理 native 库的代码
* 5. CONTENT_DEX:表示需要处理 DEX 文件
* 6. CONTENT_DEX_WITH_RESOURCES:表示需要处理 DEX 与 java 的资源文件
*/
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
/**
* 三、重写 getScopes:指定插件的适用范围
* 制定插件的试用范围
* Scope类型有5种
* 1. PROJECT:只有项目内容
* 2. SUB_PROJECTS:只有子项目
* 3. EXTERNAL_LIBRARIES:只有外部库
* 4. TESTED_CODE:由当前变体(包括依赖项)所测试的代码
* 5. PROVIDED_ONLY:只提供本地或远程依赖项
* SCOPE_FULL_PROJECT 是一个 Scope 集合,包含 Scope.PROJECT,Scope.SUB_PROJECTS, Scope.EXTERNAL_LIBRARIES 这三项,
* 即当前 Transform 的作用域包括当前项目、子项目以及外部的依赖库
*/
@Override
Set<? super QualifiedContent.Scope> getScopes() {
// 适用范围:通常是指定整个 project,也可以指定其它范围
return TransformManager.SCOPE_FULL_PROJECT
}
/**
* 四、重写 isIncremental:表示是否支持增量更新
* 是否支持增量更新
* true:transformInput会返回一份修改的文件列表
* false:全量编译,并删除上次的输出内容
*/
@Override
boolean isIncremental() {
return false
}
/**
* 五、重写 transform :进行具体的转换过程
* 下面以一个标准的 Transfrom + ASM 修改字节码的模板代码举个例子
*/
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
println '转换开始........................'
def start = System.currentTimeMillis()
def inputs = transformInvocation.inputs
def outputProvider = transformInvocation.outputProvider
if (outputProvider != null)
outputProvider.deleteAll()
// transform 的 inputs 有两种类型:一种是目录,一种是jar包
inputs.each { input ->
// 本地 project 编译成的多个 class 文件存放的目录
input.directoryInputs.each { dir ->
handleDirectory(dir,outputProvider)
}
// 各依赖锁编译成的 jar 文件
input.jarInputs.each { jar ->
handleJar(jar, outputProvider)
}
}
def cost = (System.currentTimeMillis() - start) / 1000
println "转换结束............................:$cost"
}
def static EXPAND_FRAMES = 0x100
// transform 通用模板代码
static void handleDirectory(DirectoryInput input, TransformOutputProvider provider){
// 在增量模式下可以通过 provider.changedFiles 方法获取修改的文件
// provider.changedFiles
if (input.file.size() == 0)return
if (input.file.isDirectory()) {
//遍历以某一扩展名结尾的文件
input.file.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { classFile ->
def name = classFile.name
if (checkClassFile(name)) {
println '----------- deal with "class" file <' + name + '> -----------'
def classReader = new ClassReader(classFile.bytes)
def classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
// todo Transform + ASM 后补
def classVisitor = new MyCustomClassVisitor(classWriter)
classReader.accept(classVisitor, EXPAND_FRAMES)
def codeBytes = classWriter.toByteArray()
def fileOutputStream = new FileOutputStream(classFile.parentFile.absolutePath + File.separator + name)
fileOutputStream.write(codeBytes)
fileOutputStream.close()
}
}
}
// 获取 output 目录 dest:.\app\build\intermediates\transforms\hencoderTransform\
def destFile = provider.getContentLocation(input.name,input.contentTypes,input.scopes,Format.DIRECTORY)
// 将 input 的目录复制到 output 指定目录
FileUtils.copyDirectory(input.file, destFile)
}
// transform 通用模板代码
static void handleJar(JarInput input, TransformOutputProvider provider) {
if (input.file.absolutePath.endsWith(".jar")) {
// 截取文件路径的 md5 值重命名输出文件,避免同名导致覆盖的情况出现
def jarName = input.name
def md5Name = DigestUtils.md5Hex(input.file.absolutePath)
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
def jarFile = new JarFile(input.file)
def enumeration = jarFile.entries()
def tmpFile = new File(input.file.parent+File.separator+"class_temp.jar")
if (tmpFile.exists()) {
tmpFile.delete()
}
def jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
while (enumeration.hasMoreElements()) {
def jarEntry = enumeration.nextElement()
def entryName = jarEntry.name
def zipEntry = new ZipEntry(entryName)
def inputStream = jarFile.getInputStream(zipEntry)
if (checkClassFile(entryName)) {
println '----------- deal with "jar" class file <' + entryName + '> -----------'
// 使用 ASM 对 class 文件进行操控
jarOutputStream.putNextEntry(zipEntry)
def classReader = new ClassReader(IOUtils.toByteArray(inputStream))
def classWriter = new ClassWriter(classReader, org.objectweb.asm.ClassWriter.COMPUTE_MAXS)
// todo Transform + ASM 后补
def cv = new MyCustomClassVisitor(classWriter)
classReader.accept(cv, EXPAND_FRAMES)
def code = classWriter.toByteArray()
jarOutputStream.write(code)
}else {
jarOutputStream.putNextEntry(zipEntry)
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
jarOutputStream.closeEntry()
}
jarOutputStream.closeEntry()
jarFile.close()
// 生成输出路径 dest:./app/build/intermediates/transforms/xxxTransform/...
def dest = provider.getContentLocation(jarName + md5Name,input.contentTypes, input.scopes, Format.JAR)
// 将 input 的目录复制到 output 指定目录
FileUtils.copyFile(tmpFile, dest)
tmpFile.delete()
}
}
// 检查 class 文件是否需要处理
static boolean checkClassFile(String name){
return (name.endsWith(".class") && !name.startsWith("R\$")
&& "R.class" != name && "BuildConfig.class" != name
&& "android/support/v4/app/FragmentActivity.class" == name)
}
}
以上例子说明了 Transform 的使用方法,还需要借用 ASM 的相应 API去加载相应的 .class 文件,并解析,找到满足特定条件的 .class 文件和相关方法,最后去修改相应方法实现动态插入相应的字节码
发布插件分为两种形式
发布插件到本地仓库
首先引入 maven 插件,进而在 publishing 中添加上传的仓库地址的相关配置,这样 Gradle 在执行 publishing时将生成和上传 pop.xml 文件,示例代码如下
apply plugin: 'maven-publish'
publishing {
repositories {
maven {
url '../repo'
}
}
}
afterEvaluate {
println '发布了。。。。。。。。。。。。。。。。。。。。'
publishing{
publications{
debug(MavenPublication){
from components.java
groupId 'com.monk.customplugin'
artifactId 'custom-gradle-plugin'
version '0.1.0'
description 'this is a liberary '
}
}
}
}
发布插件到远程仓库,操作方式同本地发布一样,仅仅将 url 替换成远程 maven 参考的 url 即可