gradle脚本实战

项目推荐

依赖管理

模块管理

gradle多模块管理是在settings.gradle中配置的,下例演示了在工程中引入任意路径模块代码的方法:

include ':lib'
project(':lib').projectDir = new File('xx/xx/xx/lib')

仓库管理

buildscript方法的作用是配置脚本的依赖,指定仓库也要在这里进行:

// org.gradle.api.Project#buildscript(Closure<ScriptHandler> closure)
buildscript {
    // org.gradle.api.initialization.dsl.ScriptHandler#repositories(Closure<RepositoryHandler> closure)
    repositories {
        // org.gradle.api.artifacts.dsl.RepositoryHandler#mavenLocal()
        mavenLocal()  /* 本地路径:$$USER_HOME/.m2 */
        // org.gradle.api.artifacts.dsl.RepositoryHandler#maven(groovy.lang.Closure)
        maven { url 'file://D:\\your_repo'}  /* Local repo with custom path */
        maven {  /* Remote repo with auth */
            url 'http://your.repo.com/artifactory/maven-down/'
            credentials {
                username "username"
                password "password"
            }
        }
		maven { url 'https://dl.bintray.com/umsdk/release' }  /* Remote repo without auth */
        // org.gradle.api.artifacts.dsl.RepositoryHandler#google()
        google()  // Remote repos
        jcenter()  // Remote repos
        mavenCentral() // Remote repos
        // org.gradle.api.artifacts.dsl.RepositoryHandler#flatDir(Closure closure)
		flatDir { // Include local libs dir
            dirs 'libs', project(':app').file('libs').path
        }

        // Print and operate with all repos
        all { ArtifactRepository repo ->
            if(repo instanceof MavenArtifactRepository){
                def url = repo.url.toString()
                if (url.startsWith('https://repo1.maven.org/maven2') || url.startsWith('https://jcenter.bintray.com/')) {
                    project.logger.lifecycle "Repository ${repo.url} replaced by $REPOSITORY_URL."
                    remove repo
                }
            }
        }


    }
    // View all repos.
    repositories.each {
        println it.getUrl()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:1.2.3'
    }
}

很多时候,我们需要对整个项目仓库进行全局管理,比如查看每个project的仓库以及删除一些不可访问的仓库避免build失败等,可以采用下面的方法:

// org.gradle.api.Project#allprojects(groovy.lang.Closure<Project> closure)
allprojects {
    repositories {
        .../*这里填希望使用的仓库*/
    }

    it.afterEvaluate {
        // 指定删除规则
        def rule = {
            def url = it instanceof MavenArtifactRepository ? it.url.toString() : null
            url != null &&
                    url.startsWith('https://jcenter.bintray') ||
                    url.startsWith('https://dl.google.com') ||
                    url.startsWith('https://repo.maven.apache.org')
        }
        it.rootProject.buildscript.repositories.removeIf rule
        it.repositories.removeIf rule

        // 打印处理后的最终仓库
        project.logger.lifecycle "[$it]afterEvaluateRepositories-> "+
                "\n project_repo:"+
                repositories.stream().map{"\n        - "+it.url.toString()}.collect(Collectors.joining())+
                "\n buildscript_repo:"+
                it.rootProject.buildscript.repositories.stream().map{"\n        - "+it.url.toString()}.collect(Collectors.joining())
    }

    ...
}

版本管理

参考资料:ResolutionStrategy

allprojects {
    configurations.all {
        // 剔除指定依赖
        exclude group: 'com.umeng.umsdk',module: 'utdid'
        exclude group:'com.belerweb',module:'pinyin4j'
        resolutionStrategy {
            // 强制指定版本
            force 'group:artifact:version'
            force 'group:artifact:version'
            // 指定SNAPSHOT库的缓存时间,0表示每次构建都下载更新
            cacheChangingModulesFor 0, 'minutes'
            // 指定动态版本号库(比如:1.0.+)的缓存时间,0表示每次构建都下载更新
            cacheDynamicVersionsFor 0, 'seconds'
            // 替换指定版本
            dependencySubstitution{
                substitute module('group:artifact') with module('targetGroup:targetArtifact:targetVersion')
            }
            eachDependency { details ->
                def group = details.requested.group
                def name = details.requested.name
                if('targetGroup'==group && 'targetName'==name){
                	// change version only.
                	details.useVersion 'targetVersion'
                	// change everything.
                	details.useTarget group: 'targetGroup', name: 'targetName', version: 'targetVersion'
                }
                
            }
        }
    }

}

依赖管理

离线包依赖

从远程制品仓库根据库文件的GAV坐标下载依赖库是最常见的,除此之外需要关注一下本地依赖的语法:

  • 依赖本地单个jar包:
implementation files('libs/foo.jar')
  • 依赖本地单个aar:
// 先把aar所在的目录添加到仓库中
android {
    repositories {
        flatDir {
            dirs 'libs'   // aar目录
        }
    }
}

// 再去依赖本地aar
implementation(name: 'foo', ext: 'aar')
  • 批量依赖本地jar和aar
implementation fileTree(include: ['*.jar','*.aar'], dir: 'libs')
  • 依赖本地module:
implementation project(':foo')

依赖传递

Gradle默认支持传递性依赖,比如当前工程依赖包A,包A依赖包B,那么当前工程会自动依赖包B。同时,Gradle支持排除和关闭依赖性传递:

implementation('com.me.test:test:1.0.0') {
    // 【方式一】排除指定规则的依赖库
    exclude group: 'com.android.support'
    // 【方式二】直接禁用,默认为true
    transitive=false
}

