欢迎来真孝善网,为您提供真孝善正能量书籍故事!

Android Studio项目构建与Gradle实战技巧解析

时间:11-14 现代故事 提交错误

今天给各位分享Android Studio项目构建与Gradle实战技巧解析的知识,其中也会对进行解释,如果能碰巧解决你现在面临的问题,别忘了关注本站,现在开始吧!

配置构建:https://developer.android.google.cn/studio/build/index.html

Mainfest合并规则:https://developer.android.google.cn/studio/build/manifest-merge.html

多变体(特定版本)构建指南:https://developer.android.google.cn/studio/build/build-variants.html

缩小未使用的代码和资源:https://developer.android.google.cn/studio/build/shrink-code.html

配置依赖:https://developer.android.google.cn/studio/build/dependency.html

名词

构建类型(BuildType),编译时类型,如debug、发布产品风味(ProductFlavor),不同的产品特性,可以有不同的包名等。构建变体(BuildVariant),每个特定且唯一的版本apk构建变体的产品,由构建类型和产品风味组成。 APG,全称Android Plugin for Gradle,是Google开发的用于使用gradle构建的插件。

1 一个典型的Android Studio 项目

1.1 项目结构

新建一个Android Stuido项目的结构如下:

项目结构包含三个.gradle 文件:

settings.gradle文件对应脚本执行时的设置对象。首先解析并执行该文件。这里可以进行一些常见的初始化操作。当项目包含多个子项目或模块时,它们必须包含在该文件中。这也是它最重要的功能之一。为新项目创建默认的settings.gradle。 include ":app" 这里我想打印脚本第一次执行时项目的存储路径。行动:

String projectDir=rootProject.projectDir.getAbsolutePath();

println 项目目录

在“:app”项目的根目录下包含build.gradle,对应脚本执行时的rootProject对象。一般不进行具体的模块构建操作。用于指定项目依赖的远程仓库以及使用的Gradle插件版本。它适用于所有子项目或模块。 //顶级构建文件,您可以在其中添加所有子项目/模块通用的配置选项。

构建脚本{

//jcenter是著名的远程代码仓库

存储库{

jcenter()

}

依赖项{

类路径"com.android.tools.build:gradle:2.2.3"

//注意: 不要将应用程序依赖项放在这里;他们属于

//在各个模块的build.gradle 文件中

}

}

所有项目{

存储库{

jcenter()

}

}

任务清理(type: 删除){

删除rootProject.buildDir

}上面指定的远程仓库的作用是,当本地找不到依赖库时,会在仓库中查找并自动下载。依赖构建插件,请注意,插件不是Gradle的版本,而是插件的版本。 Google在每个子项目或模块下开发一个单独的build.gradle脚本文件。在这里指定每个依赖的SDK、库属性等。这也是我们编译的脚本的主体。应用plugin:"com.android.application"

安卓{

编译SDK版本25

buildTools版本“25.0.1”

默认配置{

applicationId "com.inpor.fmcdevicedemon"

minSdkVersion 14

目标SDK版本25

版本代码1

版本名称“1.0”

testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

}

构建类型{

发布{

minifyEnabled false

proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"

}

}

}

依赖项{

编译fileTree(dir: "libs", include: ["*.jar"])

androidTestCompile("com.android.support.test.espresso:espresso-core:2.2.2", {

排除group: "com.android.support", module: "支持注释"

})

编译"com.android.support:appcompat-v7:25.0.1"

测试编译"junit:junit:4.12"

}apply plugin 指定要加载的插件。这是一个应用程序,因此加载com.android.application 插件。注意,这个插件是我们上面使用的Google开发的com.android.tools.build:gradle:2.2.3中携带的。

android 闭包来自google 插件,请在此处查看其DSL 文档:http://google.github.io/android-gradle-dsl/。

1.2Gradle Project

我们的AS项目是Gradle的Gradle项目,我们的Gradle项目是由任务组成的。我们可以点击Android Studio最右侧的Gradle工具栏来查看其项目结构。

以gradle项目为例,上面我们点击其他目录,双击第一个任务。这时候我们就可以直接生成这个任务的apk了。

gradle project2

2 配置基本编译参数

2.1 基本使用

这个主要是指设置编译时指定的SDK、bulidtools版本、包名、应用版本号等。请注意,此处定义的属性将覆盖AndroidMainfest.xml 文件中定义的属性。

//编译时SDK版本号

编译SDK版本25

//编译时指定的buildtools版本

buildTools版本“25.0.1”

默认配置{

//应用程序包名

applicationId "com.inpor.fmcdevicedemon"

//指定可以安装应用程序的系统的最低版本。这里14对应的是android 4.0

minSdkVersion 14

//指定运行时使用的SDK版本,主要是当有多个SDK版本时优先使用的SDK版本。

目标SDK版本25

//应用程序版本号

版本代码1

版本名称“1.0”

//执行单元测试时指定的Runner,正式打包时不会使用。

testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

}

2.2 提取公用字段定义到其他文件中

正如之前提到的,我们可以将一个公共属性存储在项目根目录下的build.gradle中。

//使用ext表示导出

分机{

编译SDK=25

构建工具="25.0.1"

目标SDK=25

最小SDK=14

}然后使用应用程序的build.gradle文件中定义的公共属性

编译SdkVersion rootProject.ext.compileSdk

buildTools版本rootProject.ext.buildTools

默认配置{

applicationId "com.inpor.fmcdevicedemon"

minSdkVersion rootProject.ext.minSdk

targetSdkVersion rootProject.ext.targetSdk

版本代码1

版本名称“1.0”

testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

}如果是多项目关联,提取一些共同属性是非常有用的。

