Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 深入理解Android之Gradle學習筆記

深入理解Android之Gradle學習筆記

編輯:關於Android編程

最近在學習gradle,innost的這篇文章可以說是目前中文說gradle最好的文章
深入理解 Android 之 Gradle.文章名字雖然叫深入理解,但是其實講的也不深,不過比其他的說腳本怎麼配置的文章好太多了,讀完之後收貨頗多,在這裡記錄重點,並且把他文中的demo進行實現改進(作者未提供源碼),算是對原文的一個總結和補充(源碼在文末)。

基礎知識

Gradle

Gradle 是一個框架,負責定義流程和規則,而具體的構建工作則是通過插件的方式來完成的,比如編譯 Java 有 Java 插件,編譯 Groovy 有 Groovy 插件,編譯 Android APP 有 Android APP 插件,編譯 Android Library 有 Android Library 插件。我們可以通過apply plugin:'XXX'來導入插件。

Gradle對象

Gradle 主要有三種對象,這三種對象和三種不同的腳本文件對應,在 gradle 執行的時候,會將腳本轉換成對應的對端:

Gradle 對象:當我們執行 gradle xxx 或者什麼的時候,gradle 會從默認的配置腳本中構造出一個 Gradle 對象。在整個執行過程中,只有這麼一個對象。Gradle 對象的數據類型就是 Gradle。我們一般很少去定制這個默認的配置腳本。 Project 對象:每一個 build.gradle 會轉換成一個 Project 對象。 Settings 對象:每一個 settings.gradle 都會轉換成一個 Settings 對象。

Gradle生命周期

\

Gradle工作流程

\
Gradle工作包含三個階段:

首先是初始化階段。對我們前面的multi-project build而言,就是執行settings.gradle Configration階段的目標是解析每個project中的build.gradle。比如multi-project build例子中,解析每個子目錄中的build.gradle。在這兩個階段之間,我們可以加一些定制化的Hook。這當然是通過API來添加的。 Configuration階段完了後,整個build的project以及內部的Task關系就確定了。恩?前面說過,一個Project包含很多Task,每個Task之間有依賴關系。Configuration會建立一個有向圖來描述Task之間的依賴關系。所以,我們可以添加一個HOOK,即當Task關系圖建立好後,執行一些操作。
最後一個階段就是執行任務了。當然,任務執行完後,我們還可以加Hook。

基本task

android插件依賴於Java插件,而Java插件依賴於base插件。base插件有基本的tasks生命周期和一些通用的屬性。base插件定義了例如assemble和clean任務,Java插件定義了check和build任務,這兩個任務不在base插件中定義。

這些tasks的約定含義:

assemble: 集合所有的output
clean: 清除所有的output
check: 執行所有的checks檢查,通常是unit測試和instrumentation測試
build: 執行所有的assemble和check

Posdevice實例

前提

本文OS為mac,直接使用AS的Terminal來構建,主要是2個命令./gradlew assemble和./gradlew clean,所以就不用搭環境了。當然很多時候我會在./gradlew xxx之後加入-q,可以去掉一些系統日志,讓結果看起來更清晰點。
本文的gradlew版本如下

X-Pro:Version2Asset fish$ ./gradlew -version

------------------------------------------------------------
Gradle 2.14.1
------------------------------------------------------------

Build time:   2016-07-18 06:38:37 UTC
Revision:     d9e2113d9fb05a5caabba61798bdb8dfdca83719

Groovy:       2.4.4
Ant:          Apache Ant(TM) version 1.9.6 compiled on June 29 2015
JVM:          1.8.0_77 (Oracle Corporation 25.77-b03)
OS:           Mac OS X 10.10.5 x86_64

需求

(為了更好的體現gradle的思想,我對原文的需求進行適當的修改。)
有個android Project,內有2個module,分別是app module和library module.其中app module的名字叫app,library module的名字叫cposdevicesdk。

cposdevicesdk編譯出release版本的jar包拷貝到根目錄的output文件夾下,debug版本不編譯 app編譯產生的debug和release的apk都需要拷貝到根目錄的output文件夾下 output文件夾下最後會有3個產物,app編譯產生的debug和release版本,cposdevicesdk編譯出release版本,這3個產物的名字內必須有版本號,版本號來自manifest

實現

需求定了就可以撸起來了。很容易的,我們new一個project叫做Posdevice,裡面有2個module,app和cposdevicesdk,app依賴於cposdevicesdk。此時工程結構如下所示。

\

