android studio 编译高级篇-gradle多版本编译,定制任务

导读

本文旨在介绍Gradle构建的一些高级功能,包含了多版本编译、定制任务等功能:

  1. 为相同的app构建多个版本
  2. 如果在Gradle的过程中添加定制的任务
  3. 如何使用android库module

3.构建类型

3.1 Build Types的使用

当你想自定义debug和release的Build Types的时候,我们需要对Build Types进行修改,那么如何更改Build Types呢?

Build Types决定了如何去打包一个app。默认情况下,Android的Gradle插件支持两种不同的类型:debug和release。两种都是可以在BuildTypes标签中进行定义的,如下所示:

android {
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

上面代码中只写了release构建,增加debug模块构建也是非常简单的。每一个构建标签都包含了一些列的属性。完成的属性和方法可在DSL参考中查看,DSL reference for the com.android.build.gradle.internal.dsl.BuildType。例如属性minifyEnabled可以在打包的时候去除掉无用的代码。shrinkResources可以在打包的时候去除掉无用的资源。

android {
    buildTypes {
    release {
            // 是否对代码混淆、压缩
            minifyEnabled false
            // 是否对资源压缩
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

在build types中,其它有效的属性还有debuggable。Debug标签默认将debuggable属性置为true,其它构建方法默认为false。

当你想在一台设备上安装多个构建成果,譬如想同时安装debug和release包,该如何完成呢?Android本身必须区分应用的包名(application ID)。在Gradle编译上使用属性applicationIDsuffix可以生成多个APKs,每一个都拥有唯一的ID,通过使用applicationIDsuffix函数将applicationID末尾加入.debug,代码如下所示:

android {
    // ... other properties ...
    buildTypes {
        debug {
            applicationIDsuffix '.debug'
            versionNameSuffix '-debug'
        }
        // .. other build types ...
    }
}

现在我们可以将release和debug包同时安装在一个相同设备上了。查看系统设置,查看对应的app,两个app具有相同的名字和版本。

Both debug and release versions are deployed

如何区分它们呢?选择任一个版本查看相应的详细信息,会发现版本名称后加了-debug,这个是由versionNameSuffix属性来添加的。

Version name in App info settings

为了更进一步论证,你可以查看在module目录下查看生成的buildConfig.class文件路径为/build/intermediates/classes/debug/com/../buildConfig.class,可以看到APPLICATION_ID属性后面增加了.debug,VERSION_NAME后加了-debug

3.2 产品的多样化(Flavors)和变体(Variants)

怎么理解多样化(Flavors)和变体(Variants)呢?这个也是笔者比较困惑的问题,一下提出了两个概念。使用多样化(Flavors)和变体(Variants)主要是为了实现相同的应用程序使用不同的资源或类。

Build types作为开发中的一部分,通常被当作app从开发转入生产的过程。默认情况下build types产生debug和release两种包。多样化(Flavors)允许你构建相同app的多个版本,这些版本的app都可以安装在相同设备上。例如我们需要自定义一个app在外观和感觉上能够满足多个客户或者我们需要一个付费和免费版本相同的app。

声明一个产品的多样化,使用productFlavors来完成。

考虑一个“Hello, World”的Android app,让用户能够通过EditText输入相应的名字。我们可以给出的app “friendly,”“arrogant,”和“obsequious”(名称自定义)多样化。如下所示:

android {
    productFlavors {
        arrogant {
            applicationId 'com.oreilly.helloworld.arrg'
        }
        friendly {
            applicationId 'com.oreilly.helloworld.frnd'
        }
        obsequious {
            applicationId 'com.oreilly.helloworld.obsq'
        }
    }
}

通过上面构建脚本加入,我们可以生成不同applicationId,这样三个应用都可以被安装在同一台设备上。产品多样化可以定义特有属性值,这些都是基于相同属性来自defaultConfig:

  • applicationId
  • minSdkVersion
  • targetSdkVersion
  • versionCode
  • versionName
  • signingConfig

参考arrogant的buildConfig内容如下:

[--> buildConfig.class]

public static final boolean DEBUG = Boolean.parseBoolean("true");
public static final String APPLICATION_ID = "com.oreilly.helloworld.arrg";
public static final String BUILD_TYPE = "debug";
public static final String FLAVOR = "arrogant";
public static final int VERSION_CODE = 1;
public static final String VERSION_NAME = "1.0";

此时每一个多样化产品都拥有了自己的一个applicationId,但是这并不能满足我们订制化资源或代码的功能,只能满足多个相同应用安装在同一个设备上。然而,多样化(flavor)applicationId都可以定义自己的源码路径,也就一意味着能像app/src/main/java一样增加源码目录:

  • app/src/arrogant/java
  • app/src/friendly/java
  • app/src/obsequious/java

你也可以增加额外的资源目录:

  • app/src/arrogant/res
  • app/src/arrogant/res/layout
  • app/src/arrogant/res/values

像其它子工程res一样。相同的资源结构也会应用在所有的多样化应用中,通过Gradle工具将相同的资源进程合并,实现多样化产品的多样性,也就生成了变体

Product flavors with source code and resources

如上图所示,app module中包含了arrogant, friendly, obsequious,配合默认的buildTypes所支持的debug和release总共会生成6个apk。其中arrogant, friendly, obsequious作为变体。如何查看变体名称呢?我们定制一个任务查看,如下所示:

task printVariantNames() {
    doLast {
        android.applicationVariants.all { variant ->
            println variant.name
        }
    }
}

查看输出结果:

> ./gradlew printVariantNames
:app:printVariantNames
obsequiousDebug
obsequiousRelease
arrogantDebug
arrogantRelease
friendlyDebug
friendlyRelease

BUILD SUCCESSFUL

部署一个特定的变体,Android Studio提供了一个变体选择框。通过下拉列表选择需要生成的变体产物,如图所示。

uild Variants view in Android Studio

当我们使用产品多样化的时候,使用assemble任务会生成所有的变体。使用assemble<Variant>任务会只生产相应的变体。

3.3 资源合并

当你想在多样化编译中修改相应的图片、文本或者其它的资源该如何来做?将对应的资源添加到变体目录下,如何改变相应的值呢?

我们在上一小节中讲到“Hello World”应用,定义了三个多样化产品,分别是arrogant, friendly, obsequious。每个Java代码是相同的,但是每一个变体又是不同的,我们在Gradle构建脚本中的定义为:

android {
    // ... other settings ...
    productFlavors {
        arrogant {
            applicationId 'com.oreilly.helloworld.arrg'
        }
        friendly {
            applicationId 'com.oreilly.helloworld.frnd'
        }
        obsequious {
            applicationId 'com.oreilly.helloworld.obsq'
        }
    }
}

每一个变体都有独立的applicationId,这样他们才能够被安装在相同设备上。这里先看一下MainActivity类的OnCreate方法。

public class MainActivity extends AppCompatActivity {
    private EditText editText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        editText = (EditText) findViewById(R.id.name_edit_text);
    }

    public void sayHello(View view) {
        String name = editText.getText().toString();
        Intent intent = new Intent(this, WelcomeActivity.class);
        intent.putExtra("user", name);
        startActivity(intent);
    }
}

activity使用了组件EditText,提供给用户输入姓名。sayHello方法取出相应的名字,添加到Intent的extra中,并通过Intent传递给WelcomeActivity。

MainActivity的视图很简单,只有一个垂直分布的LinearLayout,里面放入一个TextView,一个EditText和一个Button。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">
    <TextView
        android:id="@+id/name_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world"/>
    <EditText
        android:id="@+id/name_edit_text"
        android:hint="@string/name_hint"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
    <Button
        android:onClick="sayHello"
        android:text="@string/hello_button_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</LinearLayout>

将MainActivity运行,结果如下所示: Hello screen in the Arrogant flavor

每一个变体都有自己的资源目录,在app/<flavor>/res目录结构下。在不同情况下,子文件夹增加values,并拷贝strings.xml文件到app/src/main/res/values目录中。其中arrogant变体内容如下所示:

<resources>
    <string name="app_name">Arrogant</string>
    <string name="title_activity_welcome">His/Her Royal Highness</string>
    <string name="hello_world">Arrogant</string>
    <string name="greeting">We condescend to acknoweldge your
        presence, if just barely, %1$s.</string>
</resources>

Project view showing Arrogant flavor directories

通过和工程目录合并res文件夹,将相同名称的文件进行合并,实现资源合并。优先级为:build type中的变体覆盖产品,这是主要的覆盖原则。

这里来看一下WelcomeActivity的onCreate方法,读取用户名称,并问候用户。

public class WelcomeActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_welcome);
        String name = getIntent().getStringExtra("user");
        TextView greetingText = (TextView) findViewById(R.id.greeting_text);
        String format = getString(R.string.greeting);
        greetingText.setText(String.format(format, name));
    }
}