2.3 使用resConfigs只打包需要的资源

只打包我们需要的资源。我们知道Google为我们的apk提供了国际化的支持,比如适应不同屏幕分辨率的drawable资源、适应不同语言的字符串资源等等,但很多时候我们只需要一些指定分辨率和语言的资源就足够了。这时候我们就可以使用resConfigs方法来进行配置。

在defaultConfig中添加以下配置后

默认配置{

……

//过滤,国际化支持仅包中文资源,并且"xxhdpi"

//注意这里如果指定了dpi,flavor中不能指定的dpi必须与此一致,否则会报错。

resConfigs "zh-rCN", "xhdpi"

添加resConfigs之前,反编译后的res目录截图:

添加上述未过滤图片的resConfigs配置后,反编译res目录:

过滤图片注意事项:

使用resConfigs不会过滤默认的drawable和values目录。这是为了确保App在任何时候都有一个默认的可选值。 resConfigs 也可以用在稍后讨论的productFlavor 中。

3 signingConfigs(Apk签名配置)

3.1 配置不同的签名

在AS 中编译apk 时默认使用SDK 中的Debug 签名。无需显式指定签名配置项。在signingConfigs的闭包中,我们可以自定义多种签名配置,其中一种典型的签名配置:

签名配置{

//调试签名

调试{

//签名keystore文件存放的位置。这里使用的是相对路径。

storeFile 文件("sign/debug.keystore")

//密钥库的访问密码

存储密码“android”

//别名,因为一个密码库可以被多个项目使用,所以别名不同,最终的签名也不同。

keyAlias "androidreleasekey"

//别名的私钥密码

密钥密码“android”

}

发布{

storeFile 文件("sign/platform.keystore")

存储密码“android”

keyAlias "androidreleasekey"

密钥密码“android”

}

}

3.2 从指定文件加载签名和秘钥

如果你不想在build.gradle中暴露你的签名密钥,你可以把这些参数放在一个特殊的文件中,比如在项目根目录下添加一个keystore.properties文件。

//测试

debugStoreFile=sign/debug.keystore

debugStore密码=android

debugKeyAlias=androidreleasekey

调试密钥密码=android

//发布

releaseStoreFile=sign/platform.keystore

发布商店密码=android

releaseKeyAlias=androidreleasekey

releaseKeyPassword=android 在app模块的build.gradle中,解析这个文件

//创建一个名为keystorePropertiesFile 的变量,并将其初始化为您的

//keystore.properties 文件,位于rootProject 文件夹中。

def keystorePropertiesFile=rootProject.file("keystore.properties")

//初始化一个名为keystoreProperties 的新Properties() 对象。

def keystoreProperties=新属性()

//将keystore.properties 文件加载到keystoreProperties 对象中。

keystoreProperties.load(new FileInputStream(keystorePropertiesFile))

安卓{

……

}修改signConfigs闭包以引用文件中定义的属性

签名配置{

调试{

keyAlias keystoreProperties["debugKeyAlias"]

keyPassword keystoreProperties["debugKeyPassword"]

storeFile 文件(keystoreProperties["debugStoreFile"])

storePassword keystoreProperties["debugStorePassword"]

}

发布{

keyAlias keystoreProperties["releaseKeyAlias"]

keyPassword keystoreProperties["releaseKeyPassword"]

storeFile 文件(keystoreProperties["releaseStoreFile"])

storePassword keystoreProperties["releaseStorePassword"]

}

}

4 编译类型(buildTypes)

在Android studio中,我们可以自定义不同的编译类型,例如debug版本和release版本。在不同的版本中,我们可以配置不同的参数并添加属性。项目自带debug编译类型,用户无法将该类型自定义为test,已经被单元测试占用了。

比如下面我定义了三种不同的buildType,并分别设置了不同的属性值。

构建类型{

调试{

//指定签名文件的配置。如果未指定,将使用SDK 中的默认调试签名。

签名配置签名配置.debug

//压缩对齐以提高运行效率。您还可以使用zipAlignEnabled true

setZipAlignEnabled(真)

//可以调试

可调试true

//jni可以调试

jni可调试true

//渲染脚本可调试

renderscript可调试true

}

//release版本不允许调试,添加代码混淆

发布{

setZipAlignEnabled(真)

可调试错误

jni可调试false

renderscript可调试false

//指定签名文件为发布签名。请注意,这不是调试。如果不指定签名,则不会对包进行签名。

签名配置签名配置.release

//minifyEnabled 表示代码是否可以被压缩、裁剪和优化。需要配合其他工具使用,比如proguard

//添加代码混淆。注意添加混淆时必须将minifyEnabled设置为true,否则混淆不会生效。

//同样,如果不使用代码混淆,则必须设置为false,否则编译会失败。

minifyEnabled true

proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"

}

//自定义一个类型,不使用代码混淆,并在xml资源文件中添加字符串资源

风俗{

zipAlignEnabled true

可调试错误

jni可调试false

renderscript可调试false

//指定签名文件为发布签名

签名配置签名配置.release

//将字符资源添加到values/strings.xml文件中。目前无法指定资源的语言类别。

resValue "字符串","自定义名称","测试用户"

}

}上面的minifyEnabled也可以与shrinkResources属性一起使用来删除未使用的资源文件。

构建类型{

风俗{

.

minifyEnabled true

收缩资源true

.

}

}实际测试中发现,上述裁剪可以裁剪布局、图片、菜单,但不会去除数值。

注意shrinkResources优化并不一定会删除没有用到的文件,在我的实际测试中,它会图片、布局变成最小,没有删除它们

当我们需要动态加载资源时,就不需要使用这种优化,否则可能会出现运行时错误或者显示效果不正确。如果你想使用这个优化,你可以在res/raw/keep.xml 中保留特定资源或者进行优化。以下示例没有优化layout/test_layout。

?xml version="1.0"encoding="utf-8"?参考(官网持有的资源章节):https://developer.android.google.cn/studio/build/shrink-code.html#keep-resources

5splits(拆分只包含某些需要属性的apk)

分体式

它的作用是将当前配置的apk版本进行拆分,生成多个只包含指定属性的apk。目前,Google 为我们提供了基于语言、abi 和密度的拆分。 //过滤为只打包英文和简体中文的资源

分裂{

//设置基于语言的分割测试并失败。应该是字符串表达式错误。

//语言{

//启用true

//包含"values-zh-rCN"

//包含"zh-rCN"

//}

密度{

启用真

reset() //将默认列表从所有密度清除为无密度。

include "mdpi", "xxhdpi" //指定我们要为其生成APK 的两种密度。

}

}以上配置编译完成后,会生成3个Apk:

app-mdpi-custom.apk //剪切掉大部分非mdpi资源的apk

app-xxhdpi-custom.apk //剪切掉大部分非xxdpi资源的apk

app-universal-custom.apk //没有任何裁剪的apk参考Android apk splits官方文档:https://developer.android.google.cn/studio/build/configure-apk-splits.html

6 PackagingOptions(指定添加/移除某些文件到最终的apk中)

首先看它的DSL结构图。

PackagingOptions 与resConfigs 不同。后者过滤某些资源目录。前者在打包Apk时(已编译)排除了一些文件。在实际测试中,它不能用来过滤资源文件等,更多的是用于过滤一些与项目没有直接关系的文件(声明、版本控制等)。

首先,如果要添加的文件已存在于Apk中,则会被忽略。如果指定模式有多个路径,则仅添加第一个路径。合并就是合并的意思。如果该文件不存在,请添加它。如果文件已存在,则合并内容。排除,不包含的内容,以下内容默认不会打包到Apk中:/META-IN

F/LICENCE /META-INF/LICENCE.txt /META-INF/NOTICE /META-INF/NOTICE.txt /LICENCE /LICENCE.txt /NOTICE /NOTICE.txt **/.svn/** (all .svn directory contents) **/CVS/** (all CVS directory contents) **/SCCS/** (all SCCS directory contents) **/.* (all UNIX hidden files) **/.*/** (all contents of UNIX hidden directories) **/*~ (temporary files) **/thumbs.db **/picasa.ini **/about.html **/package.html **/overview.html **/_* **/_*/**PackagingOptions更多的用于去除编译时依赖不同的包时,含有相同的文件时,去除编译时的重复错误中。如: //在打包时,移除一些许可,注意文档 packagingOptions { exclude "META-INF/DEPENDENCIES.txt" exclude "META-INF/NOTICE" exclude "META-INF/NOTICE.txt" exclude "META-INF/LICENSE" exclude "META-INF/LICENSE.txt" }实际测试中还没有发现有其他的作用,待补充。官方DSL文档链接:http://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.PackagingOptions.html

7lintOptions

lint检查工具是google开发的一款代码扫描工具,其主要用于扫描布局,未使用资源,国际化等等问题,其作用在这里不是我们关注的重点,其使用和配置方法请查看官方文档:https://developer.android.google.cn/studio/write/lint.html。 这里我们要考虑的是link选项对我们打包的影响,要注意link检查抛出来的错误,并不会导致编译时候的错误,可能会导致运行时的错误,以是lintOptions的属性截图: lintOptions在Android Studio中默认下,link检查报错会导致编译中断,为了避免这个问题,我们可以在android闭包中添加如下代码: android { ...... lintOptions { //关闭编译release版本的lint检查 checkReleaseBuilds false //关闭link检查报错中断编译选项 abortOnError false } ...... }在日常研发中,我们应当频繁执行lint检查,以优化代码和提前暴露一些可能运行时报错的代码。

8productFlavor(产品风味)

8.1 基本属性与方法

productFlavor与其说是产品风味还不如说是产品工厂,我们可以根据不同的需要,最终生成不同的apk。多渠道打包是productFlavor的最常用的功能之一。上面说到的defaultConfig我们可以认为一种简略的默认productFlavor,所以我们完全可以在自定义的productFlavor中覆盖defaultConfig中的任意配置项。 基本属性perpties方法与闭包 method参考:http://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.ProductFlavor.html 从上面的属性与方法中我们发现可以设置包名,版本,混淆文件, NDK等等

8.2 示例

现在我创建多个productFlavor,它们具有不同的包名,不同的版本号 productFlavors{ sky{ //直接在原来的包名后加后缀 applicationIdSuffix ".sky" //指定不同的版本 versionName "1.2.0" versionCode 120 } gavin{ //重新命名包名 applicationId "com.gavin.gradlebuilddemo" //指定不同的最小编译版本 minSdkVersion 17 targetSdkVersion 21 } smith{ applicationId "com.smith.gradlebuilddemo" //指定不同的resConfig resConfigs "zh-rHK", //添加resValue resValue "string", "smith", "this is smith" } }考虑到一种情况,有时候我们有些公共的资源和配置是某些productFlavors公用的,我们希望把它们提取出来,减少重复,这个时候我们可以使用flavorDimensions来实现我们的需求。 //使用dimensions将一些公共的修改独立出来,可以重复使用,减小代码的重复 flavorDimensions "type", "common" productFlavors{ sky{ dimension "type" //直接在原来的包名后加后缀 applicationIdSuffix ".sky" //指定不同的版本 versionName "1.2.0" versionCode 120 } gavin{ dimension "type" //重新命名包名 applicationId "com.gavin.gradlebuilddemo" //指定不同的最小编译版本 minSdkVersion 17 targetSdkVersion 21 } smith{ dimension "type" applicationId "com.smith.gradlebuilddemo" //指定不同的resConfig resConfigs "en", "hdpi" //添加resValue resValue "string", "smith", "this is smith" } commonClient{ dimension "common" //添加resValue resValue "string", "common_client", "this is common_client" } commonPrivate{ dimension "common" //指定一个私有的混淆规则 proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules-private.pro" } }采用上述实现中,会将两两不同的dimension进行组合,最终生成我们需要的apk。比如app-sky-commonClient-debug.apk。 现在我们来计算一下最终可以生成的apk的数目,我们将flavorDimensions看成数组的话,最终可以生成的apk的数目为:count = BuildType.size * flavorDimensions[0].size * ... flavorDimensions[n].size这里的flavorDimensions[n].size是指每个Dimensions在productFlavors中的个数,比如上面的最终能够生成的apk个数就是:3 * 3(type)* 2(common) = 18。

8.3 资源替换

同样的我们也可以在srcmain的同级目录下给每个productFlavors建立目录存放我们的特定资源。 srcFlavorDir替换res资源替换res资源采用的是合并的原则,即最终的资源是所有进行合并的资源的并集,出现资源ID重复的资源,采用优先级最高的那个,具体的优先级后面会讲到。 在main/res/values/strings.xml中定义了这样的资源。 GradleBuildDemohello worldentercancelPlease input the word you wantbutton is clicked在sky/res/values/strings.xml中重新定义了如下资源。 GradleBuildDemo_Skyhello world, skyenter sky helpcancel sky最终合并的资源是这样的。 GradleBuildDemo_Skyhello world, skyenter sky helpcancel skyPlease input the word you wantbutton is clicked注意,layout资源是以整个文件覆盖的方式合并的。assets目录assets目录中的文件是以整个文件覆盖的方式进行合并的。java原代码目录源码文件的合并与其他的不同,如果我们想在不同的变体中对一个类做不同的实现,那么我们就不能在main/java目录下定义这个类,只能在每个变体中单独定义,并且路径要一致,而且对一些变体进行组合时,同时也只能存在一份代码。 以下示例中,我分别在sky, gavin两个flavors中定义了HelpTestActivity类。 sky版本的HelpTestActivity类。 package com.sky.gradlebuilddemo.activity; import android.graphics.Color; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.Button; import com.sky.gradlebuilddemo.R; /** * PACKAGE_NAME * [function] * [detail] * Created by Sky on 2016/10/24. * modify by */ public class HelpTestActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_help_test); Button skyButton = (Button) findViewById(R.id.skyButton); //点击按钮弹出提示文案, skyButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { final Snackbar snackbar = Snackbar.make(HelpTestActivity.this.getWindow().getDecorView(), "hello snackbar", Snackbar.LENGTH_LONG); snackbar.setAction("Change Color", new View.OnClickListener() { @Override public void onClick(View v) { snackbar.getView().setBackgroundResource(R.color.colorPrimary); } }).show(); } }); } }gavin版本的HelpTestActivity类 package com.sky.gradlebuilddemo.activity; import android.content.Context; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Adapter; import android.widget.BaseAdapter; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.SimpleAdapter; import android.widget.TextView; import com.sky.gradlebuilddemo.R; /** * PACKAGE_NAME * [function] * [detail] * Created by Sky on 2016/10/24. * modify by */ public class HelpTestActivity extends AppCompatActivity { private ListView listView; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_help_test); listView = (ListView) findViewById(R.id.msgListView); String[] msgs = getResources().getStringArray(R.array.listMsg); listView.setAdapter(new SimpleListAdapter(this, msgs)); } private static class SimpleListAdapter extends BaseAdapter{ private String[] data; private Context context;

