Android 開發必備知識:我和 Gradle 有個約會
0、講個故事
0.1 Ant,我還真以為你是只螞蟻
真正開始近距離接觸編程其實是在2012年,年底的時候帶我的大哥說,咱們這個 app 發布的時候手動構建耗時太久,研究一下 ant 腳本吧。
那個時候連 HashMap 都不知道是啥,可想開發經驗幾乎為零,一個小小的 ant 腳本看得我真是深深地感受到了這個世界充滿的惡意。好在后來硬著頭皮搞明白了什么 target 之類的鬼東西,不然就沒有然后了。
0.2 Maven,你們真的會讀這個單詞么
Maven /`meivn/
接觸 Maven,完全是因為讀陳雄華的《Spring 實戰》,他的源碼居然是用 Maven 構建的,結果 Spring 學得一塌糊涂,Maven我倒是用順手了。。
跟 Ant 一 樣,Maven 可以用來構建 Java 工程;跟 Ant 一樣,Maven 的配置用 xml 來描述;但,Maven 可以管理依賴,它可以讓你做 到“想要什么,就是一句話的事兒”。比如我想要個 gson,Maven 說可以,你記下來我帶會兒構建的時候給你去取。
- <dependency>
- <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId>
- <version>2.4</version>
- </dependency>
真 是讓你當大爺呢。不過,Maven 這家伙學起來有點兒費勁,很多初學的時候在搭建環境的時候就被搞死了——你以為是因為 Maven 的學習曲線陡峭 嗎?當然不是,是因為當初 Maven 的中央倉庫被 x 了,所以你就天天看著 cannot resovle dependencies 玩就好了。
后 來 OSChina 傍上了阿里這個爸爸,就有了 maven.oschina.net。我去年找工作落定之后,想著做點兒什么的時候,發 現 maven.oschina.net 估計被阿里爸爸關禁閉,死了幾天,現在又活過來了。那又怎樣呢,反正中央倉庫被 x 的事情也已經成為過去。
0.3 Gradle,你爹是不是 Google!!
13年的時候,我興奮地跟前面提到的大哥說 Maven 是個好同志的時候,大哥說,Google 推薦用 Gradle。。所以,我想 Gradle,你爹是不是 Google。。或者至少是個干爹吧。
其 實這都不重要了,畢竟 Gradle 實在是好用。比起前面兩位的 xml 配置的手段,直接用代碼的方式上陣必然是靈活得多。不僅如 此,Gradle 居然可以使用 Maven 倉庫來管理依賴,就像是一個簡易版的 Maven 一樣,如果不是看不到 pom 文件,你都還以為你仍然 在使用 Maven(當然,由于你在用 Maven 的倉庫,所以你自然也是離不開 Maven 的)。哦,你是 Ant 用戶啊,那也沒關系啊,不信你 看:
- task helloTAS << {
- ant.echo(message: 'Hello TAS.')
- }
1、用 Gradle 構建
1.1 工程結構
如圖所示,這是一個不能更普通的 android 的 gradle 工程了。
· 根目錄下面的 settings.gradle 當中主要是用來 include 子模塊的,比如我們這個工程有一個叫做 app 的子模塊,那么 settings.gradle 的內容如下:
- include ':app'
·
根目錄下面的 build.gradle 包含一些通用的配置,這些配置可以在各個子模塊當中使用。
·
gradle.properties 文件包含的屬性,會成為 project 的 properties 的成員,例如我們添加了屬性 hello,
·
hello=Hello Tas!
· 然后我們在 build.gradle 當中創建 task:
- task hello << {
- println hello
- println project.getProperties().get("hello")
- }
輸出地結果是一樣的:
- 14:28:11: Executing external task 'hello'...
- Configuration on demand is an incubating feature.
- :app:hello
- Hello Tas!
- Hello Tas!
- BUILD SUCCESSFUL
- Total time: 0.54 secs
- 14:28:12: External task execution finished 'hello'.
· local.properties 這 個文件在 android 工程當中會遇到,我們通常在其中設置 android 的 sdk 和 ndk 路徑。當然,這 個 android studio 會幫我們設置好的。為了更清楚地了解這一點,我把 android 的 gradle 插件的部分源碼摘錄出來:
SDK.groovy,下面的代碼主要包含了加載 sdk、ndk 路徑的操作。
- private void findLocation() {
- if (TEST_SDK_DIR != null) {
- androidSdkDir = TEST_SDK_DIR
- return
- }
- def rootDir = project.rootDir
- def localProperties = new File(rootDir, FN_LOCAL_PROPERTIES)
- if (localProperties.exists()) {
- Properties properties = new Properties()
- localProperties.withInputStream { instr ->
- properties.load(instr)
- }
- def sdkDirProp = properties.getProperty('sdk.dir')
- if (sdkDirProp != null) {
- androidSdkDir = new File(sdkDirProp)
- } else {
- sdkDirProp = properties.getProperty('android.dir')
- if (sdkDirProp != null) {
- androidSdkDir = new File(rootDir, sdkDirProp)
- isPlatformSdk = true
- } else {
- throw new RuntimeException(
- "No sdk.dir property defined in local.properties file.")
- }
- }
- def ndkDirProp = properties.getProperty('ndk.dir')
- if (ndkDirProp != null) {
- androidNdkDir = new File(ndkDirProp)
- }
- } else {
- String envVar = System.getenv("ANDROID_HOME")
- if (envVar != null) {
- androidSdkDir = new File(envVar)
- } else {
- String property = System.getProperty("android.home")
- if (property != null) {
- androidSdkDir = new File(property)
- }
- }
- envVar = System.getenv("ANDROID_NDK_HOME")
- if (envVar != null) {
- androidNdkDir = new File(envVar)
- }
- }
- }
BasePlugin.groovy,通過這兩個方法,我們可以在 gradle 腳本當中獲取 sdk 和 ndk 的路徑
- File getSdkDirectory() {
- return sdk.sdkDirectory
- }
- File getNdkDirectory() {
- return sdk.ndkDirectory
- }
例如:
- task hello << {
- println android.getSdkDirectory()
- }
- 14:37:33: Executing external task 'hello'...
- Configuration on demand is an incubating feature
- .:app:hello
- /Users/benny/Library/Android/sdk
- BUILD SUCCESSFUL
- Total time: 0.782 secs
- 14:37:35: External task execution finished 'hello'.
上面給出的只是最常見的 hierarchy 結構,還有 flat 結構,如下圖1為 flat 結構,2為 hierarchy 結構。有興趣的話可以 Google 一下。
1.2 幾個重要的概念
這一小節的出場順序基本上跟 build.gradle 的順序一致。
1.2.1 Repository和Dependency
如果你只是寫 Android 程序,那么依賴問題可能還不是那么的煩人——如果你用 Java 寫服務端程序,那可就是一把辛酸一把淚了。
倉庫的出現,***的解決了這個問題,我們在開發時只需要知道依賴的 id 和版本,至于它存放在哪里,我不關心;它又依賴了哪些,構建工具都可以在倉庫中幫我們找到并搞定。這一切都是那么自然,要不要來一杯拿鐵,讓代碼構建一會兒?
據 說在 Java 發展史上,涌現出非常多的倉庫,不過***的當然是 Maven 了。Maven 通過 groupId 和 artifactId 來 鎖定構件,再配置好版本,那么 Maven 倉庫就可以最終鎖定一個確定版本的構件供你使用了。比如我們開頭那個例子,
- <dependency>
- <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId>
- <version>2.4</version>
- </dependency>
Maven 就憑這么幾句配置就可以幫你搞定 gson-2.4.jar,不僅如此,它還會按照你的設置幫你把 javadoc 和 source 搞定。媽媽再也不用擔心我看不到構件的源碼了。
那么這個神奇的 Maven 倉庫在哪兒呢? Maven Central,中央倉庫,是 Maven 倉庫的鼻祖,其他的大多數倉庫都會對它進行代理,同時根據需求添加自己的特色庫房。簡單說幾個概念:
·
代理倉庫:要租房,去搜房網啊。你要去駕校報名,我是駕校代理,你找我,我去找駕校。具體到這里,還有點兒不一樣,一旦有人從代理倉庫下載過一次特定得構件,那么這個構件會被代理倉庫緩存起來,以后就不需要找被代理的倉庫下載了。
·
私有倉庫:中國特色社會主義。走自己的路,你管我啊?公司內部的倉庫里面有幾個 hosted 的倉庫,這些倉庫就是我們公司內部特有的,里面的構件也是我們自己內部的同事上傳以后供團隊開發使用的。
·
本地倉庫:大隱隱于市。跟代理倉庫的道理很像,只不過,這個倉庫是存放在你自己的硬盤上的。
·
說起來,Andoid sdk 下面有個 extra 目錄,里面的很多依賴也是以Maven 倉庫的形式組織的。不過這是 Google 特色嘛,人家牛到不往 Maven 的中央倉庫上傳,真是沒轍。
1.2.2 SourceSets
源碼集,這里面主要包含你的各種類型的代碼的路徑,比如 'src/main/java' 等等。
1.2.3 Properties
前面我們其實也稍稍有提到,這個 properties 其實是 gradle 的屬性,在 gradle 源碼當中,我們找到 Project.java 這個接口,可以看到:
- /**
- * <p>Determines if this project has the given property. See <a
- href="#properties">here</a> for details of the
- * properties which are available for a project.</p>
- *
- * @param propertyName The name of the property to locate.
- * @return True if this project has the given property, false otherwise.
- */
- boolean hasProperty(String propertyName);
- /**
- * <p>Returns the properties of this project. See <a href="#properties">here</a> for details of the properties which
- * are available for a project.</p>
- *
- * @return A map from property name to value.
- */
- Map<String, ?> getProperties();
- /**
- * <p>Returns the value of the given property. This method locates a property as follows:</p>
- *
- * <ol>
- *
- * <li>If this project object has a property with the given name, return the value of the property.</li>
- *
- * <li>If this project has an extension with the given name, return the extension.</li>
- *
- * <li>If this project's convention object has a property with the given name, return the value of the
- * property.</li>
- *
- * <li>If this project has an extra property with the given name, return the value of the property.</li>
- *
- * <li>If this project has a task with the given name, return the task.</li>
- *
- * <li>Search up through this project's ancestor projects for a convention property or extra property with the
- * given name.</li>
- *
- * <li>If not found, a {@link MissingPropertyException} is thrown.</li>
- *
- * </ol>
- *
- * @param propertyName The name of the property.
- * @return The value of the property, possibly null.
- * @throws MissingPropertyException When the given property is unknown.
- */Object property(String propertyName) throws MissingPropertyException;
- /**
- * <p>Sets a property of this project. This method searches for a property with the given name in the following
- * locations, and sets the property on the first location where it finds the property.</p>
- *
- * <ol>
- *
- * <li>The project object itself. For example, the <code>rootDir</code> project property.</li>
- *
- * <li>The project's {@link Convention} object. For example, the <code>srcRootName</code> java plugin
- * property.</li>
- *
- * <li>The project's extra properties.</li>
- *
- * </ol>
- *
- * If the property is not found, a {@link groovy.lang.MissingPropertyException} is thrown.
- *
- *@param name The name of the property
- * @param value The value of the property
- */
- void setProperty(String name, Object value) throws MissingPropertyException;
不難知道,properties 其實就是一個 map,我們可以在 gradle.properties 當中定義屬性,也可以通過 gradle 腳本來定義:
- setProperty('hello', 'Hello Tas again!')
使用方法我們前面已經提到,這里就不多說了。
1.2.4 Project和Task
如果你用過 ant,那么 project 基本上類似于 ant 的 project 標簽,task 則類似于 ant 的 target 標簽。我們在 build.gradle 當中編寫的
- task hello << {
- ......
- }
實際上,是調用
- Task Project.task(String name) throws InvalidUserDataException;
創建了一個 task,并通過 << 來定義這個 task 的行為。我們看到 task 還有如下的重載:
- Task task(String name, Closure configureClosure);
所以下面的定義也是合法的:
- task('hello2',{
- println hello
- })
簡單說,project 就是整個構建項目的一個邏輯實體,而 task 就是這個項目的具體任務點。更多地介紹可以參見官網的文檔,和 gradle 的源碼。
2、發布構件
發布構件,還是依賴倉庫,我們仍然以 Maven 倉庫為例,私有倉庫多數采用 sonatype。
2.1 UI 發布
如果管理員給你開了這個權限,你會在 ui 上面看到 upload artifact 的 tab,選擇你要上傳的構件,配置好對應的參數,點擊上傳即可。
2.2 使用 Maven 插件
這里的意思是使用 Maven 的 gradle 插件,在構建的過程中直接上傳。構建好的構件需要簽名,請下載 GPG4WIN (windows),或者 GPGTOOLS(mac),生成自己的 key。
直接上代碼:
gradle.properties
- sonatypeUsername=你的用戶名
- sonatypePassword=你的密碼
- signing.keyId=你的keyid
- signing.password=你的keypass
- #注意,通常來講是這個路徑。
- # mac/linux
- signing.secretKeyRingFile=/Users/你的用戶名/.gnupg/secring.gpg
- # Window XP and earlier (XP/2000/NT)
- # signing.secretKeyRingFile=C:\\Documents and Settings\\<username>\\Application Data\\GnuPG\\secring.gpg
- # Windows Vista and Windows 7
- #
- signing.secretKeyRingFile=C:\\Users\\<username>\\AppData\\Roaming\\gnupg\\secring.
- gpgprojectName=你的構件名稱
- group=你的構件groupid
- artifactId=你的構件artifactid
- # 版本號,采用三位數字的形式,如果是非穩定版本,請務必添加SNAPSHOT
- version=0.0.1-SNAPSHOT
build.gradle
- apply plugin: 'com.android.library'
- apply plugin: 'maven'
- apply plugin: 'signing'
- android {
- compileSdkVersion 21
- buildToolsVersion "21.1.2"
- defaultConfig {
- minSdkVersion 17
- targetSdkVersion 21
- versionCode 1
- versionName "0.2"
- }
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- }
- }
- dependencies {
- compile fileTree(include: ['*.jar'], dir: 'libs')
- ......
- }
- def isSnapshot = version.endsWith('-SNAPSHOT')
- def sonatypeRepositoryUrl
- if(isSnapshot) {
- sonatypeRepositoryUrl =
- "http://maven.oa.com/nexus/content/repositories/thirdparty-snapshots/"
- } else {
- sonatypeRepositoryUrl = "http://maven.oa.com/nexus/content/repositories/thirdparty/"
- }
- sourceSets {
- main {
- java {
- srcDir 'src/main/java'
- }
- }
- }
- task sourcesJar(type: Jar) {
- from sourceSets.main.allSource
- classifier = 'sources'
- }
- artifacts {
- //archives javadocJar
- archives sourcesJar}
- signing {
- if(project.hasProperty('signing.keyId') && project.hasProperty('signing.password') &&
- project.hasProperty('signing.secretKeyRingFile')) {
- sign configurations.archives
- } else {
- println "Signing information missing/incomplete for ${project.name}"
- }
- }
- uploadArchives {
- repositories {
- mavenDeployer {
- if(project.hasProperty('preferedRepo') && project.hasProperty('preferedUsername')
- && project.hasProperty('preferedPassword')) {
- configuration = configurations.archives
- repository(url: preferedRepo) {
- authentication(userName: preferedUsername, password: preferedPassword)
- }
- } else if(project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword')) {
- beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
- repository(url: sonatypeRepositoryUrl) { authentication(userName: sonatypeUsername, password: sonatypePassword)
- }
- } else {
- println "Settings sonatypeUsername/sonatypePassword missing/incomplete for ${project.name}"
- }
- pom.artifactId = artifactId
- pom.project {
- name projectName
- packaging 'aar'
- developers {
- developer {
- id 'wecar'
- name 'wecar'
- }
- }
- }
- }
- }
- }
然后運行 gradle uploadArchives 就可以將打包的 aar 發布到公司的 Maven 倉庫當中了。jar包的方式類似,這里就不在列出了。
2.3 使用 Maven 命令
這個可以通過 mvn 在 cmdline 直接發布構件,命令使用說明:
- mvn deploy:deploy-file -Durl=file://C:\m2-repo \ -DrepositoryId=some.id \
- -Dfile=your-artifact-1.0.jar \
- [-DpomFile=your-pom.xml] \
- [-DgroupId=org.some.group] \
- [-DartifactId=your-artifact] \
- [-Dversion=1.0] \
- [-Dpackaging=jar] \
- [-Dclassifier=test] \
- [-DgeneratePom=true] \
- [-DgeneratePom.description="My Project Description"] \ [-DrepositoryLayout=legacy] \
- [-DuniqueVersion=false]
當然這里仍然有個認證的問題,我們需要首先在 maven 的 settings 配置當中加入:
- <servers>
- <server>
- <id>Maven.oa.com</id>
- <username>rdm</username>
- <password>rdm</password>
- </server>
- </servers>
然后我們就可以使用命令上傳了:
- mvn deploy:deploy-file -DgroupId=com.tencent.test -DartifactId=test
- -Dversion=1.0.0 -Dpackaging=aar -Dfile=test.aar
- -Durl=http://maven.oa.com/nexus/content/repositories/thirdparty
- -DrepositoryId=Maven.oa.com
3、插件
3.1 什么是插件
插件其實就是用來讓我們偷懶的。如果沒有插件,我們想要構建一個 Java 工程,就要自己定義 sourceSets,自己定義 classpath,自己定義構建步驟等等。
簡單地說,插件其實就是一組配置和任務的合集。
gradle 插件的存在形式主要由三種,
· gradle 文件中直接編寫,你可以在你的 build.gradle 當中寫一個插件來直接引入:
- apply plugin: GreetingPlugin
- class GreetingPlugin implements Plugin<Project{
- void apply(Project project) {
- project.task('hello') << {
- println "Hello from the GreetingPlugin"
- }
- }
- }
·
buildSrc工程,這個就是在你的工程根目錄下面有一個標準的 Groovy 插件工程,目錄是 buildSrc,你可以直接引用其中編寫的插件。
·
獨立的工程,從結構上跟 buildSrc 工程是一樣的,只不過這種需要通過發布到倉庫的形式引用。通常我們接觸的插件都是這種形式。
·
詳細可以參考:Chapter 61. Writing Custom Plugins
3.2 常見的插件
目前接觸到的插件,有下面這么幾種:
· java,構建 java 工程
· war,發布 war 包用,構建 web 工程會用到
· groovy,構建 groovy 工程
· com.android.application,構建 Android app 工程
· com.android.library,構建 Android library,通常輸出 aar
· sign,簽名
· maven,發布到 maven 倉庫
· org.jetbrains.intellij,構建 intellij 插件工程
3.3 自己動手寫一個插件
創建一個普通的 groovy 工程(java 工程也沒有關系),創建 src/main/groovy 目錄,編寫下面的代碼:
- package com.tencent.wecar.plugin
- import org.gradle.api.Plugin
- import org.gradle.api.internal.project.ProjectInternal
- class GreetingPlugin implements Plugin<ProjectInternal> {
- void apply(ProjectInternal project) {
- project.task('hello') << {
- println 'hello'
- }
- }
- }
在 src/main/resources 創建 META-INF/gradle-plugins 目錄,創建 greetings.properties 文件:
- implementation-class=com.tencent.wecar.plugin.GreetingPlugin
其中 greettings 就是你的插件 id。
build.gradle
- group 'com.tencent.wecar.plugin'
- version '1.1-SNAPSHOT'
- buildscript {
- repositories {
- mavenLocal()
- }
- }
- apply plugin: 'groovy'
- apply plugin: 'java'
- repositories {
- mavenCentral()
- }
- sourceSets {
- main {
- groovy {
- srcDirs = [
- 'src/main/groovy',
- 'src/main/java'
- ]
- } // compile everything in src/ with groovy
- java { srcDirs = []}// no source dirs for the java compiler
- }
- }
- dependencies {
- //tasks.withType(Compile) { options.encoding = "UTF-8" }
- compile gradleApi()
- }
- // custom tasks for creating source jars
- task sourcesJar(type: Jar, dependsOn:classes) {
- classifier = 'sources'
- from sourceSets.main.allSource
- }
- // add source jar tasks as artifacts
- artifacts { archives sourcesJar }
- // upload to local
- uploadArchives {
- repositories{
- mavenLocal()
- }
- }
運行 uploadArchives 發布到本地倉庫,那么就可以找到我們自己的插件了,由于當中沒有指定 artifactId,那么我們的插件的 artifactId 就是我們的工程名稱,比如這里是 deployplugin。
那么我們要怎么引入這個插件呢?
首先要再 buildScript 增加依賴:
- buildscript {
- repositories {
- mavenLocal()
- }
- dependencies {
- classpath 'com.tencent.wecar.plugin:deployplugin:1.1-SNAPSHOT'
- }
- }
然后:
- apply plugin: 'greetings'
這樣我們的 task “hello” 就被引入了。
4、Gradle 運行慢?
用過 Gradle 的朋友多少會感覺到這貨有時候會比較慢。我們可以通過下面的三個手段加速你的 Gradle。
· 不用中央倉庫。如果你的 repository 配置的是 mavenCentral,放開它吧,全世界的人都在琢磨著怎么虐它,你就不要瞎摻和了。試試 jCenter。
· 升級***的 Gradle 版本。目前***的版本是2.4,Android Studio 從1.3開始默認使用 Gradle2.4
· 開啟Gradle的電動小馬達。在 gradle.properties(眼熟?沒錯,就是它!!)
里面添加下面的配置:
如果你的任務沒有時序要求,那么打開這個選項可以并發處理多個任務,充分利用硬件資源。。嗯,如果你的是單核 CPU。。當我沒說。。 org.gradle.parallel=true 這個也可以在命令行通過參數的形式啟動,3個小時有效。守護進程可以使編譯時間大大縮短 org.gradle.daemon=true 這個看需求吧,Gradle 是運行在 Java 虛擬機上的,這個指定了這個虛擬機的堆內存初始化為256M,***為1G。如果你內存只有2G,那當我沒說。。 org.gradle.jvmargs=-Xms256m -Xmx1024m
當然,建議的方式是在你的用戶目錄下面的 .gradle/ 下面創建一個 gradle.properties,免得坑你的隊友。。。
想了解更多干貨,請搜索關注公眾號:騰訊Bulgy,或搜索微信號:weixinBugly,關注我們
騰訊Bugly
Bugly是騰訊內部產品質量監控平臺的外發版本,支持iOS和Android兩大主流平臺,其主要功能是App發布以后,對用戶側發生的crash以及卡頓現象進行監控并上報,讓開發同學可以***時間了解到app的質量情況,及時修改。目前騰訊內部所有的產品,均在使用其進行線上產品的崩潰監控。
騰訊內部團隊4年打磨,目前騰訊內部所有的產品都在使用,基本覆蓋了中國市場的移動設備以及網絡環境,可靠性有保證。使用Bugly,你就使用了和手機QQ、QQ空間、手機管家相同的質量保障手段