WelcomeActivity的视图包含了一个TextView,TextView中底部放入了一张图片,布局如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.oreilly.helloworld.WelcomeActivity">
    <TextView
        android:id="@+id/greeting_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world"
        android:textSize="24sp"
        android:drawableBottom="@drawable/animal"
        />
</LinearLayout>

每一个变体都有values.xml和animal.png文件,用于改变问候方式。我们来看一下arrogant变体的样式: Welcome in the Arrogant flavor

这里来看一下friendly变体使用的string.xml,如下所示:

<resources>
    <string name="app_name">Friendly</string>
    <string name="title_activity_welcome">We are BFFs!</string>
    <string name="hello_world">Friendly</string>
    <string name="greeting">Hi there, %1$s!</string>
</resources>

运行出来的效果如下所示:

Welcome in the friendly flavor

这里来看一下Obsequious变体使用的string.xml,如下所示:

<resources>
    <string name="app_name">Obsequious</string>
    <string name="hello_world">Obsequious</string>
    <string name="title_activity_welcome">your humble servant</string>
    <string name="greeting">O great %1$s, please accept this pathetic
        greeting from my unworthy self. I grovel in your
        general direction.</string>
</resources>

运行出来的效果如下所示: Welcome in the Obsequious flavor

合并非Java资源是非常容易的。仅需要增加适当的文件夹和文件,这些值会覆盖到main目录中相同的文件夹或是文件。然后可以通过android Studio选择相应变体,安装app到设备中。 Build Variants view in Android Studio