此時其實有3個build.gradle文件,一個setting.gradle文件。3個build.gradle分別是根build.gradle,module app內build.gradle以及cposdevicesdk內build.gradle。

一次gradle構建只產生一個gradle對象,有多少個module便對應多少個gradle project(注意和android studio的Project區分,本文中as的project我都會寫明AS project)
所以這裡會有一個gradle對象,2個project對象,1個setting對象

編譯環境配置

首先我們看到app和cposdevicesdk的build.gradle裡面都有以下代碼,注意下compileSdkVersion和buildToolsVersion,不同人的機器上,這些值可能不一樣,所以最好不要在build.gradle裡面寫死(有時候github上拉下來的代碼編譯不過也是由此引起)。

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.0"
    defaultConfig {
        ...
    }
    buildTypes {
        ...
    }
}

那怎麼寫compileSdkVersion和buildToolsVersion才比較靈活呢?有2種方法,一種是寫在local.properties裡面。我們在setting.gradle裡去讀取值,然後利用ext給grale對象創建一個成員存起來,以後全局都可以從gradle裡獲取值了。第二種是利用gradle.properties文件。我這裡為了學習對compileSdkVersion采用方法1,對buildToolsVersion采用方法2

ext gradle

代碼如下,首先在local.properties裡添加sdk.api=android-25,注意必須要帶android,不能只寫25.

#local.properties

#AS幫我們生成的
sdk.dir=/Users/fish/Documents/android-sdk-macosx

#額外添加,必須如下寫,不能只寫25
sdk.api=android-25

接著在settings.gradle內讀取到sdk.api的值,然後用ext給gradle增加一個變量api,這樣其他地方就能用gradle.api來取這個值了

//settings.gradle
def initSdkApi(){
    println "setting initSdkApi"
    Properties properties = new Properties()
    //local.properites 也放在 posdevice 目錄下

    File propertyFile = new File(rootDir.getAbsolutePath() + "/local.properties")
    properties.load(propertyFile.newDataInputStream())
    /*
      根據 Project、Gradle 生命周期的介紹,settings 對象的創建位於具體 Project 創建之前
      而 Gradle 底對象已經創建好了。所以,我們把 local.properties 的信息讀出來後,通過
      extra 屬性的方式設置到 gradle 對象中
      而具體 Project 在執行的時候,就可以直接從 gradle 對象中得到這些屬性了!

    */
    gradle.ext.api = properties.getProperty('sdk.api')
}
//初始化
initSdkApi()
include ':app', ':cposdevicesdk'
//app和cposdevicesdk裡的build.gradle
android {
//    采用api導入的方式
    compileSdkVersion gradle.api
 }

gradle.properties

這種方式會更簡單,在gradle.properties裡定義buildToolsVer

#gradle.properties

org.gradle.jvmargs=-Xmx1536m

# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
#額外添加
buildToolsVer=25.0.0

然後在2個module的build.gradle裡都能用buildToolsVer了

android {
//    采用api導入的方式
    compileSdkVersion gradle.api
//    利用gradle.properties
    buildToolsVersion buildToolsVer
    }

結論

明顯第二種方法簡單一些,所以我們盡量利用gradle.properties,當然第一種方法學習下來熟悉gradle也不錯。
配置好之後,我們可以在AS的terminal裡執行下./gradlew assemble,順利通過

utils.gradle

在gradle中,我們常常會定義一些常用的函數,這樣全局通用,這些函數往往會寫到一個gradle文件裡,我們就定一個utils.gradle。這裡定義2個函數,一個是getVersionNameAdvanced,從manifest內去獲取版本號。另一個是disableDebugBuild,對debug的task設置disable,這樣task就不會執行了。

def getVersionNameAdvanced(){

    def xmlFile = project.file("src/main/AndroidManifest.xml")
    def rootManifest = new XmlSlurper().parse(xmlFile)
    return rootManifest['@android:versionName']
}


//對於 android library 編譯,我會 disable 所有的 debug 編譯任務

def disableDebugBuild(){

    //返回值保存到 targetTasks 容器中
    println "project.tasks size "+ project.tasks.size()

    //project.tasks 包含了所有的 tasks,下面的 findAll 是尋找那些名字中帶 debug 的 Task。
    def targetTasks = project.tasks.findAll{task ->
        task.name.contains("Debug")
    }
    //對滿足條件的 task,設置它為 disable。如此這般,這個 Task 就不會被執行

    targetTasks.each{
//        println "disable debug task  : ${it.name}"
        it.setEnabled false
    }
}
//將函數設置為 extra 屬性中去,這樣,加載 utils.gradle 的 Project 就能調用此文件中定義的函數了

