Android 开发必备知识:我和 Gradle 有个约会

308 阅读14分钟
原文链接: segmentfault.com

腾讯Bugly特约作者:霍丙乾

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.gpg


projectName=你的构件名称
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,免得坑你的队友。。。


腾讯Bugly

Bugly是腾讯内部产品质量监控平台的外发版本,支持iOS和Android两大主流平台,其主要功能是App发布以后,对用户侧发生的crash以及卡顿现象进行监控并上报,让开发同学可以第一时间了解到app的质量情况,及时修改。目前腾讯内部所有的产品,均在使用其进行线上产品的崩溃监控。

腾讯内部团队4年打磨,目前腾讯内部所有的产品都在使用,基本覆盖了中国市场的移动设备以及网络环境,可靠性有保证。使用Bugly,你就使用了和手机QQ、QQ空间、手机管家相同的质量保障手段