3.4 变体规格(Dimensions)

仅有产品的变体也是不够的。你需要为不同的app版本指定不同的标准规格该如何实现呢?在变体规格中使用flavorDimensions实现。

在前面我们提出了“Hello, World” app,提出了三个变体arrogant, friendly和obsequious。不同的变体我们从感官和applicationID上可以很好的区分。

猜想,如果不同的客户希望变体app带有自己的产品品牌,其实这很简单,我们可以在变体目录中去实现,这个上节中很简单的就可以实现了,但是为什么要提出规格这个概念呢?设想这么一种场景,我们需要出3个变体,为四家公司,每个公司要有自己的logo或是规格,也就意味着我们要出12个release包,此时我们如何去实现这种需要呢?

flavorDimensions 'attitude', 'client'

productFlavors{
    arrogant{
        dimension'attitude'
        applicationId'com.oreilly.helloworld.arrg'
    }
    friendly{
        dimension'attitude'
        applicationId'com.oreilly.helloworld.frnd'
    }
    obsequious{
        dimension'attitude'
        applicationId'com.oreilly.helloworld.obsq'
    }
    stark{
        dimension'client'
    }
    wayne{
        dimension'client'
    }
}

现在有两个规格的变体:attitude和client。arrogant,friendly和obsequious变体都在attitude规格中,stark和wayne规格为client。

组合之后会生成更多的变体。执行printVariantNames任务,可以看到:

./gradlew printVariantNames
:app:printVariantNames
obsequiousStarkDebug
obsequiousStarkRelease
obsequiousWayneDebug
obsequiousWayneRelease
arrogantStarkDebug
arrogantStarkRelease
arrogantWayneDebug
arrogantWayneRelease
friendlyStarkDebug
friendlyStarkRelease
friendlyWayneDebug
friendlyWayneRelease

BUILD SUCCESSFUL

默认情况下,Gradle插件支持两种基础类型Debug和Release,这里包含三个变体和两个规格,共计232 = 12个变体。我们来让变体做一些实际可以感知的事情。

如colors.xml文件在stark规格中,目录为stark/res/values,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="text_color">#beba46</color>
    <color name="background_color">#771414</color>
</resources>

相应的colors.xml文件在wayne规格中,目录为wayne/res/values,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="text_color">#beba46</color>
    <color name="background_color">#771414</color>
</resources>

Directory trees for the client flavors

strings.xml文件在每个规格中如下所示:

[--> stark/res/values/strings.xml]

<resources>
    <string name="hello_world">Stark Industries</string>
</resources>

[--> wayne/res/values/strings.xml]

<resources>
    <string name="hello_world">Wayne Enterprises</string>
</resources>

最后activity_main.xml中的TextView使用colors和strings修改。

<TextView
    android:id="@+id/name_text_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textColor="@color/text_color"
    android:background="@color/background_color"
    android:textSize="32sp"
    android:text="@string/hello_world" />

textColor属性使用每个变体中color资源,text属性使用每个变体中的string值提供。下面来看一下,arrogant变体,Stark规格:

The Arrogant debug flavor from Stark Industries

我们再来看一下,friendly变体,Wayne规格:

 The Arrogant debug flavor from Stark Industries

有一点是我们需要注意的。flavorDimensions标签在Gradle构建脚本中,attitude在client之前,就意味着attitude规格的优先级高于client规格。规格的合并原则为,本地有则用本地,没有则需要合并,如果出现资源缺少问题,编译是时候会提示,如果采用反射的方式查找,这个就不能保证了。

