編輯:關於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
Android實現水波紋效果
一、效果 點擊開始: 點擊停止: 二、在MainActivity中import android.graphics.Paint;import and
android Gallery組件實現的iPhone圖片滑動效果實例
實現的效果圖,可左右滑動:一、先在將Gallery標簽放入:復制代碼 代碼如下:<?xml version=1.0 encoding=utf-8?&
Android JNI/NDK開發之基本姿勢(一)
開發環境信息列舉下本篇文章編寫的Demo基本信息 操作系統 Windows 10 家庭中文版 開發工具 Android Studio 2.1 SDK n
Android筆記十一.ListView+Adapter
深入理解Adapter 一、ListView ListView是Android開發過程中較為常見的組件之一,它將數據以列表的形式展現出來。一般而言,一個ListView由