ext{
    getVersionNameAdvanced = this.&getVersionNameAdvanced
    disableDebugBuild = this.&disableDebugBuild
}

utils.gradle裡面定義了這些函數,其他gradle文件要用必須要apply(相當於import)。那我們是不是要每個gradle都加下面代碼呢?
apply from: rootProject.getRootDir().getAbsolutePath() + "/utils.gradle"

這麼做當然可以,但是還有更簡單的方法,那就是在根build.gradle裡配subprojects,完整的根build.gradle如下所示

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

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }

}


subprojects{
//為每個子 Project 加載 utils.gradle 。當然,這句話可以放到 buildscript 花括號之後,必須位於subprojects之內
    apply from: rootProject.getRootDir().getAbsolutePath() + "/utils.gradle"
}



allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

禁止cposdevicesdk的debug版本編譯

好了,基礎都寫好了,下面來完成需求,在cposdevicesdk的build.gradle裡加以下代碼,就可以禁止cposdevicesdk的debug版本編譯,這裡的project就是cposdevicesdk的build.gradle對應的project,project.afterEvaluate會在task有向圖創建完畢之後被調用。

/*
  因為我的項目只提供最終的 release 編譯出來的 Jar 包給其他人,所以不需要編譯 debug 版的東西

  當 Project 創建完所有任務的有向圖後,我通過 afterEvaluate 函數設置一個回調 Closure。在這個回調

  Closure 裡,我 disable 了所有 Debug 的 Task
*/
project.afterEvaluate{
    println 'afterEvaluate -> disableDebugBuild lib'
    disableDebugBuild()
}

拷貝jar和apk

拷貝這件事情應該發生在assemble之後,我們如何在assmble之後插入一個拷貝的任務呢?介紹2種方法

函數調用

第一種方法是函數調用,innost的文章裡用這種方法。先找的 assemble 任務,然後我通過 doLast 添加了一個 Action。這個 Action 就是 copyOutput,copyOutput是一個在utils.gradle裡定義的函數。

tasks.getByName("assemble"){
    it.doLast{
        println "$project.name: After assemble, jar libs are copied to local repository"
        copyOutput(true)
     }
}

task->finalizedBy

這種方法是寫一個copyTask,然後把copyTask綁定在assembleRelease後面,在我的代碼裡使用這種方法,代碼如下,其實要是綁在assemble後面更加合理,但是我試了下不行,不知道為什麼。

//lib的build.gradle
task copyTask(type: Copy){
    println "i am coping "
    from('build/intermediates/bundles/release/')
    into('../output/')
    include('classes.jar')
    rename (/(.*).jar/, 'cposdevicesdk-release'+project.getVersionNameAdvanced()+'.jar')
}

tasks.whenTaskAdded { task ->
    //下邊如果用 assemble,不行
    if (task.name == 'assembleRelease') {
        task.finalizedBy 'copyTask'
    }
}

命名加版本號

其實上邊的copyTask裡已經加入改名字的代碼了.app的copyTask如下,比較簡單,我們自己定義了一個task,copyTask 他的類型是Copy(代表繼承AbstractCopyTask),後面的from,into,include,rename都是AbstractCopyTask的方法,返回this,這是gradle task的常見寫法。
rename的時候使用正則替換,第一個變量是一個正則表達式用//包起來,代表以.apk結尾的任意字符串,第二個變量裡的$1就是.apk之前的所有字符串。

task copyTask(type: Copy){
    println "apk is coping "
    from('build/outputs/apk')
    include('*.apk')
    into('../output/')
    rename(/(.*).apk/,'$1-'+project.getVersionNameAdvanced()+'.apk')
}

lib的copyTask如下,首先lib編譯出來是aar文件,而我們想要jar包,jar包在哪呢?jar包是中間產物,文件是 ./build/intermediates/bundles/release/classes.jar,我們只要把他拷貝出來就行了。

task copyTask(type: Copy){
    println "jar is coping "
    from('build/intermediates/bundles/release/')
    into('../output/')
    include('classes.jar')
    rename (/(.*).jar/, 'cposdevicesdk-release'+project.getVersionNameAdvanced()+'.jar')
}

clean注意清除output

我們每次./gradlew assemble都會把2個apk,1個jar拷貝到ouput裡去,所以對應的clean要加入刪除代碼,在clean的時候刪除output文件夾,我們可以在clean後加入刪除的代碼就好了,如下所示。