3.5 Java源代码的合并应用于变体

当你希望Android activities或是Java类使用变体,该如何操作呢?创建适当的资源目录,添加java类并将这些类合并到main源码目录下。

前面我们知道,string, layout资源在变体中会覆盖main资源中的相同内容,但是Java类是不一样的。当你的main代码中涉及一个特殊类,只要这个类没在main中,每一个变体和build type需要实现这个类。

这听起来还是很复杂,我们还是使用“Hello, World” app讨论这个问题。这里增加一个按钮叫call for help。我们来看一下变体friendly,wayne规格:

Main activity for the “wayne” client

变体friendly,stark规格,这里只是header不同和颜色不同,如下:

Main activity for the “stark” client

点击按钮call for help,启动CallForHelpActivity。这个activity已经不再main目录下了,已经拷贝到stark和wayne规格目录结构下。具体如下所示:

Source folders for main, stark, and wayne flavors

我们可以看出,CallForHelpActivity分别在stark和wayne规格目录结构下。两种变体都去实现了CallForHelpActivity,而且也是完全不同的。

对于wayne规格,展示如下所示:

 Help activity for wayne flavor

对于stark规格,展示如下所示:

Help activity for stark flavor

在main代码中一些存在引用关系的元素,变体目录中必须存在,这样Gradle才能独立的去实现这部分代码。

4.定制任务

4.1编写定制任务

如何来定制我们的构建任务呢?本节将介绍如何增加task到Gradle构建文件中,使用Android插件支持任务自定义,使得任务开发变得更容易。

Gradle DSL支持定制任务模块。其中已经API中已经存在任务包括(Copy, Wrapper, 和Exec),对于这些任务,你仅需要设置其属性即可。例如Copy任务,包含了:frominto属性,其中的from模块可以配置路径位置,也可以排除指定文件名。通过Copy任务能够拷贝所有的APK到一个新目录,其中不包含unsigned或unaligned文件,如下所示:

task copyApks(type: Copy) {
    from("$buildDir/outputs/apk") {
        exclude '**/*unsigned.apk', '**/*unaligned.apk'
    }
    into '../apks'
}

其中的buildDir属性引用默认的构建目录(app/build/),其中美元符号表示对变量的引用,整体字符串加上双引号。上面代码中使用了Copy任务,其中使用了eclude标签,其中**表示匹配所有子目录。

如果你不希望简单的使用已经存在的Gradle任务,而是希望任务的完全自定义,你需要明确区分configuration和execution在Gradle中的地位。在configuration过程中,Gradle根据具体的依赖关系构建DAG视图。在execution中,根据已经构建的DAG视图进行执行任务。所有的任务都需要先配置后执行。

Gradle建议声明任务,如下所示。在哪里指定你想做的,而不是不是如何来做。如果你需要执行命令,指明如何来做,在doLast标签中加入Gradle任务。

task printVariantNames() {
    doLast {
        android.applicationVariants.all { variant ->
            println variant.name
        }
    }
}

在doLast标签之前和之后的部分都会在配置阶段被执行。但是doLast标签中的内容在执行阶段运行。

Android插件增加了android属性,如applicationVariants属性返回返回所有buildType/flavor序列。安装所有的debug包到一个设备上,使用如下task:

task installDebugFlavors() {
    android.applicationVariants.all { v ->
        if (v.name.endsWith('Debug')) {
            String name = v.name.capitalize()
            dependsOn "install$name"
        }
    }
}

如上代码,dependsOn方法显示这是配置阶段而不是执行阶段。每一个variant名称,像friendlyDebug和安装任务(installFriendlyDebug)作为installDebugFlavors任务的依赖。

在配置过程中,installArrogantDebug,installFriendlyDebug和 installObsequiousDebug被添加到installDebugFlavors中作为依赖。因此,执行installDebugFlavors需要执行3个任务。

./gradlew instDebFl
:app:preBuild UP-TO-DATE
:app:preArrogantDebugBuild UP-TO-DATE
:app:checkArrogantDebugManifest
// ... lots of tasks ...
:app:assembleArrogantDebug UP-TO-DATE
:app:installArrogantDebug
Installing APK 'app-arrogant-debug.apk' on 'Nexus_5_API_23(AVD) - 6.0'
Installed on 1 device.
:app:checkFriendlyDebugManifest
// ... lots of tasks ...
:app:assembleFriendlyDebug UP-TO-DATE
:app:installFriendlyDebug
Installing APK 'app-friendly-debug.apk' on 'Nexus_5_API_23(AVD) - 6.0'
Installed on 1 device.
:app:checkObsequiousDebugManifest
// ... lots of tasks ...
:app:assembleObsequiousDebug UP-TO-DATE
:app:installObsequiousDebug
Installing APK 'app-obsequious-debug.apk' on 'Nexus_5_API_23(AVD) - 6.0'
Installed on 1 device.
:app:installDebugFlavors