SimpleListAdapter(Context context, String[]data){ this.data = data; this.context = context; } @Override public int getCount() { return data.length; } @Override public Object getItem(int position) { return data[position]; } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { if(convertView == null){ convertView = LayoutInflater.from(context).inflate(R.layout.list_item_layout, null); } TextView textView = (TextView) convertView.findViewById(R.id.itemTextView); textView.setText(data[position]); return convertView; } } }在MainActivity中调用它。 package com.sky.gradlebuilddemo; import android.content.Intent; import android.support.design.widget.Snackbar; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import com.sky.gradlebuilddemo.activity.HelpTestActivity; public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button enterButton = (Button) findViewById(R.id.enter_button); enterButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { MainActivity.this.startActivity(new Intent(MainActivity.this, HelpTestActivity.class)); } }); ....... } }

9 sourceSets(资源集合的集合)

9.1sourceSet与优先级

sourceSet就是所谓的源集,包含特定源代码和所需资源,每一个源集不是任意命名的,每一个源集对应一个BuildType或ProductFlavor或BuildVariant,看官方的文档描述。 sourceSet 参考:https://developer.android.google.cn/studio/build/index.html#sourcesetsAndroid Studio在编译某个构建变体的时候,并不是单独的使用某个源集,而是merge不同的源集,比如有一个SkyCommonClientCustom的构建变体,并且定义了skyCommonClientCustom, custom, sky, commonClient这四个源集,那么在构建的时候就会合并上述的四个源集和默认的main源集的源代码和资源。 上面提到了merge合并资源,那么合并的优先级是怎样的呢? SourceSetPority需要补充一点,如果在ProductFlavor使用了flavorDimensions,比如: flavorDimensions "type", "common" sourceSets{ sky{ ..... } commonClient{ ..... } } productFlavors{ sky{ dimension "type" //直接在原有包名后面添加 applicationIdSuffix ".sky" } commonClient{ dimension "common" } }那么源集sky的优先级高于commonClient,所以如果把flavorDimensions看做一个数组的话,最终的优先级是:BuildVariant >BuidlType >flavorDimensions[0] >... >flavorDimensions[x] >main >内容库依赖项(aar等)