clean.doFirst {
    delete "${rootDir}/output/"
    println "delete output before clean"
}

好了,大功告成!可以用./gradlew assemble和./gradlew clean2個命令玩起來了。

其他

根據下邊的日志看起來,copy應該會無效啊,此時afterEvaluate都沒執行,是處於Configuration階段,沒有到Execution階段(在execution階段完成各種編譯鏈接) 但是實際上copy是發生在assemble之後的,我估計這就是閉包的doLast和直接代碼的區別,真正的copy發生在doLast內。L12之後開始execution。

192:Posdevice fish$ ./gradlew assemble -q
setting.gradle execute
setting initSdkApi
root build.gradle execute
app build.gradle execute
apk is coping 
lib build.gradle execute
jar is coping 
afterEvaluate -> disableDebugBuild lib
project.tasks size 152
debug tasks size 73
taskGraph.whenReady
after assemble

實例2 通過構建腳本影響源代碼

需求

默認構建是產生debug和release2個包,要求再加一個demo包,demo包的簽名和debug包保持一致 在apk的第一個頁面顯示 I am Debug/Release/Demo

實現

首先第一個需求加一個demo包,非常簡單在buildtype那裡加就ok了。主要看第二個需求,要不同的buildtype編譯出來的apk能夠知道自己是屬於哪個buildtype的,這裡實際上是通過gradle代碼影響了工程代碼。一般來說工程代碼和構建腳本是相互獨立的,要如何才能影響到工程的代碼呢?我們可以在構建的時候把當前的buildtype寫到某個文件,然後在apk的代碼裡去讀取這個文件。ok,lets do it!

buildtype增加demo

首先實現buildtype增加demo,很簡單,在app的build.gradle內的buildTypes內加下demo即可,demo還得配置下簽名。buildTypes內其實隱藏了一個debug。

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        demo{
            //和debug使用同一個簽名
            signingConfig signingConfigs.debug
        }
    }

assets文件記錄buildtype

根據innost大神的思路,我寫下了如下代碼,在preDebugBuild、preReleaseBuild、preDemoBuild任務開始的時候添加一個doFirst任務,這是一種常見的做法,preXXXBuild完成之後就會執行我們的doFirst內的任務。這樣看起來沒什麼問題,但是我試了下,有問題。
之前,我們一直在用2個命令./gradlew assemble和./gradlew clean,現在再學習幾個。./gradlew assembleDebug和./gradlew assembleRelease和./gradlew assembleDemo,這3個命令分別是構建debug包,構建relese包和構建Demo包,實際上assemble就是依賴於assembleDebug、assembleRelease、assembleDemo這3個task。在這裡,我試了下用./gradlew assembleDebug得到debug包,可是debug包裡的assets文件裡寫的是I am release。然後我又用./gradlew assembleDemo構建了demo包,結構裡面還是I am release。Why?

    def  runtime_config_file = 'app/src/main/assets/runtime_config'

    project.afterEvaluate{
      //找到 preDebugBuild 任務,然後添加一個 Action  
      tasks.getByName("preDebugBuild"){
            it.doFirst{
                println "generate debug configuration for ${project.name}"
                def configFile = new File(runtime_config_file)
                configFile.withOutputStream{os->
                    os << I am Debug\n'  //往配置文件裡寫 I am Debug
                 }
            }
        }
       //找到 preReleaseBuild 任務  

        tasks.getByName("preReleaseBuild"){
            it.doFirst{
                println "generate release configuration for ${project.name}"
                def configFile = new File(runtime_config_file)
                configFile.withOutputStream{os->
                    os << I am release\n'
                }
            }
        }
       //找到 preDemoBuild。這個任務明顯是因為我們在 buildType 裡添加了一個 demo 的元素  

      //所以 Android APP 插件自動為我們生成的  

        tasks.getByName("preDemoBuild"){
            it.doFirst{
                println "generate offlinedemo configuration for ${project.name}"
                def configFile = new File(runtime_config_file)
                configFile.withOutputStream{os->
                    os << I am Demo\n'
                }
            }
        }
    }

我把task的依賴圖打出來後發現,原來有如下依賴關系,從下面可以看出assembleDebug會調用preDebugBuild, preDemoBuild,preReleaseBuild,所以雖然我們只是執行assembleDebug,但是preDebugBuild, preDemoBuild,preReleaseBuild都會被調用,所以最後寫成了I am release。原作者能夠成功,估計是gradle插件的版本不一樣。