BUILD SUCCESSFUL

目前可以知道,编写定制任务需要一些Groovy知识。

4.2添加定制任务到编译过程

当你想将自己编写的任务添加到编译过程,作为编译过程的一部分,我们需要使用dependOn属性。

在初始化的过程中,Gradle根据依赖情况组装任务成一个序列。组装后的结果就是DAG。下面我们来看一个Gradle文档中Java插件的任务序列图(DAG)。

Directed acyclic graph for the Java plug-in tasks

当前任务序列图(DAG)展示了任务之间的依赖关系,同时任务之间是没有环状的。添加你的定制任务就意味着在某个任务位置插入定制任务。

如当前copyApks任务,copyApks任务是用来将所有生成的apk文件拷贝到一个独立的文件夹,如下所示:

task copyApks(type: Copy) {
    from("$buildDir/outputs/apk") {
        exclude '**/*unsigned.apk', '**/*unaligned.apk'
    }
    into '../apks'
}

copyApks内容如上所示,细心的你也许会想到上述的写法有些不足,当apk文件没有生成的时候,这个任务没有作用。我们知道assemble任务生成apk文件,所以使copyApks任务依赖assemble任务,如下所示:

task copyApks(type: Copy, dependsOn: assembleDebug) {
    from("$buildDir/outputs/apk") {
        exclude '**/*unsigned.apk', '**/*unaligned.apk'
    }
    into '../apks'
}

通过依赖assembleDebug意味着所有debug apk文件在copyApks任务之前会生成。也可以使用assemble任务替代assembleDebug任务,生成相应的APK包。

如果想copyApks任务在每一次build的时候都执行,需要build任务去依赖当前的copyApks任务,具体写法如下所示。

build.dependsOn copyApks

现在运行build任务,也会拷贝apk文件到一个独立文件夹。我们可以通过正确的依赖将copyApks任务插入到DAG中。

移除生成的apk文件夹和所有apk文件也可以做一个定制任务,在顶层的Gradle的构建文件中有一个clean任务,我们也可以对其进行修改:

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

delete任务负责删除所有的文件夹,所有在当前目录结构下的文件和文件夹列表都将被删除,我们很多时候只是希望删除某一个文件夹或某几个文件夹,这也很容易修改,如下所示:

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

使用该机制,许多定制任务能够插入到构建过程中。

4.3执行任务

Gradle构建过程中涉及到大量的任务顺序执行。大部分任务依赖于其他任务,但是如果我们希望更快速编译,一些任务也是不需要的。例如lint任务,任务是非常有用的,但是不是每次都必须执行的。

调用-x标志(--exclude-task简写)去掉不希望执行的任务。因此当执行构建的时候,会根据该标志跳过相应的lint任务,或者一些你想跳过的任务。

> ./gradlew build -x lint

被排除的lint任务也有一些依赖,这些以来的任务也将不会被执行,所以要明确你排除的任务后续不会在构建过程中被依赖或是使用到。

唯一的问题是当你的项目中有多个变种,每一个项目都有一个lint任务。原则上,你可以将它们全部排除在外,但你可能更倾向于排除整个集合的一部分,作为构建的一部分。

当Gradle运行,首先要构建DAG视图。从中可以查看gradle项目的依赖关系,任何操作都需要在DAG视图形成之后执行,所以你可以使用whenReady属性应用一些修改。

gradle.taskGraph.whenReady { graph ->
    graph.allTasks.findAll { it.name ==~ /lint.*/ }*.enabled = false
}

allTasks属性是调用getAllTasks方法,使用Groovy语法,返回java.util.List结构的任务列表。Groovy增加了findAll方法,从List中查找匹配当前条件的任务。在这种情况下,闭包表示访问每个任务的名称属性,并检查它是否完全匹配正则表达式。将“扩展点”运算符应用到结果列表中禁用列表中的每个符合条件的任务。

查询结果是将所有包含lint名称并有enable属性的任务设置位false,这样所有这些任务都将不再执行。

也许你并非想禁止所有的lint任务,可以通过条件判断来设置部分不执行lint,如下所示:

gradle.taskGraph.whenReady { graph ->
    if (project.hasProperty('noLint')) {
        graph.allTasks.findAll { it.name ==~ /lint.*/ }*.enabled = false
    }
}

我们可以通过-P标志来设置工程属性:

