Android Gradle 是項目中的構建神器


最近的分庫遇到了一些問題,很多都要通過Gradle腳本解決,所以稍微花時間研究了一下。

Gradle雖爲構建神器,但感覺學習曲線比較陡峭。Gradle User Guide內容很多,但有點太多了,多的你看不完,Gradle Plugin User Guide一篇文章主要講了Android相關的配置,看完可能感覺馬馬虎虎會用,但到了修改一些構建流程的時候還是不知所措。經過一段時間的摸索,我覺得在Android項目中用好Gradle,你要做到以下三點:

  1. 瞭解Groovy基本語法。

  2. 粗讀Gradle User GuideGradle Plugin User Guide

  3. 實戰,實戰,再實戰。(三遍,你懂的)

涉及到的知識點和內容比較多,我不會一一講解,本文主要會解答自己學習過程中的一些疑問,講解一些相關概念和實戰經驗,過程中也會推薦一些有質量的博客文章。

Groovy語言

Gradle基於Groovy語言,雖然接觸Gradle比較久,甚至寫過一點Groovy語句,但對語言本身並不瞭解。爲什麼用Groovy呢?Groovy運行在JVM上,在Java語言的基礎上,借鑑了腳本語言的諸多特性,相比Java代碼量更少,Groovy兼容Java,可以使用Groovy和Java混合編程,可以直接使用各種Java類庫。

Groovy語法的學習,推薦官方文章Differences with Java和IBM developerWorks的精通Groovy。瞭解了基本語法,對讀寫gradle腳本都會有幫助,比如隨便舉下面幾個例子:

  1. 比如爲何在gradle腳本中使用InputStream不用import包,而使用ZipFile需要import包?因爲groovy默認import了下面的包和類,無需再import.

    java.io.*
    java.lang.*
    java.math.BigDecimal
    java.math.BigInteger
    java.net.*
    java.util.*
    groovy.lang.*
    groovy.util.*
  2. 經常看到${var1}的用法是怎麼回事? 這是Groovy中的GString,可以在雙引號中直接使用,用於字符串疊加非常方便。

    def dx = tasks.findByName("dex${variant.name.capitalize()}")
  3. 下面的代碼你真的能看懂嗎?

    //apply是一個方法,plugin是參數,值爲'com.android.application'
    apply plugin: 'com.android.application'
    
    /**
    *buildscript,repositories和dependencies本身是方法名。
    *後面跟的大括號部分,都是一個閉包,作爲方法的參數。
    *閉包可以簡單的理解爲一個代碼塊或方法指針。
    */
    buildscript {
        repositories {
            jcenter()
        }
        dependencies {
            classpath 'com.android.tools.build:gradle:1.2.3'
        }
    }
    
    //groovy遍歷的一種寫法 each後面是閉包
    android.applicationVariants.each { variant ->
    }

Gradle概念

下面講幾個Gradle相關的概念,幾個比較重要的吧,更多的東西還是要自己去看Gradle User Guide

生命週期

Gradle構建系統有自己的生命週期,初始化、配置和運行三個階段。

  1. 初始化階段,會去讀取根工程中setting.gradle中的include信息,決定有哪幾個工程加入構建,創建project實例,比如下面有三個工程: include ':app', ':lib1', ':lib2'

  2. 配置階段,會去執行所有工程的build.gradle腳本,配置project對象,一個對象由多個任務組成,此階段也會去創建、配置task及相關信息。

  3. 運行階段,根據gradle命令傳遞過來的task名稱,執行相關依賴任務。

任務創建

很多文章都會告訴你,任務創建要這樣:

task hello {
    doLast {
        println "hello"
    }
}

或者用<<替換doLast,那我就很納悶,定義個任務怎麼這麼麻煩,還要加什麼doLast,我直接這樣不行嗎?

task hello {
    println "hello"
}

上面的這種寫法,“hello” 是在gradle的配置階段打印出來的,而前面的寫法是在gradle的運行階段打印出來的,所以怎麼寫要看你的需求了。

另外task中有一個action list,task運行時會順序執行action list中的action,doLast或者doFirst後面跟的閉包就是一個action,doLast是把action插入到list的最後面,而doFirst是把action插入到list的最前面。

任務依賴

當我們在Android工程中執行./gradlew build的時候,會有很多任務運行,因爲build任務依賴了很多任務,要先執行依賴任務才能運行當前任務。任務依賴主要使用dependsOn方法,如下所示:

task A << {println 'Hello from A'}
task B << {println 'Hello from B'}
task C << {println 'Hello from C'}
B.dependsOn A
C.dependsOn B

瞭解更多,可以看一下偵躍翻譯的Gradle tip #3-Task順序

增量構建

你在執行gradle命令的時候,是不是經常看到有些任務後面跟着[UP-TO-DATE],這是怎麼回事?

在Gradle中,每一個task都有inputs和outputs,如果在執行一個Task時,如果它的輸入和輸出與前一次執行時沒有發生變化,那麼Gradle便會認爲該Task是最新的,因此Gradle將不予執行,這就是增量構建的概念。

一個task的inputs和outputs可以是一個或多個文件,可以是文件夾,還可以是project的某個property,甚至可以是某個閉包所定義的條件。自定義task默認每次執行,但通過指定inputs和outputs,可以達到增量構建的效果。

依賴傳遞

Gradle默認支持傳遞性依賴,比如當前工程依賴包A,包A依賴包B,那麼當前工程會自動依賴包B。同時,Gradle支持排除和關閉依賴性傳遞。

之前引入遠程AAR,一般會這樣寫:

compile 'com.somepackage:LIBRARY_NAME:1.0.0@aar'

上面的寫法會關閉依賴性傳遞,所以有時候可能就會出問題,爲什麼呢?本來以爲@aar是指定下載的格式,但其實不然,遠程倉庫文件下載格式應該是由pom文件中packaging屬性決定的,@符號的真正作用是Artifact only notation,也就是隻下載文件本身,不下載依賴,相當於變相的關閉了依賴傳遞,可以看一下sf的這個問題,通過添加transitive=true可以解決。但其實如果遠程倉庫有pom文件存在,compile後面根本不需要加"@aar",也就不會遇到這個問題了。

Android Gradle實戰

下面講講在Android Gradle實戰中遇到的一些問題和經驗,感覺還是蠻多幹貨的。

productFlavors

這個東西基本上已經爛大街了,gradle的項目一般都會使用Product Flavor,看完美團的文章,你應該就懂了。

美團Android自動化之旅—適配渠道包 http://tech.meituan.com/mt-apk-packaging.html

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發佈公共項目http://www.linuxidc.com/Linux/2015-06/118878p2.htm)。

取消任務

項目構建過程中那麼多任務,有些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\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()

上面是對每個任務計時的一個例子,想要了解每個任務的作用,你可以修改上面的腳本,打印出每個任務的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更加得心應手。


發佈了27 篇原創文章 · 獲贊 1 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章