9.2 sourceSet的基本属性

sourceSet简单来说就是指定在编译指定了某些特定源代码和资源的集合,在Android中使用的是google的AndroidSourceSet。 android闭包中的sourceSets就是由上面用户定义的一系列的AndroidSourceSet的集合。 先看AndroidSourceSet的DSL属性结构图: AndroidSourceSet从上面的图中我们可以看出,针对每个AndroidSourceSet可以配置不同的: //跨进程通信声明文件(.aidl) aidl //assets文件下文件 assets //.java文件目录 java //.c, .cpp文件位置 jni //.so文件路径,注意该路径只需要指定到所包含平台的外层,不需要指定到具体的平台如`armeabi`,否则无法找到SO jnilibs //mainfest文件 manifest //Android resource res //(java resource) resource //渲染脚本 rendersript最终根据这些不同的资源集合生成不同的apk。 以下四个属性为只读属性: //sourceSet的名称,如custom name //编译时的配置名称,如customCompile,与后面要讲的dependencies的配置项compile相对应。 compileConfigurationName //如customApk,与后面要讲的dependencies的配置项apk相对应。 packageConfigurationName //如customProvided,与后面要讲的dependencies的配置项provided相对应。 providedConfigurationName

9.3 示例

sourceSets { main{ jniLibs.srcDirs=["libs"] } custom{ //指定一个新的jnilibs为根目录下的jnilibs jniLibs.srcDirs=[rootProject.projectDir.absolutePath + "/jnilibs"] //指定一个新的assets为根目录下的skyTestAssets目录 assets.srcDirs = [rootProject.projectDir.absolutePath + "/assets/skyTestAssets"] } }上面的例子中我们给custom这个源集指定了新的jniLibs和Assets目录,上面的源集也可以采用闭包的形式。 custom{ jniLibs{ //指定一个新的jnilibs为根目录下的jnilibs srcDirs=[rootProject.projectDir.absolutePath + "/jnilibs"] } assets{ //指定一个新的assets为根目录下的skyTestAssets目录 srcDirs = [rootProject.projectDir.absolutePath + "/assets/skyTestAssets"] } }两点注意: srcDir和srcDirs的区别,当使用srcDir指定文件目录时,不允许将要合并的源集的同一目录下有一样名称的资源,提示重复资源异常,而srcDirs则会根据前面所说的优先级进行覆盖。 如果我们在src/main的同级目录下,也建立一个如下的文件目录: buildTypeCustonDir当在源集中指定了asserts的目录时,custom/assets目录会直接失效。

9.4 未解决的问题

在上面的源集中,Android官方构建指南没有提及一点,就是如何过滤源集目录下的一些文件不编译或打包到最后的apk中,我使用如下方式,希望过滤掉src/main/assets/mainIngoreTest.txt文件不打包到最终的apk中。 sourceSets { ..... custom{ //指定一个新的jnilibs为根目录下的jnilibs jniLibs.srcDirs=[rootProject.projectDir.absolutePath + "/jnilibs"] //指定一个新的assets为根目录下的skyTestAssets目录 assets.srcDirs = [rootProject.projectDir.absolutePath + "/assets/skyTestAssets"] //意图过滤掉`src/main/assets/mainIngoreTest.txt`文件 assets.exclude (project.projectDir.absolutePath + "\src\main\assets\mainIngoreTest.txt") //或者采用闭包 assets.exclude{ File f = it.file println f.absolutePath f.absolutePath.endsWith("mainIngoreTest.txt") } } ..... }采用上述方式并不能成功过滤掉文件,并且上面的闭包中的代码也没有执行,目前还没有找到原因。

10Dependencies(依赖内容库)

依赖内容库指的是不是当前工程的代码或资源,它们存在于其他的项目中,它们可以以源码库,jar,arr形式存在。

10.1 声明依赖项的三种方式

先看声明方式。 android {...} ... dependencies { // 添加含有源码的模块依赖 compile project(":mylibrary") // 远程二进制库依赖 compile "com.android.support:appcompat-v7:25.1.0" // 本地库(jar)依赖 compile fileTree(dir: "libs", include: ["*.jar"]) }下面逐一介绍,以下来自官方文档。模块依赖项compile project(":mylibrary")行声明了一个名为mylibrary的本地Android库模块作为依赖项,这样的库可能是另外某个工程的一部分,此时是具有源代码的,注意依赖本地库模块时,必须在根目录下的settings.gradle文件中include它。远程二进制依赖项compile "com.android.support:appcompat-v7:25.1.0"行通过指定其 JCenter 远程仓库中的标识,当本地不存在该依赖库时,则自动从远程下载,默认存放在sdk/extras/目录下,当然我们也可以在 SDK 管理器下载和安装特定的依赖项。本地二进制依赖项简单来说就是依赖已经打包好的jar库,compile fileTree(dir: "libs", include: ["*.jar"])的意思就是依赖app/libs目录下的所有的以.jar结尾的文件。

10.2 配置依赖项

当我们希望对依赖项在编译和打包时做一些特殊处理的时候,通过使用不同的关键词,google给我们提供三种配置方式: compile,最常见的配置项,编译时依赖,Gradle将此配置依赖项添加到类路径和最终的apk中,其实就是说在编译时和最终的apk中都需要。 apk,其指定的依赖项只在打包最终的apk的时候才需要,此配置只能和JAR二进制依赖项一起使用,而不能与其他库模块依赖项或 AAR 二进制依赖项一起使用。 provided,其指定的依赖项,此配置依赖项将添加到类路径中,只在编译的时候需要,不打包到最终的apk中(也就是说运行时无须该依赖项),比如我们编译时使用的SDK就属于这一类,同样的此配置只能和JAR二进制依赖项一起使用,而不能与其他库模块依赖项或 AAR 二进制依赖项一起使用。 示例: dependencies { ..... // 依赖app/apklib下的jar文件,只在apk打包的时候依赖 apk fileTree(dir: "apklib", include: ["*.jar"]) // 依赖app/rovidedlib下的jar文件,只在编译的时候依赖 provided fileTree(dir: "providedlib", include: ["*.jar"]) }

10.3 指定特定的构建变体的依赖项

在实际构建中,我们常常遇到有这样的需求,我们希望某些依赖项只有在某些特定构建变体编译时才被依赖,在Android Studio中我们可以指定依赖项在以下特定的构建类型下才依赖: BuildTypesBuildVariantProductFlavors也就是说我们可以以上述任意一种方式指定特定的依赖项,我们知道每一个buildType, flavor都有一个与之名字相同的的sourceSet,所以我们要指定依赖项为某特定类型的方式为: sourceSet.compileConfigurationName,如skyCompilesourceSet.packageConfigurationName,如skyApksourceSet.providedConfigurationName,如skyProvided以下是具体示例。 dependencies { // BuildTypes为AndroidTest,做单元测试时才编译 androidTestCompile("com.android.support.test.espresso:espresso-core:2.2.2", { exclude group: "com.android.support", module: "support-annotations" }) // BuildTypes为test的 依赖项junit testCompile "junit:junit:4.12" // BuildTypes为debug的添加含有源码的模块依赖 debugCompile project(":mylibrary") // flavor为sky的指定jar库,且不使用`okhttp-3.2.0.jar`库,使用skyCommon目录下的okhttp-3.3.1.jar skyCompile fileTree(include: ["*.jar"], dir: "jar/sky", excludes: ["okhttp-3.2.0.jar"]) // 构建变体依赖项指定 skyCommonClientCustomCompile fileTree(include: ["*.jar"], dir: "jar/skyCommonClientCustom") //以下是正常的依赖 // 远程二进制库依赖 compile "com.android.support:appcompat-v7:25.1.0" // 本地库(jar)依赖 compile fileTree(dir: "libs", include: ["*.jar"]) compile "com.android.support:design:24.2.1" }注意,在上述使用xxxCompile时,有时会提示找不到对应的xxxCompile方法的错误: Error:(180, 0) Could not find method skyCommonClientCustomCompile() for arguments [directory "jar/skyCommonClientCustom"] on object of type org.gradle.api.internal.artifacts.dsl.dependencies.DefaultDependencyHandler.Open File解决办法为,我们在sourceSets闭包下,新建一个空的对应的soruceSet就可以了。 sourceSets{ ..... // 此处增加一个空的sourceSet,是为了解决在dependencies中使用 // skyCommonClientCustomCompile 指定依赖项时提示找不到方法的错误 skyCommonClientCustom{ } }资源合并的规则同样适用与依赖合并,所以在指定特定依赖后,构建某个特定变体时(flavorType),其编译时最终的依赖项(不考虑provided)就变为:flavorTypeCompile + typeCompile + flavorCompile + compile指定依赖项的构建类型我们可以直接指定依赖项的构建类型,这里的构建类型可以是BuildVariant, buildType, flavor。 dependencies { ... // relase构建时指定依赖的`library`也是release releaseCompile project(path: ":library", configuration: "release") // debug构建时指定依赖的`library`的构建也是`debug` debugCompile project(path: ":library", configuration: "debug") ...... }

10.4transitive, force, exclude的使用与依赖冲突解决

通过gradle命令查看依赖树,在模块所在的目录(app目录),执行gradle dependencies,执行结果如图(以androidTest为例)。 android_test_dependenicetransitivetransitive用于自动处理子依赖项。默认为true,gradle自动添加子依赖项,形成一个多层树形结构;设置为false,则需要手动添加每个依赖项。 为所有的配置指定自动添加子依赖项为falseconfigurations.all { transitive = false }为单独的某个依赖项指定字典添加子依赖项为falseandroidTestCompile("com.android.support.test.espresso:espresso-core:2.2.2", { transitive = false })force即强制设置某个模块的版本。 configurations.all { resolutionStrategy { force "com.android.support.test:runner:0.2" } }以上设置之后所有对com.android.support.test:runner模块有依赖的其他库都被强制使用0.2版本。exclude排除依赖项中的某些子依赖项,这在解决依赖库版本冲突或者重复时特别有用,我们可以通过如下两种方式进行排除: group,maven项目的GroupId,GroupID是项目组织唯一的标识符,对于小型的项目,常常对应JAVA的包的结构,是main目录里java的目录结构,但是也可以很多个项目共用一个GroupID,如com.android.support下就有很多个子项目。 module,maven项目的ArtifactID,ArtifactID就是具体某个项目的唯一的标识符,实际常常对应项目的名称,就是项目根目录的名称,如support-annotations。 group和module可以配合一起使用也可以单独使用。 配合使用//移除所有依赖项中,组织为`com.android.support`项目为`support-annotations`的子依赖项 configurations { all*.exclude group: "com.android.support", module: "support-annotations" } //移除单个依赖项中,组织为`com.android.support`项目为`support-annotations`的子依赖项 androidTestCompile("com.android.support.test.espresso:espresso-core:2.2.2", { exclude group: "com.android.support", module: "support-annotations" })单独使用group和module//移除所有依赖项中,组织为`com.android.support`的子依赖项 configurations { all*.exclude group: "com.android.support" } //移除单个依赖项中,组织为`com.android.support`的子依赖项 androidTestCompile("com.android.support.test.espresso:espresso-core:2.2.2", { exclude group: "com.android.support" })单独使用module//移除所有依赖项中名为`support-annotations`的子依赖项 configurations { all*.exclude module: "support-annotations" } //移除单个依赖项中名为`support-annotations`的子依赖项 androidTestCompile("com.android.support.test.espresso:espresso-core:2.2.2", { exclude module: "support-annotations" })依赖项的版本冲突gradle在同一个配置下(例如androidTestCompile),某个模块的不同版本同时被依赖时,默认使用最新版,gradle同步时不会报错,例如: dependencies { androidTestCompile("com.android.support.test:runner:0.4") androidTestCompile("com.android.support.test:rules:0.2") androidTestCompile("com.android.support.test.espresso:espresso-core:2.1") }上面espresso:espresso-core依赖runner不同与上面我们上面指定的版本的runner,此时gradle会自动同步最新版本。 对于不同的配置下,出现的依赖项不一样,gradle就会直接报错,这时我们就可以使用上面提到的force, exclude来解决。

10.5 参考

Google官网依赖项说明: https://developer.android.google.cn/studio/build/dependencies.htmlhttps://developer.android.google.cn/studio/build/build-variants.html#dependenciesGradle官方说明文档: https://docs.gradle.org/current/userguide/artifact_dependencies_tutorial.htmlhttps://docs.gradle.org/current/userguide/dependency_management.html#sec:how_to_declare_your_dependencies依赖的dependencies、transitive、force、exclude及版本冲突处理 http://www.paincker.com/gradle-dependencies

11 综合

11.1 过滤不需要生成的apk

当我们添加自定义的BuildType, flavor时,必然会组合出很多我们不需要的apk,google也给我们提供了解决方案。 核心就是使用variantFilter这个方法过滤满足我们特定条件的所有构建变体。 // 移除不需要打包的apk variantFilter { variant ->String buildTypeName = variant.buildType.name String flavors0Name = variant.getFlavors().get(0).name //对于编译类型为`release 或者 custom` 并且 flavors0类型为`smith 或 gavin`的构建类型直接忽略,不用编译 if((buildTypeName.equals("release") || buildTypeName.equals("custom")) && (flavors0Name.equals("smith") || flavors0Name.equals("gavin"))) { variant.setIgnore(true); } }

11.2 apk名称修改

Android studio 构建生成的apk的默认名称为app-flavor[0]-...-flavor[n]-buildType.apk,google给我们提供了修改Apk名称的方法。 applicationVariants包含了所有可能构建变体的集合,我们使用闭包遍历所有的输出,修改我们想修改的apk的名称,以下是一个示例。 //修改生成的apk的名称,命名为demo-flavorsName-buildType-versionName.apk applicationVariants.all { variant ->//遍历所有的输出文件 variant.outputs.each { output ->File tempFile = output.outputFile //对于包含`commonClient` flavor的我们在名称中去掉它 if (variant.productFlavors[1].name.contains("commonClient")) { output.outputFile = new File(tempFile.parent, tempFile.name.replace(tempFile.name, "demo" + variant.productFlavors[0].name + "_" + variant.buildType.name + "_${variant.versionName}.apk")) } else { output.outputFile = new File(tempFile.parent, tempFile.name.replace(tempFile.name, "demo" + variant.productFlavors[0].name + "_" + variant.productFlavors[1].name + "_" + variant.buildType.name + "_${variant.versionName}.apk")) } } }

11.3 mainfest文件添加属性

当我们打多渠道包或需要给不同的构建变体加入的不同的属性时,此时我们就需要修改mainfest文件,gradle给我们提供了两种方式动态的修改mainfest文件。在变体对应src目录下添加一个mainfest文件在添加的mainfest文件中添加/修改配置项,此种方式与动态合并res/values/strings.xml的方式一致,其遵守的规则也和它们保持一致,这种方式不仅能添加属性,还能添加四大组件等等,具体可以参考: Mainfest合并规则:https://developer.android.google.cn/studio/build/manifest-merge.html使用APG中的manifestPlaceholders属性manifestPlaceholders在build.gradle中以键值对的方式给mainfest中对应键设置相应的值的方式来实现,看如下示例,我在mainfest文件中添加以下需要动态设置的属性。 ..........接下来我分别在defaultCofig, sky, gavin三个变体中从不同路径的配置文件config.xml中读取属性,这里我把解析xml文件放在根目录下自定义的utils.gradle文件中。 /** * 解析XML文件 * */ def parseXml(String path) { println ("parseXml called, path = " + path) return new XmlParser().parse(path) } ext { parseXml = this.&parseXml }然后在模块的build.gradle文件中引用它。 // 加载自定义的utils.gradle apply from: rootProject.projectDir.getAbsolutePath() + File.separator + "utils.gradle"最后在三个变体中解析相应的配置文件。 defaultConfig { ...... def defaultConfig = parseXml("app/config/main/config.xml") manifestPlaceholders = [ APP_KEY_VALUE : defaultConfig.appKey[0].text(), APP_ID_VALUE : defaultConfig.id[0].text() ] } flavor{ sky{ ...... def skyConfig = parseXml("app/config/sky/config.xml") manifestPlaceholders = [ APP_KEY_VALUE : skyConfig.appKey[0].text(), APP_ID_VALUE : skyConfig.id[0].text() ] } gavin{ ...... def gavinConfig = parseXml("app/config/gavin/config.xml") manifestPlaceholders = [ APP_KEY_VALUE : gavinConfig.appKey[0].text(), APP_ID_VALUE : gavinConfig.id[0].text() ] } ...... }这样最后构建出来的apk使用的就是上面配置的不同的值。

11.4APG动态生成的BuildConfig类的使用

使用BuildConfig使用APG构建apk,会动态自动生成一个BuildConfig类,里面会包含当前构建变体的一些基本属性,如版本号等等,一下是一个默认下的示例(当前构建变体是skyCommonClientDebug)。 /** * Automatically generated file. DO NOT MODIFY */ package com.sky.gradlebuilddemo; public final class BuildConfig { public static final boolean DEBUG = Boolean.parseBoolean("true"); public static final String APPLICATION_ID = "com.inpor.fmcdevicedemon.sky"; public static final String BUILD_TYPE = "debug"; public static final String FLAVOR = "skyCommonClient"; public static final int VERSION_CODE = 120; public static final String VERSION_NAME = "1.2.0"; public static final String FLAVOR_type = "sky"; public static final String FLAVOR_common = "commonClient"; }一种最典型的用法,就是在源代码中判断当前是不是debug版本,然后做某些操作,如果是其他版本又做什么操作等等。 一个示例,在MainActivity中添加如下代码。 public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; ..... private void checkBuildConfig(){ if(BuildConfig.DEBUG){ Log.i(TAG, "now this is debug build"); } } }自定义BuildConfig属性APG给我们提供了自定义BuildConfig属性的方法buildConfigField,注意,添加Field时,最好在defaultConfig中给要添加的Field设置一个默认值,否则当编译其他没有设置该Field的变体时,会编译报错,我们可以在buildTypes, flavors中复写它。 在下面的示例中我添加一个字段。 defaultConfig { ...... buildConfigField "int", "ID", "0" } flavor{ sky{ ...... buildConfigField "int", "ID", "1" } ...... }最终生成的BuildConfig是这样的。 /** * Automatically generated file. DO NOT MODIFY */ package com.sky.gradlebuilddemo; public final class BuildConfig { public static final boolean DEBUG = Boolean.parseBoolean("true"); public static final String APPLICATION_ID = "com.inpor.fmcdevicedemon.sky"; public static final String BUILD_TYPE = "debug"; public static final String FLAVOR = "skyCommonClient"; public static final int VERSION_CODE = 120; public static final String VERSION_NAME = "1.2.0"; public static final String FLAVOR_type = "sky"; public static final String FLAVOR_common = "commonClient"; // Fields from product flavor: sky public static final int ID = 1;

关于Android Studio项目构建与Gradle实战技巧解析的内容到此结束,希望对大家有所帮助。

用户评论

那伤。眞美

终于开始摸索Android Studio的项目构建了

    有17位网友表示赞同!

青山暮雪

看到"Gradle构建实践"这个词感觉有点专业的样子,希望能学到实用技巧!

    有13位网友表示赞同!

艺菲

我对Gradle的语法和配置不太了解,这篇文章能讲解详细吗?

    有11位网友表示赞同!

清原

想在Android Studio里写一个自己的App, Gradle是必不可少的工具吧?

    有7位网友表示赞同!

迷路的男人

学习开发Android APP应该先掌握Gradle构建系统吧!

    有11位网友表示赞同!

掉眼泪

这个主题一直很困扰我,希望能从文章中找到解决方法!

    有5位网友表示赞同!

冷落了♂自己·

终于有人讲解Gradle构建了,以前总是觉得它很难上手

    有9位网友表示赞同!

最怕挣扎

看了标题,感觉可以学习很多Android开发的知识点

    有18位网友表示赞同!

闷骚闷出味道了

之前用过Gradle,但还是想再看一次实践案例,加深理解

    有18位网友表示赞同!

还未走i

最近在做Android开发项目,遇到了一些Gradle构建的问题,希望能找到答案

    有10位网友表示赞同!

墨城烟柳

需要学习Gradle来提高Android开发效率!

    有12位网友表示赞同!

焚心劫

这个文章标题吸引了我,看起来很详细地讲解了Gradle的使用方法

    有17位网友表示赞同!

蔚蓝的天空〃没有我的翅膀

Android开发入门必修课,希望能从实践中学到东西

    有5位网友表示赞同!

孤者何惧

期待一篇好的文章可以帮助我更好地理解Gradle

    有19位网友表示赞同!

凝残月

学习建 Android 项目的流程,感觉Gradle 是关键步

    有9位网友表示赞同!

聽風

分享一些实际案例会更好理解Gradle的应用场景!

    有19位网友表示赞同!

厌归人

希望文中能包含常见的Gradle构建问题和解决方案!

    有13位网友表示赞同!

ー半忧伤

Gradle是Android开发的基础,掌握它很重要!

    有17位网友表示赞同!

鹿先森,教魔方

文章标题看起来很专业,应该会有很多实用的技巧!

    有9位网友表示赞同!

【Android Studio项目构建与Gradle实战技巧解析】相关文章:

1.蛤蟆讨媳妇【哈尼族民间故事】

2.米颠拜石

3.王羲之临池学书

4.清代敢于创新的“浓墨宰相”——刘墉

5.“巧取豪夺”的由来--米芾逸事

6.荒唐洁癖 惜砚如身(米芾逸事)

7.拜石为兄--米芾逸事

8.郑板桥轶事十则

9.王献之被公主抢亲后的悲惨人生

10.史上真实张三丰:在棺材中竟神奇复活