> ./gradlew build -PnoLint | grep lint
:app:lintVitalArrogantRelease SKIPPED
:app:lintVitalFriendlyRelease SKIPPED
:app:lintVitalObsequiousRelease SKIPPED
:app:lint SKIPPED

显然,这里需要一些Groovy知识,但是能够想到使用任务图操作构建的想法是非常棒的。

4.4定制源码设置

当我们想将源码路径变得非标准,该如何做呢?使用sourceSets可以实现。

当我们有多个源码文件夹,并非遵从标准的源码路径,因为开发或是分离的原因很难完全匹配标准结构,这时需要修改Gradle构建文件,如下所示:

// The sample build uses multiple directories to
// keep boilerplate and common code separate from
// the main sample code.
List<String> dirs = [
    'main', // main sample code; look here for the interesting stuff.
    'common', // components that are reused by multiple samples
    'template'] // boilerplate code that is generated by the sample template process

android {
    // ... code omitted ...
    sourceSets {
        main {
            dirs.each { dir ->
                java.srcDirs "src/${dir}/java"
                res.srcDirs "src/${dir}/res"
            }
        }
        androidTest.setRoot('tests')
        androidTest.java.srcDirs = ['tests/src']
    }
}

使用List<String> dirs表示源码目录。Groovy支持列表的本地语法,使用方括号表示,逗号分够。当前main、common、template作为源码路径。

在android标签下,使用sourceSets属性添加源码目录。查看main标签下内容,遍历源码路径,如下所示:

dirs.each { dir ->
    java.srcDirs "src/${dir}/java"
    res.srcDirs "src/${dir}/res"
}

each方法来自Groovy。它会遍历集合里的每一个元素,并把参数传递到闭包中作为参数。

标准的android Studio工程中,源码位于src/main/java中,资源位于src/main/res中。在这种情况下,增加目录到srcDirs属性中,目录为src/main/java,src/common/java和src/template/java都会被添加到编译路径中。src/main/res, src/common/res, and src/template/res会作为资源文件夹。

比较讽刺的是,Android Studio任何一个例子都没有引用外部的源码文件夹。所有的java源码都在src/main/java中,资源位于src/main/res中。事实上,没有例子使用自定义结构,都是使用的标准结构放入了源码和资源。这种结构估计是对未来的考虑吧,或是保留一些时间也可能会去掉,也说明了Google工程师还是很幽默的。

4.5使用android库文件

本节介绍为Android工程增加外部库module依赖。你可以通过java库为app增加许多功能,这些都以jar文件格式存在。在上一篇文章中深入理解gradle编译-Android基础篇,我们已经说到了怎样使用dependencies标签。例如使用Google的Gson库文件解析JSON数据,在构建文件中build.gradle中增加相应的module依赖,如下:

dependencies {
    compile 'com.google.code.gson:gson:2.6.2'
}

Android库文件比java库更丰富,可以说是Android库集合包含java库,包含了类文件,Android API和其它需要的资源。当工程构建的的时候,Gradle组装(assembles)Android库工程为aar(Android Archive)文件,像jar文件一样,但是包含Android依赖。

在Gradle上,Android库作为根目录下的子工程。也就意味着它们像Android应用程序一样,但是在子目录中。这些库工程作为module添加到settings.gradle文件中。

include ':app', ':icndb'

此时可以看到,Android库module名称为icndb,表示了Internet Chuck Norris Database,依赖了Chuck Norris jokes的JSON响应格式。这个API的主页如下所示:

The API page for the ICNDB site

一个Android库的一个例子,网站将作为一个RESTful Web服务访问,返回的JSON数据将被解析,以及由此产生的内容将被添加到一个Activity的TextView中。

首先创建一个Android Studio module,使用“New Module”向导,选择“Android Library”类型,如下所示:

The Android Library option in the New Module wizard

给出相应的库文件名称,你可以添加你想要的任何类型的Activity。完成创建库module向导后,会将库名称添加到根目录的settings.grade文件中。

每一个库module都有自己的Gradle构建文件,使用了根目录相同的设置。你可以指定最小和最大SDK版本,定制编译类型,增加多样化,修改依赖等等。最大的区别在于使用了不同的构建插件,如下所示:

// 使用library插件
apply plugin: 'com.android.library'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"
    // 使用packagingOptions标签,取出一些可能与其它工程存在冲突的文件
    packagingOptions {
        exclude 'META-INF/notice.txt'
        exclude 'META-INF/license.txt'
        exclude 'LICENSE.txt'
    }
    defaultConfig {
        minSdkVersion 16
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                    'proguard-rules.pro'
        }
    }
}
dependencies {
    compile 'com.google.code.gson:gson:2.6.2'
    compile 'com.squareup.retrofit2:retrofit:2.0.1'
    compile 'com.squareup.retrofit2:converter-gson:2.0.1'
}

如果使用了这个库module,实现ICNDB库变得非常简单:

public class JokeFinder {
    private TextView jokeView;
    private Retrofit retrofit;
    private AsyncTask<String, Void, String> task;

    // 实现GET请求
    public interface ICNDB {
        @GET("/jokes/random")
        Call<IcndbJoke> getJoke(@Query("firstName") String firstName,
                                @Query("lastName") String lastName,
                                @Query("limitTo") String limitTo);
    }

    public JokeFinder() {
        // 实例化Retrofit,做Gson转换
        retrofit = new Retrofit.Builder()
                .baseUrl("http://api.icndb.com")
                .addConverterFactory(GsonConverterFactory.create())
        4.5 Using Android Libraries | 91
                .build();
    }

    public void getJoke(TextView textView, String first, String last) {
        this.textView = textView;
        new JokeTask().execute(first, last);
    }

    // 异步线程访问web service
    private class JokeTask extends AsyncTask<String, Void, String> {
        @Override
        protected String doInBackground(String... params) {
            ICNDB icndb = retrofit.create(ICNDB.class);
            Call<IcndbJoke> icndbJoke = icndb.getJoke(
                    params[0], params[1], "[nerdy]");
            String joke = "";
            try {
                joke = icndbJoke.execute().body().getJoke();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return joke;
        }

        @Override
        protected void onPostExecute(String result) {
            jokeView.setText(result);
        }
    }
}

JokeFinder类访问ICNDB网站服务,使用用户名等信息,使用异步线程加载页面,操作在用户界面线程上执行。getJoke方法使用TextView作为参数,用于JokeTask更新解析结果。

IcndbJoke任务是一个简单的JSON结果映射,格式如下:

public class IcndbJoke {
    private String type;
    private Joke value;

    public String getJoke() {
        return value.getJoke();
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public Joke getValue() {
        return value;
    }

    public void setValue(Joke value) {
        this.value = value;
    }

    private static class Joke {
        private int ID;
        private String joke;
        private String[] categories;

        public int getId() {
            return ID;
        }

        public void setId(int ID) {
            this.id = ID;
        }

        public String getJoke() {
            return joke;
        }

        public void setJoke(String joke) {
            this.joke = joke;
        }

        public String[] getCategories() {
            return categories;
        }

        public void setCategories(String[] categories) {
            this.categories = categories;
        }
    }
}

JSON response from the ICNDB service

上述代码作为一个库工程。当一个工程对库工程进程module依赖后,app可以使用库工程,调用JokeFinder类。如下所示:

apply plug-in: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"
// ... all the regular settings ...
}
dependencies {
    compile project(':icndb')
}

编译依赖后使用库module工程,compile project的参数加入module名称作为参数。Gradle在构建app之前会首先构建ICNDB module,并使得类在编译使有效。

WelcomeActivity通过JokeFinder类调用getJoke方法,实现TextView更新,支持SharedPreferences存储first和last name,下次访问的时候可以直接使用,代码如下:

public class WelcomeActivity extends Activity {
    private TextView jokeText;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_welcome);
        jokeText = (TextView) findViewById(R.id.joke_text);
        final SharedPreferences prefs =
                PreferenceManager.getDefaultSharedPreferences(this);
        new JokeFinder().getJoke(jokeText,
                prefs.getString("first", "Xavier"),
                prefs.getString("last", "Ducrohet"));
    }
}

运行结果如下: Running the app

build过程生成debug和release的库文件版本在icndb/build/outputs/arr目录下。

> ./gradlew build
> ls icndb/build/outputs/aar
icndb-debug.aar icndb-release.aar

aar文件可以发布到仓库,这样app就可以使用它了。

总结:

  • android库工程是java工程一种,需要Android依赖,包含Android API支持和资源支持。
  • Gradle使用子目录结构支持多个工程构建,每一个子工程都会被添加到setting.gradle文件中。
  • 在Android Studio中可以使用,通过 “New Module”向导,使用“Android Library”选项创建Android库工程。
  • Android库工程使用com.android.library插件。
  • app构建文件中,使用project(":library")依赖,使用app外的库工程。

通过上述模型,你可以添加android库工程在你的app或是其它app中使用了。

5.性能文档

5.1 构建性能优化

当你想提高Gradle构建性能该如何来做呢?

这里我们要知道:这些都不是建议,将影响您的应用程序的性能。有很多事情你可以做,以帮助编写应用程序,包括许多涉及Android的混淆器工具。本节不是为了提供app自身性能,而是在于提高构建速度等方面性能。

