編輯:關於Android編程
最近在學習gradle,innost的這篇文章可以說是目前中文說gradle最好的文章
深入理解 Android 之 Gradle.文章名字雖然叫深入理解,但是其實講的也不深,不過比其他的說腳本怎麼配置的文章好太多了,讀完之後收貨頗多,在這裡記錄重點,並且把他文中的demo進行實現改進(作者未提供源碼),算是對原文的一個總結和補充(源碼在文末)。
Gradle 是一個框架,負責定義流程和規則,而具體的構建工作則是通過插件的方式來完成的,比如編譯 Java 有 Java 插件,編譯 Groovy 有 Groovy 插件,編譯 Android APP 有 Android APP 插件,編譯 Android Library 有 Android Library 插件。我們可以通過apply plugin:'XXX'來導入插件。
Gradle 主要有三種對象,這三種對象和三種不同的腳本文件對應,在 gradle 執行的時候,會將腳本轉換成對應的對端:
Gradle 對象:當我們執行 gradle xxx 或者什麼的時候,gradle 會從默認的配置腳本中構造出一個 Gradle 對象。在整個執行過程中,只有這麼一個對象。Gradle 對象的數據類型就是 Gradle。我們一般很少去定制這個默認的配置腳本。 Project 對象:每一個 build.gradle 會轉換成一個 Project 對象。 Settings 對象:每一個 settings.gradle 都會轉換成一個 Settings 對象。
Gradle工作包含三個階段:
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
本文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。
需求定了就可以撸起來了。很容易的,我們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
代碼如下,首先在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裡定義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,順利通過
在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的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() }
拷貝這件事情應該發生在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) } }
這種方法是寫一個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') }
我們每次./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
首先第一個需求加一個demo包,非常簡單在buildtype那裡加就ok了。主要看第二個需求,要不同的buildtype編譯出來的apk能夠知道自己是屬於哪個buildtype的,這裡實際上是通過gradle代碼影響了工程代碼。一般來說工程代碼和構建腳本是相互獨立的,要如何才能影響到工程的代碼呢?我們可以在構建的時候把當前的buildtype寫到某個文件,然後在apk的代碼裡去讀取這個文件。ok,lets do it!
首先實現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 } }
根據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。
我們在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的需求。
Posdevice實例 https://github.com/chefish/Posdevice
實例2 https://github.com/chefish/Version2Asset
一、效果 點擊開始: 點擊停止: 二、在MainActivity中import android.graphics.Paint;import and
實現的效果圖,可左右滑動:一、先在將Gallery標簽放入:復制代碼 代碼如下:<?xml version=1.0 encoding=utf-8?&
開發環境信息列舉下本篇文章編寫的Demo基本信息 操作系統 Windows 10 家庭中文版 開發工具 Android Studio 2.1 SDK n
深入理解Adapter 一、ListView ListView是Android開發過程中較為常見的組件之一,它將數據以列表的形式展現出來。一般而言,一個ListView由