• Android项目中如何用好构建神器Gradle?


     

    摘要:本文作者贾吉鑫为大众点评Android工程师,在进行团队并行开发时,分库遇到的问题很多都要通过Gradle脚本解决。Gradle虽为构建神器,但学习曲线比较陡峭,要想在Android项目中用好Gradle必须要做到三点。

    Android Gradle实战

    下面讲讲在Android Gradle实战中遇到的一些问题和经验,感觉还是蛮多干货的。

    productFlavors

    这个东西基本上已经烂大街了,gradle的项目一般都会使用Product Flavor,看完美团的文章,你应该就懂了。

    美团Android自动化之旅—适配渠道包

    buildTypes

    很多App有内测版和正式版,怎么让他们同时安装在一个手机上?同时安装在一个手机上,要求packageName不同的,用productFlavors可以解决,但可能不够优雅,alpha版本还要来个debug和release版本岂不是很蛋疼?可以用buildTypes来解决,淘宝资深架构师朱鸿的文章有比较详细的讲解,但有些内容可能有些过时了,需要更改脚本。

    依赖更新

    项目依赖的远程包如果有更新,会有提醒或者自动更新吗? 不会的,需要你手动设置changing标记为true,这样gradle会每24小时检查更新,通过更改resolutionStrategy可以修改检查周期。

    configurations.all {
        // check for updates every build
        resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
    }
    dependencies {
        compile group: "group", name: "projectA", version: "1.1-SNAPSHOT", changing: true
    }

    之前上传aar同一版本到maven仓库,但依赖却没有更新,该怎么办呢?可以直接删除本地缓存,缓存在~/.gradle/caches目录下,删除缓存后,下次运行就会自动重新下载远程依赖了。

    上传aar到Maven仓库

    在工程的build.gradle中添加如下脚本:

    apply plugin: 'maven'
    uploadArchives {
        repositories {
            mavenDeployer {
                pom.groupId = GROUP_ID
                pom.artifactId = ARTIFACT_ID
                pom.version = VERSION
                repository(url: RELEASE_REPOSITORY_URL) {
                    authentication(userName: USERNAME, password: PASSWORD)
                }
            }
        }
    }

    在build.gradle同目录下添加gradle.properties文件,配置如下:

    GROUP_ID=dianping.android.nova.thirdparty
    ARTIFACT_ID=zxing
    VERSION=1.0
    RELEASE_REPOSITORY_URL=http://mvn.dp.com/nova
    USERNAME=hello
    PASSWORD=hello

    gradle.properties的属性会被build.gradle读取用来上传aar,最后执行./gradlew :Zxing:uploadArchives即可。

    更多配置,可参考建立企业内部maven服务器并使用Android Studio发布公共项目

    取消任务

    项目构建过程中那么多任务,有些test相关的任务可能根本不需要,可以直接关掉,在build.gradle中加入如下脚本:

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

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

    加入任务

    任务可以取消了,但还不尽兴啊,想加入任务怎么搞?前面讲了dependsOn的方法,那就拿过来用啊,但是原有任务的依赖关系你又不是很清楚,甚至任务名称都不知道,怎么搞?

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

    afterEvaluate {
        android.applicationVariants.each { variant ->
            def dx = tasks.findByName("dex${variant.name.capitalize()}")
            def hello = "hello${variant.name.capitalize()}"
            task(hello) << {
                println "hello"
            }
            tasks.findByName(hello).dependsOn dx.taskDependencies.getDependencies(dx)
            dx.dependsOn tasks.findByName(hello)
        }
    }

    afterEvaluate是什么鸟?你可以理解为在配置阶段要结束,项目评估完会走到这一步。

    variant呢?variant = productFlavors+ buildTypes,所以dex打包的任务可能就是dexCommonDebug。

    你怎么知道dex任务的具体名称?Android Studio中的Gradle Console在执行gradle任务的时候会有输出,可以仔细观察一下。

    hello任务定义的这么复杂干啥?我直接就叫hello不行吗?不行,each就是遍历variants,如果每个都叫hello,多个variant都一样,岂不是傻傻分不清楚,加上variant的name做后缀,才有任务的区分。

    关键来了,dx.taskDependencies.getDependencies(dx)会获取dx任务的所有依赖,让hello任务依赖dx任务的所有依赖,再让dx任务依赖hello任务,这样就可以加入某个任务到构建流程了,是不是感觉非常灵活。

    我突然想到,用doFirst的方式加入一个action到dx任务中,应该也可以达到上面效果。

    gradle加速

    gradle加速可以看看这位朋友写的加速Android Studio/Gradle构建,我就不多嘴了。并行编译,常驻内存,还有离线模式这些思路对gradle的加速感觉还是比较有限。

    想要更快,可以尝试下Facebook出品的Buck,可以看一下Vine团队适配Buck的技术文章,我们的架构师也有适配Buck,加速效果在10倍左右,但有两个缺点,不支持Windows系统,不支持远程依赖。

    任务监听

    你想知道每个执行任务的运行时间吗?你想知道每个执行任务都是干嘛的吗?把下面这段脚本加入build.gradle中即可:

    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
    ", 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()

    上面是对每个任务计时的一个例子,想要了解每个任务的作用,你可以修改上面的脚本,打印出每个任务的inputs和outputs。比如assembleDebug那么多依赖任务,每个都是干什么的,一会compile,一会generate,有什么区别?看到每个task的输入输出,就可以大体看出它的作用。如果对assemble的每个任务监听,你会发现改一行代码build的时间主要花费在了dex上,buck牛逼的地方就是对这个地方进行了优化,大大减少了增量编译运行的时间。

    buildscript方法

    Android项目中,根工程默认的build.gradle应该是这样的:

    // Top-level build file where you can add configuration options common to all sub-projects/modules.
    
    buildscript {
        repositories {
            jcenter()
        }
        dependencies {
            classpath 'com.android.tools.build:gradle:1.2.3'
        }
    }
    
    allprojects {
        repositories {
            jcenter()
        }
    }

    一会一个jcenter()这是在干什么?buildscript方法的作用是配置脚本的依赖,而我们平常用的compile是配置project的依赖。repositories的意思就是需要包的时候到哥这里来找,然后你以为com.android.tools.build:gradle:1.2.3会从jcenter那里下载了是吧,图样图森破,不信加入下面这段脚本看看输出:

    buildscript {
        repositories {
            jcenter()
        }
        repositories.each {
            println it.getUrl()
        }
        dependencies {
            classpath 'com.android.tools.build:gradle:1.2.3'
        }
    }

    结果是这样的:

    file:/Applications/Android%20Studio.app/Contents/gradle/m2repository/ https://jcenter.bintray.com/

    我靠,仓库竟然直接在Android Studio应用内部,所以说你去掉buildscript的jcenter()完全没有关系啊,下面还有更爽的,我们知道有依赖传递,上面classpath 中的gradle依赖gradle-coregradle-core依赖lintlint依赖lint-checkslint-checks最后依赖到了asm,并且这个根目录中的依赖配置会传到所有工程的配置文件,所以如果你要引用asm相关的类,不用设置classpath,直接import就可以了。你怎么知道前面的依赖关系的?看上面m2repository目录中对应的pom文件就可以了。

    为什么讲到ASM呢?ASM又是个比较刁的东西,可以直接用来操纵Java字节码,达到动态更改class文件的效果。可以用ASM面向切面编程,达到解耦效果。Android DEX自动拆包及动态加载简介中提到的class依赖分析和R常量替换的脚本都可以用ASM来搞。

    引入脚本

    脚本写多了,都挤在一个build.gradle里也不好,人长大了总要自己出去住,那可以把部分脚本抽出去吗?当然可以,新建一个other.gradle把脚本抽离,然后在build.gradle中添加apply from 'other.gradle'即可,抽出去以后你会发现本来可以直接import的asm包找不到了,怎么回事?根工程中配置的buildscript会传递到所有工程,但只会传到build.gradle脚本中,其他脚本可不管,所以你要在other.gradle中重新配置buildscript,并且other.gradle中的repositories不再包含m2repository目录,自己配置jcenter()又会导致依赖重新下载到~/.gradle/caches目录。如果不想额外下载,也可以在other.gradle中这么搞:

    buildscript {
        repositories {
            maven {
                url rootProject.buildscript.repositories[0].getUrl()
            }
        }
        dependencies {
            classpath 'com.android.tools.build:gradle:1.2.3'
        }
    }

    获取AndroidManifest文件

    ApplicationId versus PackageName提到,gradle中的applicationid用来区分应用,manifest中packageName用来指定R文件包名,并且各个productFlavor 的manifest中的packageName应该一致。applicationid只是gradle脚本中的定义,其实最后生成的apk中的manifest文件的packageName还是会被applicationid替换掉。

    那获取R文件的包名怎么搞?要获取AndroidManifest中package属性,并且这个manifest要是起始的文件,因为最终文件中的package属性会被applicationid冲掉,由于各个manifest中的package属性一样,并且非主manifest可以没有package属性,所以只有获取主manifest的package属性才是最准确的。

    def manifestFile = android.sourceSets.main.manifest.srcFile
    def packageName = new XmlParser().parse(manifestFile).attribute('package')

    无用资源

    无用的资源就不要打包进APK了。

    Resource Shrinking

    一个Bug

    之前在创业公司,用 Travis做持续继承,遇到一个让我很纠结的问题。在Travis上执行构建脚本如下:

    ./gradlew clean
    ./gradlew assembleXR

    最后生成的APK在运行的时候报错,提示找不到某个.so文件,解压发现APK中果然缺少某个库工程的.so文件,但在本地运行的时候却是没有问题,纠结了好久,后来研究发现Android Studio中执行Clean Project的时候,会执行generateSources的任务,把它加入构建脚本后才打包正确。最近发现,这原来是个Bug,并且已经在android gradle1.3被修复了。

    匆匆忙忙间,写了很多东西。读完此文,希望你能感受到构建神器的魅力,感受到它的灵活强大,当然也希望能让你使用Gradle更加得心应手。

    贾吉鑫(@寒江不钓),大众点评Android工程师,开发经验丰富,乌云白帽子,关注网络安全,个人博客:http://jiajixin.cn

  • 相关阅读:
    前沿技术解密——VirtualDOM
    Ques核心思想——CSS Namespace
    Unix Pipes to Javascript Pipes
    Road to the future——伪MVVM库Q.js
    聊聊CSS postproccessors
    【译】十款性能最佳的压缩算法
    Kafka Streams开发入门(9)
    Kafka Streams开发入门(8)
    【译】Kafka Producer Sticky Partitioner
    【译】99th Percentile Latency at Scale with Apache Kafka
  • 原文地址:https://www.cnblogs.com/tianshiaimi/p/4941745.html
Copyright © 2020-2023  润新知