在前面介绍中我们已经介绍了在根目录下面gradle.properties文件的使用。如果我们想使用全局设置,增加一个属性在gradle.properties文件中,即可调用。

Gradle守护

Gradle守护进程运行在后台,用来构建、缓存代码和数据。新版本的Gradle会自动启动Gradle守护进程。

默认情况下,Android Studio为工程开启Gradle守护进程,超时间为3个小时,这么长的时间对于绝大多数任务都是满足的。每次打开Android Studio或是修改相应的编译脚本的时候,Gradle都会自动编译,这里就是守护进程的能力。如果我们通过命令行使用Gradle,是不会自动启动Gradle守护进程的。守护进程的配置是在哪里呢?在根目录的gradle.properties中。

[--> gradle.properties]

org.gradle.daemon=true

守护进程也可以通过命令行开始和停止。使用--daemon--no-daemon来决定使用或不使用相应的守护进程。有时候我们担心缓存溢出或者其它情况希望停止守护进程,在gradle中使用--stop参数。

并行编译

Gradle在独立编译工程是时候可以选择是否使用并行编译。如果使用,需要在gradle.properties中加上一行:

[--> gradle.properties]

org.gradle.parallel=true

注意:并行编译作用并不是非常大。大多数模块之间都是互相联系的或是互相依赖的,这里并行编译并没有多大的收益。

配置需求

通常Gradle会在执行之前为工程中所有的任务进行构建(build)。对于工程来讲,大量的子工程和大量的任务构建起来是非常低效的。因此这里可以只配置请求任务相关工程,并进行编译。在gradle.properties文件中使用“configure on demand”,如下所示:

org.gradle.configureondemand=true

大部分Android工程只有少量的子工程,所以该属性在这种情况下意义并不大。当然这个属性目前属于试探性属性,未来版本有新的功能还需要参考具体文档。

去除无用任务

我们使用-x标志可以去除一些特殊任务,譬如lint和一些并非每次编译都需要的任务,提高编译效率。

更改JVM设置

Gradle编译最终还是要运行在Java进程中,所以影响Gradle的关键点还是在JVM性能上。如何有效的分配虚拟机性能变得格外重要。

org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError

其中的-Xmx标志来表示Java进程可以使用的最大内存。-Xmx标志设置后,虚拟机会在进程中申请相应值的进程空间大小。当出现内存溢出的时候,由java.lang.OutOfMemoryError抛出异常。

只使用需要的依赖

这里特别指出Google Play services,它需要大量的库,目前已独立module形式存在。例如使用Google Maps,你需要添加相应的依赖,如下所示:

dependencies {
    compile 'com.google.android.gms:play-service:7.8.0'
}

Google Play services含有大量的库文件和很多依赖,下图列出相应的依赖文件:

The complete set of Google Play services

用户dex过程选择

当我们将Java字节代码转成Dalvik执行文件(.dex)的时候,Gradle的Android插件允许我们对dex过程进行选择。具体支持的选项如下:

dexOptions {
    incremental true
    javaMaxHeapSize '2g'
    jumboMode = true
    preDexLibraries = true
}
属性说明
incremental是否允许增量模式,但是文档说该方式有许多限制也可能不工作,慎用
javaMaxHeapSizeXmx值的替代,上面我们已经讲到了2g=2048m
jumboMode允许较大字符在dex文件中,配置这个我们需要花费些时间来配置ProGuard
preDexLibraries在libraries打成dex之前启动dx进程。文档说可以加快构建速度,但是清除构建会变慢

所有的设置都在这里,具体情况由读者自己选择使用。

构建分析

在使用命令行对Gradle进程构建的时候,使用--profile标志可以生成有用的构建信息。结果会以HTML格式存到build/report/profile目录下,当前路劲是相对工程根目录。

./gradlew --profile assembleDebug

输出文件在build/report/profile目录下,命名格式为“profile-YYYY-MM-dd-hh-mm-ss.html”,

Sample profile report

报告将汇总分解成两个独立的部分:配置步骤和执行步骤。在小的工程中文档没什么帮助,但对于大的工程有助于找到瓶颈所在。

源:https://my.oschina.net/feiyangxiaomi/blog/758404

5.2 DSL文档

Android开发者网站:https://developer.android.com/index.html

Android Tools项目:https://sites.google.com/a/android.com/tools/tech-docs/new-build-system

Gradle android插件指导:https://sites.google.com/a/android.com/tools/tech-docs/new-build-system/user-guide

DSL参考文档(Github):https://github.com/google/android-gradle-dsl

Gradle android插件对应DSL版本与参考:http://google.github.io/android-gradle-dsl/current/index.html

参考

译《Gradle Recipes for Android》-Just Enough Groovy to Get By

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值