还有一种情况,比如你手动上传了一个aar到制品仓库,然后你应该这么引用:

// 注意这里末尾使用了@aar,如果不加此配置,那么gradle将默认以jar文件方式去下载,自然是找不到文件
implementation 'com.me.test:test:1.0.0@aar'

上面的@aar术语叫做Artifact only notation,它告诉gradle不要只下载对应文件本身,相当于依赖传递就失效了,此时如果希望执行以来传递,可以使用transitive:

implementation('com.me.test:test:1.0.0@aar') {
    transitive=true
}

应当关注implementation和compileOnly的区别。

发布配置

生成aar包

AS默认生成的aar仅支持合入本模块源码以及本地jar(不支持本地aar、远程依赖、其他本地module源码)的合入,当需要合入那些远程依赖时可以考虑使用fat–aar

除此之外,library一般只会publish其buildVariant为release的构建生成物,gradle支持指定publish哪个buildVariant:

android {
	// 指定发布flavor1Debug的生成物
    defaultPublishConfig "flavor1Debug"
    // 指定发布所有buildVariant的生成物,会在各个build文件夹中生成多个aar
    publishNonDefault true
}

基于上面一个library可以发布多种生成物的特性,对应的我们在依赖module时也可以指定需要依赖哪个buildVariant的生成物,具体用法如下:

dependencies {
    flavor1Compile project(path: ':lib1', configuration: 'flavor1Release')
    flavor2Compile project(path: ':lib1', configuration: 'flavor2Release')
}

生成jar包

有时候我们开发的module是一个纯java代码的lib,而AS默认生成的是aar,如何发布为jar包呢?可以使用下面的脚本:

makeJar.gradle:

def jarName = 'sdk-kit'

task deleteOldJar(type: Delete){
    delete "build/libs/${jarName}.jar"
}

task makeJar(type: Copy) {
    group 'your.group'
    description 'generate jar for module'
    from('build/intermediates/aar_main_jar/release')
    into('build/libs/')
    include('classes.jar')
    rename('classes.jar', "${jarName}.jar")
    doLast {
        println "Get it from: build/libs/${jarName}.jar"
    }
}

makeJar.dependsOn(deleteOldJar, build)

task sourcesJar(type: Jar){
    from android.sourceSets.main.java.srcDirs
    classifier = 'sources'
    archiveName = jarName + '-sources.jar'
}

//-encoding UTF-8  -charset UTF-8
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
    // add the following config to check
//    options.compilerArgs << "-Xlint:unchecked"
}

artifacts {
    archives sourcesJar
//    archives javadocJar
}

先在gradle中引入上述脚本:

apply from: '../makeJar.gradle'

然后执行命令:

gradlew makeJar

修改apk名称

gradle 3.0及以上:

android {
	...
    applicationVariants.all { v ->
        if (v.buildType.name == 'release') {
        	// 修改apk输出目录(老接口)
        	v.packageApplication.outputDirectory = new File(project.projectDir, "release")
        	// 修改apk输出目录(新)
        	v.packageApplicationProvider.get().outputDirectory = new File(project.projectDir, "release")
        	// 修改apk名字
            v.outputs.all {
                outputFileName = "app_V${v.versionName}+${v.versionCode}_${buildTime()}_${v.flavorName}.apk"
            }
        }
    }
}

gradle 3.0以下:

android.applicationVariants.all { variant ->
        variant.outputs.each{ output ->
            if(output.outputFile != null
                   && output.outputFile.name.endsWith('.apk')){
                def apkFile = new File(
                        output.outputFile.getParent(),
                        "SDK_${variant.flavorName}_${variant.buildType.name}_V${variant.versionName}_${buildTime()}.apk"
                )
                output.outputFile = apkFile
            }
        }
    }

备份mapping文件

android.applicationVariants.all { v ->
        if (v.buildType.name == 'release') {
            if (v.buildType.isMinifyEnabled()) {
                v.assemble.doLast{
                    copy {
                        from v.mappingFile
                        into project.projectDir
                        rename { String name->
                            "mapping_${android.defaultConfig.applicationId}_${android.defaultConfig.versionName}.txt"
                        }
                    }
                }
            }
        }
    }

备份aar

kotlin实现:

val Project.isAndroidApp: Boolean
    get() = this.plugins.hasPlugin("com.android.application")
val Project.isAndroidLib: Boolean
    get() = this.plugins.hasPlugin("com.android.library")
val Project.android: TestedExtension
    get() = this.extensions.getByName("android") as TestedExtension
val Project.androidLib: LibraryExtension
    get() = this.extensions.getByName("android") as LibraryExtension
val Project.androidApp: BaseAppModuleExtension
    get() = this.extensions.getByName("android") as BaseAppModuleExtension
    
/**
 * 1. android library only
 * 2. must be called in afterEvaluate{} block
 */
fun Project.backupLibAssembleOutputs(mappingDir: String, aarDir: String) {
    this.androidLib.libraryVariants.configureEach { v ->
        LogUtil.log( "auto backup assemble outputs: [${this.name}]${v.name}")
        val backupMapping = v.buildType.isMinifyEnabled
        v.assembleProvider.get().doLast {
            // Backup mapping files.
            if (backupMapping) {
                this.copy {
                    it.from(v.mappingFile)
                    it.into(mappingDir)
                    it.rename {
                        "mapping_${this.name}_${v.name}.txt"
                    }
                }
            }
            // Backup aar files.
            this.copy {
                it.from(v.packageLibrary.archivePath)
                it.into(aarDir)
//                it.rename { oldName ->
//                    oldName
//                }
            }
        }
    }
}
/**
 * 1. android library only
 * 2. must be called in afterEvaluate{} block
 */
fun Project.backBundleOutputs(mappingDir: String, aarDir: String){
    val backupAarAndMapping =
        this.tasks.register("backupAarAndMapping", Copy::class.java) {
            it.from("${this.buildDir.path}/outputs/mapping") { i ->
                i.into(mappingDir)
                i.include("*.txt")
            }
            it.from("${this.buildDir.path}/outputs/aar") { i ->
                i.into(aarDir)
                i.include("*.aar")
            }
        }
    this.tasks.withType(BundleAar::class.java).configureEach {
        if (it.name.endsWith("ReleaseAar")) {
            LogUtil.log(name, "auto backup bundle outputs: [${this.name}]${it.name}")
            it.finalizedBy(backupAarAndMapping)
        }
    }
}

groovy实现:

def outputDir = "${project.rootDir}/output".toString()
def mappingDir = "${project.rootDir}/mapping".toString()
p.afterEvaluate {
    if (!p.plugins.hasPlugin('com.android.library')) {
        return
    }
    p.android.libraryVariants.configureEach { v ->
        def backupMapping = v.buildType.isMinifyEnabled()
        v.assembleProvider.get().doLast {
            // Backup mapping files.
            if (backupMapping) {
                p.copy {
                    from v.mappingFile
                    into mappingDir
                    rename {
                        "mapping_${v.name}.txt"
                    }
                }
            }
            // Backup aar files.
            p.copy {
                from v.packageLibrary.archivePath
                into outputDir
                rename { name ->
                    return name
                }
            }
        }
    }


	def backupAarAndMapping =  p.tasks.register('backupAarAndMapping', Copy){
	    from "${p.buildDir.path}/outputs/mapping"
	    into mappingDir
	    include '*.txt'
	
	    from "${p.buildDir.path}/outputs/aar"
	    into outputDir
	    include '*.aar'
	}
	p.tasks.withType(com.android.build.gradle.tasks.BundleAar).configureEach{
	    if (it.name.endsWith("ReleaseAar")) {
	        it.finalizedBy backupAarAndMapping
	    }
	}
}

备份apk

kotlin实现:

/**
 * 1. android application only
 * 2. must be called in afterEvaluate{} block
 */
fun Project.backupAppAssembleOutputs(mappingDir: String, apkDir: String){
    androidApp.applicationVariants.configureEach { v ->
        val backupMapping = v.buildType.isMinifyEnabled
        v.assembleProvider.get().doLast {
            // Backup mapping files.
            if (backupMapping) {
                this.copy {
                    it.from(v.mappingFile)
                    it.into(mappingDir)
                    it.rename {
                        "mapping_${this.name}_${v.name}.txt"
                    }
                }
            }

            val apkSrcDir = "${this.buildDir}/outputs/apk/${v.flavorName}/${v.buildType.name}"
            // Backup apk files.
            copy {
                it.from(fileTree(apkSrcDir) { t->
                    t.include("*.apk")
                }.files.first())
                it.into (apkDir)
//                it.rename { name ->
//                    name
//                }
            }
        }
    }
}

groovy实现:

android.applicationVariants.configureEach { v ->
    def backupMapping = v.buildType.isMinifyEnabled()
    v.assemble.doLast {
        def outputDir = "${project.projectDir.path}/output".toString()
        def mappingDir = "${project.projectDir.path}/mapping".toString()
        // Backup mapping files.
        if (backupMapping) {
            project.copy {
                from v.mappingFile
                into mappingDir
                rename {
                    "mapping_${v.name}.txt"
                }
            }
        }

        def apkDir = "${buildDir}/outputs/apk/${v.flavorName}/${v.buildType.name}"
        // Backup apk files.
        copy {
            from fileTree(dir: apkDir, include: '*.apk').files.first()
            into outputDir
            rename { name ->
                return name
            }
        }
    }
}

发布源码

无论是java还是kotlin代码默认会被编译为字节码发布,使用下述配置发布为源码可以解决此问题:

task sourcesJar(type: Jar) {
    if (project.hasProperty("kotlin")) {
        from android.sourceSets.main.java.getSrcDirs()
    } else if (project.hasProperty("android")) {
        from android.sourceSets.main.java.sourceFiles
    } else {
        from sourceSets.main.allSource
    }
    classifier = 'sources'
}

artifacts {
    archives androidSourcesJar
}

maven插件

有时我们依赖第三方的jar或者aar并没有上传到远程仓库,如果在module使用本地依赖,那么当其他工程module也需要依赖这个jar或者aar时就很不方便甚至依赖冲突。此时,我们可以把离线的jar/aar上传到我们的私有仓库。

  • 创建upload_jar_aar.gradle
  • 同级目录下创建libs目录,将你的jar或aar文件放入
  • 根据你的jar或aar文件名以及其他maven坐标参数修改下面的脚本
  • 命令行运行:gradle uploadArchives
apply plugin: 'maven'
apply plugin: 'java-library'

sourceCompatibility = "1.8"
targetCompatibility = "1.8"

tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8'
}

def jarName = 'open_sdk_r5781'
def coreAarFile = file("./libs/${jarName}.aar")
def coreJarFile = file("./libs/${jarName}.jar")

def GROUP_ID = "your.group"
def ARTIFACT_ID = "qq-open-sdk"
group GROUP_ID
version "r5781"

artifacts {
    // 如果是aar则使用这个
//    archives coreAarFile
    // 如果是jar则使用这个
    archives file: coreJarFile, name: ARTIFACT_ID, type: 'jar'
}

uploadArchives {
    repositories {
        mavenDeployer {
        	// 你可以先用本地路径测试一下,没问题再尝试上传远程仓库
//            repository(url: uri('F:\\maven_repo'))
            repository(url: 'http://your.remote.repo') {
                authentication userName: 'name', password: 'password'
            }
            // 快照仓库地址(可选)
            snapshotRepository(url: repositorySnapshotUrl) {
                // 用户校验 用户名/密码
                authentication(userName: uname, password: pwd)
            }
            addFilter(ARTIFACT_ID) { artifact, file ->
                artifact.name == ARTIFACT_ID
            }
			
			// 配置pom
           pom.project {
               name artifactName // artifact名称
               version versionName // artifact版本
               artifactId _artifactId // artifact id
               groupId _groupId // artifact所属Group Id
               packaging packagingType // 文件格式,例如jar、aar
               description _description // 描述
           }
			// 配置pom
            pom.groupId = GROUP_ID        //默认是rootProject.name
            pom.artifactId = ARTIFACT_ID  //默认是project.name,即module的名称
            pom.version = version         //默认是project.version
            pom.packaging = 'jar'
        }
    }
}
            

maven-publish插件

用于将编译的输出物(artifacts)发布到Apache Maven仓库当中,例如aar、jar等library发布到仓库当中,我们可以通过gradle或者maven进行远程依赖使用。

参考资料:

publishing内部有两个配置publications 和repositories,示例如下:

apply plugin:'maven-publish'
publishing{
	repositories{
	}

	publications{
		libA(MavenPublication){}
		libB(MavenPublication){}
	}
}

repositories

指定需要发布的仓库,配置方法与前面仓库管理中的一致。

repositories{
		
     maven {
         name "myRepo"
         def releasesRepoUrl = "$buildDir/repos/releases"
         def snapshotsRepoUrl = "$buildDir/repos/snapshots"
         url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
     }

}

publicaions

主要包含四种类型的配置项:

  1. A component:继承自MavenPublication
  2. Custom artifacts :自定义的输出物
  3. Standard metadata :metadata信息,如groupId,version,artifactId
  4. Other contents of the POM file :maven依赖配置文件
1. 创建发布

看下详细配置:

publications{
	libA(MavenPublication){ //libA继承MavenPublication
         // 1. 配置metadata
         groupId "${project.group}"
         artifactId "${project.name}"
         version "${project.version}"
         
         // 2. 配置发布产物
         artifact "$buildDir/outputs/aar/${project.name}-release.aar"
         
         // 3. 配置pom文件
         pom { }
	}
}

如上所示,可以通过静态配置的方式创建发布物,libA(MavenPublication){ ... }的原型是create(string name, MavenPublication publication),我们可以利用利用该方法灵活创建发布物:

// publish.gradle
import org.gradle.api.internal.artifacts.dependencies.DefaultProjectDependency

apply plugin: "maven-publish"
apply plugin: 'signing'  //可选


publishing {
    repositories {
        mavenLocal()
        maven {
            name "build"
            url "$buildDir/repo"
        }
        maven {         
            url "xxxxx"
        }
    }

    def config = configs[project.name]
    def aarName = aarNames[project.name]
    publications.create(config.PUBLICATION_NAME, MavenPublication){
            version config.VERSION
            artifactId config.ARTIFACT_ID
            groupId config.GROUP_ID
            artifact "$buildDir/outputs/aar/${aarName}.aar"
            from components.release  //非必须
            println "*******************************************************"
            println "implementation '$groupId:$artifactId:$version'"
            println "*******************************************************"
            
            signing { // introduced by plugin: 'signing' 
                useInMemoryPgpKeys(
                        properties.getProperty('signing.keyId'),
                        properties.getProperty('signing.secretKeyRingFile'),
                        properties.getProperty('signing.password')
                )
                sign publishing.publications.release 
                sign configurations.archives
            }
        }

}
1. 配置metadata

比较简单略过。

2. 配置发布产物

MavenPublication提供了下述几种接口配置产物:

  • void from(SoftwareComponent component)
publishing {
  publications {
    maven(MavenPublication) {
      from components.java
    }
  }
}

该方式存在如下限制:

  1. 仅支持三种类型组件: components.java (added by the JavaPlugin), components.web(added by the WarPlugin)
    以及 components.javaPlatform (added by the JavaPlatformPlugin);
  2. 每个发布物仅能配置一个from,不像artifact可以配置多个;
  • MavenArtifact artifact(Object source)
task sourceJar(type: Jar) {
   archiveClassifier = "sources"
}

publishing {
  publications {
    maven(MavenPublication) {
      artifact sourceJar // Publish the output of the sourceJar task
      artifact 'my-file-name.jar' // Publish a file created outside of the build
      artifact source: sourceJar, classifier: 'src', extension: 'zip'
    }
  }
}
  • MavenArtifact artifact(Object source, Action<? super MavenArtifact> config)
    同上
publishing {
  publications {
    maven(MavenPublication) {
      artifact(sourceJar) {
        // These values will be used instead of the values from the task. The task values will not be updated.
        classifier "src"
        extension "zip"
      }
      artifact("my-docs-file.htm") {
        classifier "documentation"
        extension "html"
      }
    }
  }
}
  • void setArtifacts(Iterable<?> sources)
publishing {
  publications {
    maven(MavenPublication) {
      from components.java
      artifacts = ["my-custom-jar.jar", sourceJar]
    }
  }
}
3. 配置pom文件
  • 静态配置
pom {
    name = 'My Library'
    description = 'A concise description of my library'
    url = 'http://www.example.com/library'
    properties = [
        myProp: "value",
        "prop.with.dots": "anotherValue"
    ]
    licenses {
        license {
            name = 'The Apache License, Version 2.0'
            url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
        }
    }
    developers {
        developer {
            id = 'johnd'
            name = 'John Doe'
            email = 'john.doe@example.com'
        }
    }
    scm {
        connection = 'scm:git:git://example.com/my-library.git'
        developerConnection = 'scm:git:ssh://example.com/my-library.git'
        url = 'http://example.com/my-library/'
    }
}
  • 动态生成
pom.withXml {
    def xml = asNode()
    def dependenciesNode = xml.getAt('dependencies')[0] ?: xml.appendNode('dependencies')
    configurations.implementation.allDependencies.each {
        if (it.name != 'unspecified') {
            def dependencyNode = dependenciesNode.appendNode('dependency')
            if (it instanceof DefaultProjectDependency) {
                def temp = configs[it.name]
                dependencyNode.appendNode('groupId', temp.GROUP_ID)
                dependencyNode.appendNode('artifactId', temp.ARTIFACT_ID)
                dependencyNode.appendNode('version', temp.SDK_VERSION)
            } else {
                dependencyNode.appendNode('groupId', it.group)
                dependencyNode.appendNode('artifactId', it.name)
                dependencyNode.appendNode('version', it.version)
            }
        }
    }
}

完整示例

plugins {
    id 'com.android.library'
    id 'maven-publish'
}

group = 'com.example'
version = '1.0'

task sourcesJar(type: Jar) {
    from android.sourceSets.main.java.srcDirs  
    // archiveClassifier = 'sources' //已废弃,
    archiveClassifier.set('sources')
}

task javadocJar(type: Jar) {
    from javadoc
    // archiveClassifier = 'javadoc'
    archiveClassifier.set('javadoc')
}

publishing {
    publications {
        mavenJava(MavenPublication) {
            artifactId = 'my-library'
            from components.java
            artifact sourcesJar
            artifact javadocJar
            versionMapping {
                usage('java-api') {
                    fromResolutionOf('runtimeClasspath')
                }
                usage('java-runtime') {
                    fromResolutionResult()
                }
            }
            pom {
                name = 'My Library'
                description = 'A concise description of my library'
                url = 'http://www.example.com/library'
                properties = [
                    myProp: "value",
                    "prop.with.dots": "anotherValue"
                ]
                licenses {
                    license {
                        name = 'The Apache License, Version 2.0'
                        url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                    }
                }
                developers {
                    developer {
                        id = 'johnd'
                        name = 'John Doe'
                        email = 'john.doe@example.com'
                    }
                }
                scm {
                    connection = 'scm:git:git://example.com/my-library.git'
                    developerConnection = 'scm:git:ssh://example.com/my-library.git'
                    url = 'http://example.com/my-library/'
                }
            }
        }
    }
    repositories {
        maven {
            // change URLs to point to your repos, e.g. http://my.org/repo
            allowInsecureProtocol = true // 仓库默认不允许使用非https协议,所以这里设置为允许
            def releasesRepoUrl = "$buildDir/repos/releases"
            def snapshotsRepoUrl = "$buildDir/repos/snapshots"
            url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
            credentials {
                username 'user1'     // maven仓库账号
                password 'abc'   // maven仓库密码
            }
        }
    }
}

signing {
    sign publishing.publications.mavenJava
}


javadoc {
    if(JavaVersion.current().isJava9Compatible()) {
        options.addBooleanOption('html5', true)
    }
}


多版本发布

基于buildTypes

AGP默认为我们提供了buildTypes:debug、release。可以使用下面的代码进行验证:

// 输出:names of android.buildTypes.* is [debug, release]
println "names of android.buildTypes.* is ${android.buildTypes.stream().map{it.name}.collect()}"

一般我们会根据需要对默认的配置进行修改,比如下面:

android {
    buildTypes {
        debug {
            applicationIdSuffix ".debug"
        }

        jnidebug.initWith(buildTypes.debug)
        jnidebug {
        	setRoot('foo/jnidebug')
            packageNameSuffix ".jnidebug"
            jnidebugBuild true
        }
    }

	sourceSets {
        main { ... }
        jnidebug { ... }
    }
}

注意:每一个BuildType都会自动创建一个匹配的sourceSet。默认的路径为:src/<buildtypename>/。因此BuildType名称不能是main或者androidTest(因为这两个是由plugin强制实现的),并且他们互相之间都必须是唯一的。

先打印一下andorid.buildType.*是个啥:

println "type of android.buildTypes.* is ${android.buildTypes.first()}"

输出结果:

type of android.buildTypes.* is BuildType_Decorated{
	name=debug, 
	debuggable=true, 
	testCoverageEnabled=false, 
	jniDebuggable=false, 
	pseudoLocalesEnabled=false, 
	renderscriptDebuggable=false, 
	renderscriptOptimLevel=3, 
	minifyEnabled=false, 
	zipAlignEnabled=true, 
	signingConfig=SigningConfig_Decorated{
		name=debug, 	
		storeFile=/Users/xx/.android/debug.keystore, 
		storePassword=android, 
		keyAlias=AndroidDebugKey, 
		keyPassword=android, 
		storeType=pkcs12, 
		v1SigningEnabled=true, 
		v2SigningEnabled=true
	}, 
	embedMicroApp=false, 
	mBuildConfigFields={}, 
	mResValues={}, 
	mProguardFiles=[], 
	mConsumerProguardFiles=[], 
	mManifestPlaceholders={}
}

可以看到是BuildType类型,其支持的配置及其默认值:

Property nameDefault values for debugDefault values for release / other
debuggabletruefalse
jniDebugBuildfalsefalse
renderscriptDebugBuildfalsefalse
renderscriptOptimLevel33
applicationIdSuffixnullnull
versionNameSuffixnullnull
signingConfigandroid.signingConfigs.debugnull
zipAlignfalsetrue
runProguardfalsefalse
proguardFileN/A (set only)N/A (set only)
proguardFilesN/A (set only)N/A (set only)
manifestPlaceholdersnullnull,
eg. [umeng_app_key: “你替代的内容”,umeng_app_secret:“你要替换的内容”]
buildConfigFieldnullnull,
eg. buildConfigField “String”, “ENDPOINT”, ““http://example.com””
resValuenulleg. resValue “string”, “AppName”, “app1”
resConfignulleg. resConfig “zh”, “en” // 仅打包指定的locale的资源文件

基于productFlavors

不像buildTypes那样已经有debug、release这样的默认配置,productFlavors需要我们自行配置。先看看如何配置productFlavors:

android {
	...
	defaultConfig {
		...
		// 必须:申明所有风味维度,比如是否收费、登录方式、分发渠道等等
		flavorDimensions 'fruit', 'color'
	}

	productFlavors {
        flavor1 {
        	// 必须:为每个flavor指定维度
            dimension "apple"
            buildConfigField 'String', 'flagField', "\"$var1\""
        }
        flavor2 {
            dimension "pear"
            buildConfigField 'String', 'flagField', "\"$var2\""
        }
    }

	sourceSets {
		// main相当于base,flavor会继承main的配置
        main { ... }
        apple { ...}
        pear { ...}
    }
}

我们如法炮制,看看android.prodectFlavors.*是什么东西:

type of android.productFlavors is ProductFlavor_Decorated{
	name=apple, 
	dimension=fruit, 
	minSdkVersion=null, 
	targetSdkVersion=null, 
	renderscriptTargetApi=null, 
	renderscriptSupportModeEnabled=null, 
	renderscriptSupportModeBlasEnabled=null, 
	renderscriptNdkModeEnabled=null, 
	versionCode=null, 
	versionName=null, 
	applicationId=null, 
	testApplicationId=null, 
	testInstrumentationRunner=null, 
	testInstrumentationRunnerArguments={}, 
	testHandleProfiling=null, 
	testFunctionalTest=null, 
	signingConfig=null, 
	resConfig=null, 
	mBuildConfigFields={}, 
	mResValues={}, 
	mProguardFiles=[], 
	mConsumerProguardFiles=[], 
	mManifestPlaceholders={}, 
	mWearAppUnbundled=null
}

注意:ProductFlavor类型的android.productFlavors.*对象与android.defaultConfig共享相同的属性。defaultConfig为所有的flavor提供基本的配置,每一个flavor都可以重设这些配置的值。

为了验证上面的结论,我们再打印一下android.defaultConfig

type of android.defaultConfig is DefaultConfig_Decorated{
	name=main, 
	dimension=null, 
	minSdkVersion=DefaultApiVersion{mApiLevel=19, mCodename='null'}, 
	targetSdkVersion=DefaultApiVersion{mApiLevel=28, mCodename='null'}, 
	renderscriptTargetApi=null, 
	renderscriptSupportModeEnabled=null, r
	enderscriptSupportModeBlasEnabled=null, r
	enderscriptNdkModeEnabled=null, 
	versionCode=1, 
	versionName=1.0, 
	applicationId=com.hao.android_api_test, 
	testApplicationId=null, 
	testInstrumentationRunner=androidx.test.runner.AndroidJUnitRunner, 
	testInstrumentationRunnerArguments={}, 
	testHandleProfiling=null, 
	testFunctionalTest=null, 
	signingConfig=null, 
	resConfig=null, 
	mBuildConfigFields={}, 
	mResValues={}, 
	mProguardFiles=[], 
	mConsumerProguardFiles=[], 
	mManifestPlaceholders={}, 
	mWearAppUnbundled=null
}

扩展:android.flavorGroups

基于buildVariants实现构建干预

buildVariant的name是基于buildTypes和productFlavors组合生成的,AGP提供了三个DomainObjectCollection来承载构各个构建task所需的参数:

  • applicationVariants: application plugin
  • libraryVariants: library plugin
  • testVariants: both plugins

由于是collection,所以支持foreach遍历:

android.applicationVariants.each { ApplicationVariant variant->
	......
}

其属性如下:来源
在这里插入图片描述

关于sourceSets

当配置好android.productFlavors之后,使用如下代码进行打印:

println "content of android.sourceSets: ${android.sourceSets}"

打印结果:

content of android.sourceSets: [
	source set android test, 
	source set android test apple, 
	source set android test debug, 
	source set android test pear, 
	source set apple, 
	source set debug, 
	source set main, 
	source set pear, 
	source set release, 
	source set test, 
	source set test apple, 
	source set test debug, 
	source set test pear, 
	source set test release
]

最终执行构建任务时,会把buildVariant对应的buildType和productFlavor的sourceSet配置合并到main中进行编译打包,即:

  • 多个文件夹中的所有的源代码(src/*/java,其中*可以是:BuildVariantBuildTypeProductFlavormain)都会合并起来生成一个输出。
  • 所有的Manifest文件都会合并成一个Manifest文件。类似于BuildType,允许ProductFlavor可以拥有不同的的组件和权限声明。
  • 所有资源(Android res和assets)遵循的优先级为:BuildVariant > BuildType > ProductFlavor > main sourceSet
  • 每一个BuildVariant都会根据资源生成自己的R类(或者其它一些源代码)。Variant互相之间没有什么是共享的。

sourceSets完整配置如下:

android{
	
	sourceSets {
		// 1. main相当于base,flavor会继承main的配置
		// 2. manifest将被混合进app的manifest
		// 3. 代码行为只是另一个资源文件夹??
		// 4. 资源将叠加到main的资源中,并替换已存在的资源。
        main {
            jniLibs.srcDirs = ['libs']
            res.srcDirs = ['src/main/res']
            //manifest.srcFile 'AndroidManifest.xml'
            //java.srcDirs = ['src']
            //aidl.srcDirs = ['src']
            //renderscript.srcDirs = ['src']
            //assets.srcDirs = ['src/main/assets']
            //setRoot('main')  //android特有,作用是迁移/src/main到/main,老的Android项目才需要做这种迁移工作
        }
        // 每个buildType都会生成一个
        buildTypeX { ... }
        // 每个productFlavor都会生成一个
        productFlavorX { ... }
        ...
    }
	
	...
}

sourceSets必须跟在android后面,即android.sourceSets,否则报错

Task技巧

打印Task的依赖关系

查看task依赖关系可以帮助我们寻找符合功能需求的最佳hook的入口,代码片段如下:

afterEvaluate {
    project.tasks.findByName('assembleDebug').taskDependencies.getDependencies().each {
        println "dependOn: ${it.name}"
        it.taskDependencies.getDependencies().each {
            printDependency(it, 1)
        }
    }
}

static printDependency(task, int depth) {
    task.taskDependencies.getDependencies().each {
        println "${"        " * depth}: ${it.name}"
        it.taskDependencies.getDependencies().each {
            printDependency(it, depth + 1)
        }
    }
}

禁用Task

项目构建过程中那么多任务,有些test相关的任务可能根本不需要,可以直接关掉:

tasks.whenTaskAdded { task ->
    if (task.name.contains('AndroidTest')) {
        task.enabled = false
    }
}

tasks会获取当前project中所有的task,enabled属性控制任务开关,whenTaskAdded后面的闭包会在gradle配置阶段完成。

hook任意Task

比如我想在执行dependencies之前,加入一个hello任务,可以这么写:

afterEvaluate {
    android.applicationVariants.each {
        // 根据变体名命名hello任务
        def hello = "hello${it.name.capitalize()}"
        task(hello) {  //注册任务
            group 'myTasks' //给个分组,方便查找
            doFirst {
                println hello
            }
        }
        // 找到目标任务,即:dependencies
        def dp = tasks.findByName("dependencies")
        // 方式一:通过dependsOn实现hook
        // dp.taskDependencies.getDependencies(dp)会获取dp任务的所有依赖,
        // 让hello任务依赖dp任务的所有依赖,再让dp任务依赖hello任务,这样就可以加入某个任务到构建流程了
        tasks.findByName(hello).dependsOn dp.taskDependencies.getDependencies(dp)
        dp.dependsOn tasks.findByName(hello)
        // 【牢记】:上述操作是hook任意task的模版代码

        // 方式二:通过doFirst实现hook
        dp.doFirst {
            println hello+"_from doFirst"
        }
    }
}

Task监听

我们可能需要监听每个task的执行时间,从而确定优化方向,可以通过下面的方式监听任务执行周期:

class TimingsListener implements TaskExecutionListener, BuildListener {
    private Clock clock
    private timings = []

    @Override
    void beforeExecute(Task task) {
        clock = new org.gradle.util.Clock()
    }

    @Override
    void afterExecute(Task task, TaskState taskState) {
        def ms = clock.timeInMs
        timings.add([ms, task.path])
        task.project.logger.warn "${task.path} took ${ms}ms"
    }

    @Override
    void buildFinished(BuildResult result) {
        println "Task timings:"
        for (timing in timings) {
            if (timing[0] >= 50) {
                printf "%7sms  %s\n", timing
            }
        }
    }

    @Override
    void buildStarted(Gradle gradle) {}

    @Override
    void projectsEvaluated(Gradle gradle) {}

    @Override
    void projectsLoaded(Gradle gradle) {}

    @Override
    void settingsEvaluated(Settings settings) {}
}

gradle.addListener new TimingsListener()

JavaCompile参数调整

顾名思义JavaCompile是执行javac的gradle task,我们可以通过gradle脚本实现全局/局部的JavaCompile的执行参数修改,比如:

allprojects {
    gradle.projectsEvaluated {
        tasks.withType(JavaCompile) {
        	// 编译参数
            options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
            // 解决console中文乱码
            options.encoding = 'UTF-8'  
            // 全局设置编译JDK版本,当然也支持在android.compileOptions中进行局部设置
            sourceCompatibility JavaVersion.VERSION_1_7
        	targetCompatibility JavaVersion.VERSION_1_7
        }
    }
    ...
}

Android构建的核心task

  • ProcessManifest
    File manifestOutputFile
  • AidlCompile
    File sourceOutputDir
    RenderscriptCompile
    File sourceOutputDir
    File resOutputDir
  • MergeResources
    File outputDir
  • MergeAssets
    File outputDir
  • ProcessAndroidResources
    File manifestFile
    File resDir
    File assetsDir
    File sourceOutputDir
    File textSymbolOutputDir
    File packageOutputFile
    File proguardOutputFile
  • GenerateBuildConfig
    File sourceOutputDir
  • Dex
    File outputFolder
  • PackageApplication
    File resourceFile
    File dexFile
    File javaResourceDir
    File jniDir
    File outputFile:直接在Variant对象中使用“outputFile”可以改变最终的输出文件夹。
  • ZipAlign
    File inputFile
    File outputFile:直接在Variant对象中使用“outputFile”可以改变最终的输出文件夹。

除此之外需要了解的基础task包括:DefaultTask,JavaCompile,Copy,Zip

project技巧

[project.ext]全局参数

project.ext.set("compressPngs",  1);
project.ext {
	compressPngs = 1
}
// 引用参数
println $compressPngs

推荐使用rootProject.ext进行全局参数的传递

[var.properties]打印变量的所有属性

在gradle脚本中,可以通过下面的代码片段打印变量的所有属性:

// v是变量
def filtered = ['class', 'active']
println "productFlavor :${v.properties.sort {it.key}.collect {it}.findAll {!filtered.contains(it.key)}.join('\n')}"

[project.configurations]获取project的所有依赖项

有时候我们需要获取到module所有的依赖项,以:

  1. 生成pom文件;
  2. 获取到所有依赖库文件;
  3. 分析依赖;
task printDepsFile{
    doLast {
    	// 遍历implementation配置的依赖
        configurations.implementation.allDependencies{
            println "group: ${id.group}, name: ${id.name}, version: ${id.version} ,type:${artifact.type},extension:${artifact.extension}"
        }
        def gradleJarDepends = []
        configurations.implementation.resolvedConfiguration.resolvedArtifacts.each { artifact ->
            def id = artifact.moduleVersion.id
            println "group: ${id.group}, name: ${id.name}, version: ${id.version} ,type:${artifact.type},extension:${artifact.extension}"
            if("${id.group}"){
                gradleJarDepends.add("${id}")
            }
        }
        println "总共:" + gradleJarDepends.size + "个"+", $gradleJarDepends"
		
		// 遍历所有类型配置的依赖
		project.configurations.all { config ->
            config.dependencies
                    .withType(ProjectDependency)
                    .collect { it.dependencyProject }
                    .each { dependency ->

                        def graphKey = new Tuple2<Project, Project>(project, dependency)
                        def traits = dependencies.computeIfAbsent(graphKey) { new ArrayList<String>() }

                        if (config.name.toLowerCase().endsWith('implementation')) {
                            
                        }
                    }
        }
    }
}

[project.plugins]判断project构建类型

思路是通过根据apply的插件类型来区分:

if (project.plugins.hasPlugin('org.jetbrains.kotlin.multiplatform')) {
    multiplatformProjects.add(project)
    // multiplatform project
}
if (project.plugins.hasPlugin('org.jetbrains.kotlin.js')) {
    // js project
}
if (project.plugins.hasPlugin('com.android.library') || project.plugins.hasPlugin('com.android.application')) {
    // android Project
}
if (project.plugins.hasPlugin('java-library') || project.plugins.hasPlugin('java')) {
    // java Project
}

[project.extensions]判断android类型

val androidExtensions = context.extensions.getByName("android") as TestedExtension

if (androidExtensions is LibraryExtension) {
    // this is android library
}else if(androidExtensions is ApplicationExtension){
	// this is android application
}

AGP技巧

解析AndroidManifest文件

// 获取主清单文件
def manifestFile = android.sourceSets.main.manifest.srcFile
// 读取属性
def packageName = new XmlParser().parse(manifestFile).attribute('package')

补充知识:

  1. gradle中的applicationid用来区分应用,manifest中packageName用来指定R文件包名,并且各个productFlavor的manifest中的packageName应该一致,资料
  2. 因此初始主manifest中包名是真正总的R文件的真实包名,这对于做R文件裁剪是前提条件;

其他技巧

println文字颜色

要在Gradle脚本中打印彩色文本,可以使用ANSI转义序列。ANSI转义序列是一系列控制代码,可用于更改终端的颜色和样式。

请注意,不是所有终端都支持ANSI转义序列。如果您的终端不支持ANSI转义序列,打印的文本可能会以不同的方式显示。

task printColor {
    doLast {
        // 红色
        println "\u001b[31mThis text is red.\u001b[0m"
        // 绿色
        println "\u001b[32mThis text is green.\u001b[0m"
        // 黄色
        println "\u001b[33mThis text is yellow.\u001b[0m"
        // 蓝色
        println "\u001b[34mThis text is blue.\u001b[0m"
        // 紫色
        println "\u001b[35mThis text is purple.\u001b[0m"
        // 青色
        println "\u001b[36mThis text is cyan.\u001b[0m"
        // 白色
        println "\u001b[37mThis text is white.\u001b[0m"
        // 灰色
        println "\u001b[90mThis text is gray.\u001b[0m"
    }
}

获取打包实时的productFlavor

有时候我们会根据不同的productFlavor依赖不同的库或用不同的方式进行依赖。

  • 依赖不同的库
    比较好解决,gradle直接提供了debugImplementation这种方式
  • 不同的依赖方式
    比如debug用implement,release用compileOnly乃至embed等

可以通过下面的方式获取当前运行实时的productFlavor

// 方式一:通过gradle.startParameters.taskNames
def taskNames = gradle.startParameters.taskNames
def isAssembleRelease = taskNames.contains("assembleRelease")
def isAssembleRelease = "assembleRelease" in taskNames

// 方式二:通过gradle.taskGraph.hasTask
gradle.taskGraph.whenReady { taskGraph ->
    if (taskGraph.hasTask(task_1))    {
        compressPngs=1
    }
    if (taskGraph.hasTask(task_2))    {
        compressPngs=0
    }
}
  1. gradle.startParameters.taskNames是一个运行时概念,当我们sync project的时候其值为空数组,只有当运行gradlew app:assemble这种具体的任务时才有值;
  2. gradle.graph.allTasks也是一个运行时概念;
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值