->表示depend on
assembleDebug->packageDebug->transformClassesWithDexForDebug->prepareDebugDependencies->prepareComAndroidSupportSupportCoreUi2501Libarary->preDebugBuild, preDemoBuild,preReleaseBuild

那怎麼辦呢?其實很簡單,把assembleDebug和assembleDemo的具體task看一下,比較一下看看各自有什麼特殊的task,基於這個task就可以了。怎麼看assembleDebug的具體task呢?執行./gradlew assembleDebug就可以了,注意不要加-q,大概如下所示,以冒號開頭的都是任務,比較多。

...
:app:prepareComAndroidSupportAnimatedVectorDrawable2501Library UP-TO-DATE
:app:prepareComAndroidSupportAppcompatV72501Library UP-TO-DATE
:app:prepareComAndroidSupportSupportCompat2501Library UP-TO-DATE
:app:prepareComAndroidSupportSupportCoreUi2501Library UP-TO-DATE
:app:prepareComAndroidSupportSupportCoreUtils2501Library UP-TO-DATE
:app:prepareComAndroidSupportSupportFragment2501Library UP-TO-DATE

...

我把assembleDebug和assembleDemo的具體task對比了一下,找到了prepareDebugDependencies和prepareDemoDependencies,嘗試了下用prepareXXXDependencies,果然成功了,而且直接用./gradlew assemble生成3個包也沒問題!

效果如下:

\

app的build.gradle部分代碼如下所示<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> def runtime_config_file = 'app/src/main/assets/runtime_config' project.afterEvaluate{ println "task size "+tasks.size() //找到 prepareDebugDependencies 任務,然後添加一個 Action tasks.getByName("prepareDebugDependencies"){ it.doFirst{ println "generate debug configuration for ${project.name}" def configFile = new File(runtime_config_file) configFile.withOutputStream{os-> os << 'I am Debug\n' //往配置文件裡寫 I am Debug } } } //找到 prepareReleaseDependencies 任務 tasks.getByName("prepareReleaseDependencies"){ it.doFirst{ println "generate release configuration for ${project.name}" def configFile = new File(runtime_config_file) configFile.withOutputStream{os-> os << 'I am release\n' } } } //找到 prepareDemoDependencies tasks.getByName("prepareDemoDependencies"){ it.doFirst{ println "generate demo configuration for ${project.name}" def configFile = new File(runtime_config_file) configFile.withOutputStream{os-> os << 'I am Demo\n' } } } }

實例2優化–buildConfigField

實例2這麼做其實挺復雜的,我們完全可以使用更簡單的方式來解決問題,那就是使用buildConfigField。
我們在app的build.gradle內寫如下代碼,用buildConfigField來定義一個field叫做API_URL。

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            buildConfigField "String", "API_URL","\"i am release\""

        }
        demo{
            //和debug使用同一個簽名
            signingConfig signingConfigs.debug
            applicationIdSuffix 'demo'
            buildConfigField "String", "API_URL","\"i am demo\""

        }
        debug{
            buildConfigField "String", "API_URL","\"i am debug\""
        }
    }

這個gradle在編譯之後會產生3個Build.Config文件,可以看到我們定義的API_URL變為了BuildConfig的一個成員變量,然後我們可以在代碼裡直接用BuildConfig.API_URL.為什麼?看BuildConfig的包名,和我們程序包名一致,所以可以直接用。

package com.fish.test;
public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "com.fish.test";
  public static final String BUILD_TYPE = "debug";
  public static final String FLAVOR = "";
  public static final int VERSION_CODE = 1;
  public static final String VERSION_NAME = "1.0";
  // Fields from build type: debug
  public static final String API_URL = "i am debug";
}

android代碼如下


public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        String s = BuildConfig.API_URL;
        TextView tv = (TextView) findViewById(R.id.aa);
        tv.setText(s);
    }
}

可以看到利用buildConfigField簡便優雅的實現了實例2的需求。

經驗總結

跟本地編譯環境相關參數應該放入gradle.properties內 Gradle可以通過ext為對象額外添加屬性或者方法, 由於groovy支持動態類型,所以有時候寫錯了也不會有提示,而且AS內的gradle也無法debug,所以要多用println來打日志 由於AS的terminal比較簡陋,所以我打日志的時候一般會在日志裡填中文,這樣會醒目很多

源碼

Posdevice實例 https://github.com/chefish/Posdevice
實例2 https://github.com/chefish/Version2Asset

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved