Gladle构建知识点

1 ,Android 构建系统

构建 APK 的过程是个相当复杂的过程,Android 构建系统需要将应用的资源文件和源文件一同打包到最终的 APK 文件中。应用可能会依赖一些外部库,构建工具要灵活地管理这些依赖的下载、编译、打包(包括合并、解决冲突、资源优化)等过程。

应用的源码可能包括 Java 、RenderScript、AIDL 以及 Native 代码,构建工具需要分别处理这些语言的编译打包过程,而有些时候我们需要生成不同配置(如不同 CPU 架构、不同 SDK 版本、不同应用商店配置等)的 APK 文件,构建工具就需要根据不同情况编译打包不同的 APK。

总之,构建工具需要完成从工程资源管理到最终编译、测试、打包、发布的几乎所有工作。而 Android Studio 选择了使用 Gradle,一个高级、灵活、强大的自动构建工具构建 Android 应用,利用 Gradle 和 Android Gradle 插件可以非常灵活高效地构建和测试 Android 应用了: 

Gradle和其Android插件可以帮助你自定义以下几方面的构建配置:

AppExtension的属性

aaptOptions:aapt是一个可以将资源文件编译成二进制文件的工具。aaptOptions表示aapt工具设置的可选项参数。

adbExecutable:adb从编译sdk时执行

adbOptions:adb的可选项参数

applicationVariants:应用变体列表

==buildToolsVersion==:构建工具版本(必要的)

buildTypes:构建类型(一般是release和debug,还可以自定义)

compileOptions:编译可选项参数

==compileSdkVersion==:编译sdk版本(必要的)

dataBinding:Data Binding可选项参数(关于DataBinding的使用)

defualtConfig:默认配置,对于所有的打包项目

defualtPublishConfig:默认是release。使用参考

dexOptions:Dex可选项参数。

externalNativeBuild:native编译支持。参考

flavorDimensionList:

generatePureSplits:是否拆成多个APK

jacoco:JaCoCo可选项参数

lintOptions:Lint工具可选项参数

ndkDirectory:ndk目录(一般在local.properties中)

packagingOptions:packaging的可选参数

productFlavors:项目所有flavor

publishNonDefualt:不仅仅使用默认的publish artifacts。可参考defualtPublishConfig。

resourcePrefix:创建新资源时使用的前缀。

sdkDirectory:sdk目录(一般在local.properties中)

signingConfigs:签名文件的可选项参数

sourceSets:资源文件目录指定(Android中有自己的AndroidSourceSets,这个一般用于assets,jin等目录)

splits:splits类型。

testBuildType:测试构建类型

testOptions:测试可选项参数

testVariants:测试变体

unitTestVariants:单元测试变体

variantFilter:变体过滤器

而这些构建配置要体现在不同的构建配置文件中,典型的Android应用结构为: 

1.1 Gradle Settings 文件

位于工程根目录的 settings.gradle 文件用于告诉Gradle构建应用时需要包含哪些 module,如 :

include':app',':lib'

对于setting.gradle中也可以写代码的,可以参考:

https://kymjs.com/code/2018/02/25/01/

1.2 顶层 Build 文件

位于工程根目录的 build.gradle 文件用于定义工程所有 module 的构建配置,一般顶层 build 文件使用 buildscript 代码块定义 Gradle 的 repositories 和 dependencies,如自动生成的顶层 build 文件:

/**

* buildscript代码块用来配置Gradle自己的repositories和dependencies,所以不能包含modules使用的dependencies

*/

buildscript {

/**

* repositories 代码块用来配置 Gradle 用来搜索和下载依赖的仓库

* Gradle 默认是支持像 JCenter,Maven Central,和 Ivy 远程仓库的,你也可以使用本地仓库或定义你自己的远程仓库

* 下面的代码定义了 Gradle 用于搜索下载依赖的 JCenter 仓库和 Google 的 Maven 仓库

*/

repositories {

google()

jcenter()

}

/**

* dependencies 代码块用来配置 Gradle 用来构建工程的依赖,下面的代码表示添加一个

* Gradle 的 Android 插件作为 classpath 依赖

*/

dependencies {

classpath'com.android.tools.build:gradle:3.0.1'

}

}

/**

* allprojects 代码块用来配置工程中所有 modules 都要使用的仓库和依赖

* 但是你应该在每个 module 级的 build 文件中配置 module 独有的依赖。

* 对于一个新工程,Android Studio 默认会让所有 modules 使用 JCenter 仓库和 Google 的 Maven 仓库

*/

allprojects {

repositories {

google()

jcenter()

}

}

除了这些,你还可以使用 ext 代码块在这个顶层 build 文件中定义工程级(工程中所有 modules 共享)的属性:

buildscript {...}

allprojects {...}

ext {

// 如让所有 modules 都使用相同的版本以避免冲突

compileSdkVersion =26

supportLibVersion ="27.0.2"

...

}

...

每个 module 的 build 文件使用 rootProject.ext.property_name 语法使用这些属性即可:

android{

compileSdkVersionrootProject.ext.compileSdkVersion

...

}

...

dependencies {

compile"com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"

...

}


自定义BuildConfig

实际开发中服务器可能有正式环境和测试环境,gradle可以通过buildConfigField来配置。

defaultConfig {

        buildConfigField 'String','API_SERVER_URL','"http://url/"'    }

buildConfigField 一共有3个参数,第一个是数据类型,和Java的类型是对等的;第二个参数是常量名,这里是API_SERVER_URL;第三个参数就是你要配置的值。

defualtConfig{}

defaultConfig{}是所有flavor都共有的配置。英文解释:Thedefaultconfiguration, inherited by all product flavors(ifanyaredefined).defaultConfig的使用:defaultConfig {        applicationId"com.example.zhang.demo"minSdkVersion15targetSdkVersion25versionCode1versionName"1.0"testInstrumentationRunner"android.support.test.runner.AndroidJUnitRunner"}

如果项目中包含多个Module,可以将共有的minSdkVersion和targetSdkVersion抽取到Project中的build.gradle文件中。具体细节下一章节。

1.3 Module 级 Build 文件

位于每个 project/module/ 目录的 build.gradle 文件用于定义该 module 自己的构建配置,同时你也可以重写顶层 build 文件或 main app manifest 的配置:

/**

* 为这个构建应用 Gradle 的 Android 插件,以便 android 代码块中 Android 特定的构建配置可用

*/

apply plugin:'com.android.application'

/**

* android 代码块用来配置 Android 特定的构建配置

*/

android {

/**

* compileSdkVersion 用来指定 Gradle 用来编译应用的 Android API level,也就是说

* 你的应用可以使用这个 API level 及更低 API level 的 API 特性

*/

compileSdkVersion26

/**

* buildToolsVersion 用来指定 SDK 所有构建工具、命令行工具、以及 Gradle 用来构建应用的编译器版本

* 你需要使用 SDK Manager 下载好该版本的构建工具

* 在 3.0.0 或更高版本的插件中。该属性是可选的,插件会使用推荐的版本

*/

buildToolsVersion"27.0.3"

/**

* defaultConfig 代码块包含所有构建变体(build variants)默认使用的配置,也可以重写 main/AndroidManifest.xml 中的属性

* 当然,你也可以在 product flavors(产品风味)中重写其中一些属性

*/

defaultConfig {

/**

* applicationId 是发布时的唯一指定包名,尽管如此,你还是需要在 main/AndroidManifest.xml 文件中

* 定义值是该包名的 package 属性

*/

applicationId'com.example.myapp'

// 定义可以运行该应用的最小 API level

minSdkVersion15

// 指定测试该应用的 API level

targetSdkVersion26

// 定义应用的版本号

versionCode1

// 定义用户友好型的版本号描述

versionName"1.0"

}

/**

* buildTypes 代码块用来配置多个构建类型,构建系统默认定义了两个构建类型: debug 和 release

* debug 构建类型默认不显式声明,但它包含调试工具并使用 debug key 签名

* release 构建类型默认应用了混淆配置

*/

buildTypes {

release {

minifyEnabledtrue

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

}

}

/**

* 由于 product flavors 必须属于一个已命名的 flavor dimension,所以你至少需要定义一个 flavor dimension

* 如定义一个等级和最小 api 的 flavor dimension

*/

flavorDimensions"tier","minApi"

productFlavors {

free {

// 这个 product flavor 属于 "tier" flavor dimension

// 如果只有一个 dimension 那么这个属性就是可选的

dimension"tier"

...

}

paid {

dimension"tier"

...

}

minApi23 {

dimension"minApi"

...

}

minApi18 {

dimension"minApi"

...

}

}

/**

* 你可以使用 splits 代码块配置为不同屏幕分辨率或 ABI 的设备生成仅包含其支持的代码和资源的 APK

* 同时你需要配置 build 文件以便每个 APK 使用不同的 versionCode

*/

splits {

density {

// 启用或禁用构建多个 APK

enablefalse

// 构建多个 APK 时排除这些分辨率

exclude"ldpi","tvdpi","xxxhdpi","400dpi","560dpi"

}

}

}

/**

* 该 module 级 build 文件的 dependencies 代码块仅用来指定该 module 自己的依赖

*/

dependencies {

implementation project(":lib")

implementation fileTree(dir:'libs',include: ['*.jar'])

implementation'com.android.support:appcompat-v7:27.0.2'

}

1.4 Gradle 属性文件

位于工程根目录的 gradle.properties 文件和 local.properties 用来指定 Gradle 构建工具自己的设置。 

gradle.properties 文件可以用来配置工程的 Gradle 设置,如 Gradle 守护进程的最大堆栈大小

local.properties 文件用来配置构建系统的本地环境属性,如 SDK 安装路径,由于该文件内容是 Android Studio 自动生成的且与本地开发环境有关,所以你不要更改更不要上传到版本控制系统中。

2

Gradle 概述

Gradle 是专注于灵活性和性能的开源自动构建工具。Gradle 的构建脚本使用 Groovy 或 Kotlin 语言。Gradle 构建工具的优势在于:

高度可定制 - Gradle 是以最基本的可定制和可扩展的方式模块化的

更快 - Gradle 通过重用之前执行的输出、只处理更改的输入以及并行执行 task 的方式加快构建速度

更强大 - Gradle 支持跨多语言和平台,是 Android 官方构建工具,支持很多主流 IDE,包括 Android Studio、Eclipse、IntelliJ IDEA、Visual Studio 2017 以及 XCode,将来会支持更多语言和平台

2.1 Gradle 的依赖管理

依赖管理(Dependency management)是每个构建系统的关键特征,Gradle 提供了一个既容易理解又其他依赖方法兼容的一流依赖管理系统,如果你熟悉 Maven 或 Ivy 用法,那么你肯定乐于学习 Gradle,因为 Gradle 的依赖管理和两者差不多但比两者更加灵活。

Gradle 依赖管理的优势包括:

传递依赖管理 - Gradle 让你可以完全控制工程的依赖树

支持非托管依赖 - 如果你只依赖版本控制系统或共享磁盘中的单个文件,Gradle 提供了强大的功能支持这种依赖

支持个性化依赖定义 - Gradle 的 Module Dependencies 让你可以在构建脚本中描述依赖层级

为依赖解析提供完全可定制的方法 - Gradle 让你可以自定义依赖解析规则以便让依赖可以方便地替换

完全兼容Maven和Ivy - 如果你已经定义了 Maven POM 或 Ivy 文件,Gradle 可以通过相应的构建工具无缝集成

可以与已存在的依赖管理系统集成 - Gradle 完全兼容 Maven 和 Ivy 仓库,所以如果你使用 Archiva、Nexus 或 Artifactory,Gradle 可以100%兼容所有的仓库格式

2.2 常用的依赖配置

Java Library插件 继承自 Java插件,但 Java Library 插件与 Java 插件最主要的不同是 Java Library 插件引入了将 API 暴露给消费者(使用者)的概念,一个 library 就是一个用来供其他组件(component)消费的 Java 组件。 

Java Library 插件暴露了两个用于声明依赖的 Configuration(依赖配置):

api 和 implementation。出现在 api 依赖配置中的依赖将会传递性地暴露给该 library 的消费者,并会出现在其消费者的编译 classpath 中。而出现在 implementation 依赖配置中的依赖将不会暴露给消费者,也就不会泄漏到消费者的编译 classpath 中。因此,api 依赖配置应该用来声明library API 使用的依赖,而 implementation 依赖配置应该用来声明组件内部的依赖。implementation 依赖配置有几个明显的优势:

依赖不会泄漏到消费者的编译 classpath 中,所以你也就不会无意中依赖一个传递依赖了

由于 classpath 大小的减少编译也会更快

当 implementation 的依赖改变时,消费者不需要重新编译,要重新编译的很少

更清洁地发布,当结合新的 maven-publish 插件使用时,Java librariy 会生成一个 POM 文件来精确地区分编译这个 librariy 需要的东西和运行这个 librariy 需要的东西

那到底什么时候使用 API 依赖什么时候使用 Implementation 依赖呢?

这里有几个简单的规则: 

一个 API 是 library binary 接口暴露的类型,通常被称为 ABI (Application Binary Interface),这包括但不限于:

父类或接口用的类型

公共方法中参数用到的类型,包括泛型类型(公共指的是对编译器可见的 public,protected 和 package private)

public 字段用到的类型

public 注解类型

相反,下面列表重要到的所有类型都与 ABI 无关,因此应该使用 implementation 依赖:

只用在方法体内的类型

只出现在 private 成员的类型

只出现在内部类中的类型

例如

// The following types can appear anywhere in the code

// but say nothing about API or implementation usage

importorg.apache.commons.httpclient.*;

importorg.apache.commons.httpclient.methods.*;

importorg.apache.commons.lang3.exception.ExceptionUtils;

importjava.io.IOException;

importjava.io.UnsupportedEncodingException;

publicclassHttpClientWrapper{

privatefinalHttpClient client;// private member: implementation details

// HttpClient is used as a parameter of a public method

// so "leaks" into the public API of this component

publicHttpClientWrapper(HttpClient client){

this.client = client;

}

// public methods belongs to your API

publicbyte[] doRawGet(String url) {

GetMethod method =newGetMethod(url);

try{

intstatusCode = doGet(method);

returnmethod.getResponseBody();

}catch(Exception e) {

ExceptionUtils.rethrow(e);// this dependency is internal only

}finally{

method.releaseConnection();

}

returnnull;

}

// GetMethod is used in a private method, so doesn't belong to the API

privateintdoGet(GetMethod method)throwsException{

intstatusCode = client.executeMethod(method);

if(statusCode != HttpStatus.SC_OK) {

System.err.println("Method failed: "+ method.getStatusLine());

}

returnstatusCode;

}

}

其中,public 构造器 HttpClientWrapper 使用了 HttpClient 参数暴露给了使用者,所以属于 API 依赖。而 ExceptionUtils 只在方法体中出现了,所以属于 implementation 依赖。

所以 build 文件这样写:

dependencies{

api'commons-httpclient:commons-httpclient:3.1'

implementation'org.apache.commons:commons-lang3:3.5'

}

因此,应该优先选择使用 implementation 依赖:缺少一些类型将会直接导致消费者的编译错误,可以通过移除这些类型或改成 API 依赖解决。 

compileOnly 依赖配置会告诉 Gradle 将依赖只添加到编译 classpath 中(不会添加到构建输出中),在你创建一个 Android library module 且在编译时需要这个依赖时使用 compileOnly 是个很好的选择。但这并不能保证运行时良好,也就是说,如果你使用这个配置,那么你的 library module 必须包含一个运行时条件去检查依赖是否可用,在不可用的时候仍然可以优雅地改变他的行为来正常工作,这有助于减少最终 APK 的大小(通过不添加不重要的transient依赖)。 

runtimeOnly 依赖配置告诉 Gradle 将依赖只添加到构建输出中,只在运行时使用,也就是说这个依赖不添加到编译 classpath 中。 

此外,debugImplementation 会使依赖仅在 module 的 debug 变体中可用,而如 testImplementation、androidTestImplementation 等依赖配置可以更好地处理测试相关依赖。

2.3 声明依赖

2.3.1 声明 binary 依赖

现在的软件工程很少单独地构建代码,因为现在的工程通常为了重用已存在且久经考验的功能而引入外部库,因此被称为 binary dependencies。Gradle 会解析 binary 依赖然后从专门的远程仓库中下载并存到 cache 中以避免不必要的网络请求: 

每个 artifact 在仓库中的 coordinate 都会包含 groupId 、 artifactId 和 version 三个元素,如在一个使用 Spring 框架的 Java 工程中添加一个编译时依赖:

apply plugin:'java-library'

repositories {

mavenCentral()

}

dependencies {

implementation'org.springframework:spring-web:5.0.2.RELEASE'

}

Gradle 会从 Maven中央仓库https://search.maven.org/ 解析并下载这个依赖(包括它的传递依赖),然后使用它去编译 Java 源码,其中的 version 属性是指定了具体版本,表明总是使用这个具体的依赖不再更改。

当然,如果你总是想使用最新版本的 binary 依赖,你可以使用动态的 version,Gradle 默认会缓存 24 小时:

implementation'org.springframework:spring-web:5.+'

有些情况开发团队在完全完成新版本的开发之前为了让使用者能体验最新的功能特色,会提供一个 changing version,在 Maven 仓库中 changing version 通常被称作 snapshot version,而 snapshot version会包含-SNAPSHOT后缀,如:

implementation'org.springframework:spring-web:5.0.3.BUILD-SNAPSHOT'

2.3.2 声明文件依赖

工程有时候不会依赖 binary 仓库中的库,而是把依赖放在共享磁盘或者版本控制系统的工程源码中(JFrog Artifactory 或 Sonatype Nexus 可以存储解析这种外部依赖),这种依赖被称为 file dependencies ,因为它们是以不涉及任何 metadata(如传递依赖、作者)的文件形式存在的。如我们添加来自 ant、libs 和 tools 目录的文件依赖:

configurations{

antContrib

externalLibs

deploymentTools

}

dependencies {

antContribfiles('ant/antcontrib.jar')

externalLibs files('libs/commons-lang.jar','libs/log4j.jar')

deploymentTools fileTree(dir:'tools', include:'*.exe')

}

2.3.3 声明工程依赖

现在的工程通常把组件独立成 module 以提高可维护性及防止强耦合,这些 module 可以定义相互依赖以重用代码,而 Gradle 可以管理这些 module 间的依赖。由于每个 module 都表现成一个 Gradle project,这种依赖被称为 project dependencies 。在运行时,Gradle 构建会自动确保工程的依赖以正确的顺序构建并添加到 classpath 中编译。

project(':web-service') {

dependencies {

implementation project(':utils')

implementation project(':api')

}

}

3

Gradle 常用配置

强制所有的 android support libraries 使用相同的版本:

configurations.all {

resolutionStrategy {

eachDependency { details ->

// Force all of the primary support libraries to use the same version.

if(details.requested.group=='com.android.support'&&

details.requested.name !='multidex'&&

details.requested.name !='multidex-instrumentation') {

details.useVersion supportLibVersion

}

}

}

}

更改生成的 APK 文件名:

android.applicationVariants.all { variant ->

variant.outputs.all {

outputFileName ="${variant.name}-${variant.versionName}.apk"

}

}

如果开启了 Multidex 后在 Android 5.0 以下设备上出现了 java.lang.NoClassDefFoundError 异常,可能是由于构建工具没能把某些依赖库代码放进主 dex 文件中,这时就需要手动指定还有哪些要放入主 dex 文件中的类。在构建类型中指定 multiDexKeepFile 或 multiDexKeepProguard 属性即可: 

在 build 文件同级目录新建 multidex-config.txt 文件,文件的每一行为类的全限定名,如:

com/example/MyClass.class

com/example/MyOtherClass.class

android{

buildTypes{

release{

multiDexKeepFilefile('multidex-config.txt')

...

}

}

}

或者新建 multidex-config.pro 文件,使用 Proguard 语法指定放入主 dex 文件中的类,如:

-keepclasscom.example.** { *; }

android{

buildTypes{

release{

multiDexKeepProguardfile('multidex-config.pro')

...

}

}

}

参考: https://www.cnblogs.com/xinmengwuheng/p/5797048.html

https://blog.csdn.net/qq_33689414/article/details/53152212

文章来至:http://mp.weixin.qq.com/s/nDnw3fBtXSD8RHZXdvRsmA

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,567评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,358评论 6 343
  • 说明 本文主要介绍和Gradle关系密切、相对不容易理解的配置,偏重概念介绍。部分内容是Android特有的(例如...
    jzj1993阅读 15,361评论 1 62
  • bb2ec6e297b5阅读 121评论 0 0