原创 xuduokai 百度App技能

前语

无论是否意识到 Gradle 的存在,每位 Android 程序员都会直接或间接的与 Gradle 打交道。每逢经过 Android Studio 新建一个工程时,AS 都会主动创立一个通用的目录结构,然后+ ( m U A O E就能够进行开K : a发,在 apw L 2 4p 的 build.gradle 中添加一些依靠,点击右上角的 Sync Now,编写代码,点击绿色小箭头 Run 运转代码,全部都这么美好,除了偶{ I !然Sync Now 会失败变) O S为 Try Again,假如经过几次 Try Again,问题还没有处理,翻h | 4 | |开浏览器,张贴对应报错,查找处理方案,复制张贴处理方案,点击 Try AU k P /gain,问题处理,日子仍是那么美好。

假如你u ) O @ [ K想要了解问. u u , $ ` ) {题为什么会发作,为什么这样做会处理问题7 5 } 8 , i =,那么你需求脱离 AS 那几个常见的按钮,探究它背面的秘密—Gradle,这一主动化构建东西。

本文将介绍:

  1. 为什么需求主动化构建东西?
  2. 默许创立的 Android 工程都有什么
  3. 依靠办理
  4. 打包流程

经过阅览本文,能够G X H { C ~ Q大致了解 Gradle 是怎样作业,能够有针对性搜r & O 1 ] m l索相关b R b [ +内容,更加快速的处理常见编译过错

为什么需求主动化构建东西?

以下指令只是示例,便利阐明问题,具体运用办法请查找b f 8 c $ O | ` )相关指令手册用法

咱们知道,一个 APK 包其实是一个 zip 包D v N 2 M 0 ` g 1,包含代码和资源。那么咱们能够编写一个Shell 脚本,命} 8 u名为 assemble.sh,任何人只要经过履行这个脚本就能够得到 apk 包,完美:

  1. 将 .java 文件转换为 .P J N P – xclass 文件,履行指令: javac xxx.java
  2. Android 还会将 .class0 * & : J B 7 s 文件转换为 .d| $ M P ex 文件: dx xxx.class
  3. 打包成 apk: zip xxx.apk [需求打s 9 + S C ~ a } V包的代码和资源]

在 Android 中代码Z N j对资源是经过 R.java 文件引用,i s c i ]所以需求持续添加指令,并要求这个指令在 javac 指令前履行。在实践开发中咱们不或许: U . ?全部功用都自己完成,有或许会依靠优异的开源库,修正后的伪代码如下:

  1. 生成 R.F : 3 @ r ]java: aapt [资源文件]
  2. 将 .java 文件转换为 .class 文件,履行指令H H B f: javac xxx.java R.java -classpath xxx.jar
  3. Android 还会将 .class 文件转换为 .dex 文件: dx xxx.class6 U 6 b R.class xxx.jar
  4. 打包成 apk: zip xxx.apL c v } B k [需T t R ^ h求打包的代码和资t ^ ! R源]

全部似乎都尽在把握之中,真的吗?让咱们看看 Android APK 实践打包的流程是什f ( w V r l L J么样的:

想想完成如此杂乱流程的 Shell 脚本是不是有些头大?别急,完成后还会遇到下面这些问题:

  • 关于多个工程,每个工程都需求拷贝上述 Shell 脚本
  • 关于单个工程,每次添加一个功用都需求在原有流程中刺进一段代码,随着需求添加,脚本难以维护
  • 怎样办理引入的外部依靠?怎样打 debug、release 包?怎样打多渠道包?

此刻咱们需求一个简0 ] q U E 0 X Z 8化上述进程的东西,经过一些约好,如将代码、资源等放在指定目录,再i { f J f O F +辅以构建脚本就能够快速得到终究的构建产品4 t . V,这便是主动化构建东西w & = @ 5 Z H K,而 Gradle 便是其间一个。

对照刚刚那个简略的比方,每一个工程在 Gradle 中叫做一个 Project,每一个需求履行的任务,如生成 R 文件、编译 java 文件等,在 Gradle I A x * d 中叫做一个 Task。经过 TaskA.dependsOn(TasY l * M ikB# – ^ t * R G +)能够完成先履行 TaskB 再履行 TaskA 的作用。一起 Gradle 也供给 doFirst、doLast 答应在每个 Task 前和后履行一些代码。

至此,咱们知道为什么需求主动化构建东西:

  • 防止手动介入构建
  • 创立可重复的构建
  • 以及最重要的:提高编程功率,将精力会集在需求开2 I X y O f +发上

默许创立的 Android 工程都有什么

每逢经过 Android Studio 新建一个工程时,AS 都会主动创立如上图所示的目录结构,图片中简略介绍了各个目录是干什么的,接下来为咱们具体介绍每一个目录或许文件的意义:

.gradle 与 .idea

.gradle 与 .idea 寄存 Gradle 和 AS 关于当前工程的缓存。

最常见的一个运用便是点击 sync 后,AS 会在每个工程下生成 .iml 文件,他们与 .gradle、.idea 配合为咱们供给了代码提示等常见功用。所以假如你的代码飘红而你承认依靠没有问题,能够测Z | q h验下面步骤铲除 AS 缓存:

  • 删.idea 删.gradle 文件
  • 指令行履行 ./gradlew clean
  • 挑选 File ->g ` p T m invalidatZ 1 –e caches/restart
  • Sync

gradle/wrapper 与 gradlew gradlew.bav Q 8t

当咱们初次装备 Android 环境时,需求装y [ R u ; i置 Java,装置 AS,但并不需求5 Y R U : z Q 1装置 Gs ; J B =radle,这其间便是 gradle/wrapper 的劳绩。

当履行 gradlew 脚本时,它能够确保每个| u A x * T Z u #项目都运用自己希望的 Gradle 版别,而其间的奥秘就在 gradlew 的这段代码中

exec "$JAVACM ) J oD" "${JVM_OPTS[@]}" -classpath 9 _ : G n $ N a i"$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

gradlew 并没有直接发动 Gradle 而是发动 gradle-wrappD ? 0er.jar,它会判别假如没有 Gradle 环境,从 gradle-wrapper.properties 中的 distributioR 3 6 –nUrl 中下载相应环境,并发动 Gradle。

由于 Gradle 答应指令行发动时附加参数来自界说 Gradle 的运转环境,所以百度app经过自界说 gradle-wrapper.jar,完成经过装备文件为不同内存大小的电脑、debug/release. 6 x e p 8 + E 包指定J ? W z . o不同 gradle 运转内存,提高咱们编译速度。

se0 x . v f _ = utting.gradle

setting.gradle 中最关键的便是其供给的 include 办法i 1 6 8 7,经过这个办{ v j _ ! M 0 Z法能够指定哪些工程需求参加编译,每一个参加编译的工程 Gradle 会为它创立一个 Project 目标

* 6 n U Q ] B ?D I e P ?录 build.gradle

首要是 builds0 E F 9 v 3crL # b # 2 7 ]ipt 代码块:
gradl0 = ] h ze 默许是自顶向下履行,无论 buildscript 代码块在哪,它都会第一个履行

接下来是 reposi] h f $ 6 # F 7 Ltories 和 dependencies:
repositories 表明 dependencies 声明的依靠去哪些库房找,google、jcenter、mavenCentral 都是第三方 Maven 库房。一起,也能够经过 maven 办法添加自己的 Maven 库房。需求留意的是,不应该假定组件一定会从特定库房拉O – ? C 7 : |取,假如 GR ) x 4 V Q ,radle 恳求一个库房超时,它会主动恳求v d 6 z @ t ~其他库房。

dependencies:代表 Gradle 履行需求哪些依靠。比方需求 Android Gradle Plugin 插件为咱们 ; r s y Q | 1 }打包 apk 包,就需求添加: classpath ‘com.android.tools.build:gradw g ( : r 6 ele:3.4.0′

终究是 allprojects 和 re: F } y Fpositories:
在 allproje6 D ` R . 4cts 中的装备会对全部工程收效而里边的 repositor* 1 F 3 h 7 c X vies 则表明工程声明的 dependencies 去哪些库房查找

app build.gradle

首要能够看到H = g & apply plugin: ‘com.andb X ^ 5roid.application’,当运用这个插件后,它会为咱们创立一系列 Taski L f b & b @ . *,比方 assembleDebug、assembleRelease,履行这些 Task,就会得到终究的 APK。

android 代码块是插件为咱们供给的 API答应咱们修正 Task 的行为。

dependencies 代码x T | i ~块的内容决议当前 Project 依靠哪些组件,而不同的依靠声明会有不同的结果,具体内容咱们鄙人一节剖析。

依靠办理

依靠装备

在 Android Gz Y ] 8 | , D X Iradle Plugin 3.0 年代,` g I 9 f L Y *Google 运用 implementation 和 api 选项取代曩昔的 compile 选项。既然接口都变了,Google 干S M –脆将L G j + D其他的装备项也进行了改名,便利咱们了解其装备的意义。需求留意的是,老版别的接口没有被立刻删去,可是鄙人一个首要版别中会被删去。下面是各个装备项的官方中文解释:

举个比方:
假定 A 依靠 B,B 依靠 C。
假如 B 对 C 运用 implementatioY Q 1 J Z V F O Kn 依靠,则 A 无法调用 C 的代2 = G a
假如 B 对 C 运用 api 依靠,则 A 能够调J K J , d用 C 的代码
假如 B 对 C 运用 compile, NOnly 依靠,则 A 无法调用 C 的代码,且 C 的代码不会被打包到 APK 中
假如 B 对 C 运用 runtimeOnly 依靠,则 A、B 无法调用 C 的代码,但 C 的代码会被打包到 APK 中

实践上每一Y g g P j个组件都有自己的[ 8 @ [ Z D compileClasspath 和 runtimeClasspath
当一个组件参加编译时,Gradle 就会将其s t _放在 compileClasspath 中
当一个组件参加打包时,Gradle 就会将其放在 runtimeClasspath 中

不同的依靠装备项,其实便是将声明的依靠放入不同组件的不同的 classpath 中,回到上面的比方
关于 implementation ,其实便是将 C 放入 B 的 compileClasspath 和 runtimeClassn y . B $ Hpath,放入 A 的 runtimeClasspath 中,然后完成 A 假如调用 C 的代码,在 A 的编译阶段 javac 报5 6 I U S o ^ f错,但终究 C 会被打包到 APK 包中

关于 api、compileOnQ F x s = o $ !ly、runtimeOnly 原理相同m e M m

源码与二进制

当想要依靠一个源码工程时只需求这样写: implementationu – k Z project(‘:demo:mylibrary’)

而且咱们能够明确知道 mylibrary 中的依靠都会被正确打包到 APK 中

当咱们依靠二进制需求这样写: implem& N @entation ‘androidx.appcompat:appcompat:1.0.2’

当履行依靠指令(只输出 release 包的 runtimeClasspath):
./gradlew :app:dependencies –configuration releaseRuntimeClasspath > depenv – + } b |dencies.txt

输出依靠联系图时会看w ` ; 9 4 ) +到并不是只是依靠一个 appcompat 组件(只显示部分依靠),还包含该组件自己的依靠,以及依靠的依靠b ; ? e 3 , / 2,直到组件本身没有依靠,这样的特性叫做依靠g R : I 7传递

release[ o R T k SRuntimeClasspath - Resolved configuration for runtime f& _ : X T u mor vM a G Fariant: relea1 r D 4 E 9se
--- androidx.appcompat:appcompat:1.0.2
+--- androidx.annotation:annotation:1.0.0
+--- android# Q # ! Xx.core:core:1.0.1
| +--- android7 V 0x.annotation:annotation:1.0.0
| +--- androidx.collection:collection:1.0.0
| | --- androidx.annotation:annoO 6 ] F # Ztation:1.0.0
| +--- androidx.lif : `ecycle:lifecyce m / ! % T k vle-runtime:2.0.0
| | +--- androidx.lM s , r ?ifR % 0 D @ K w 1 recycJ X + :le:lifecycle-common:2.0.0
| | | --- androidx.annotation:L 2 H j t ) | X lannotation:1.0.0
| | +--- androidx.7 E a S iarch.core:core-common:y , z N o ! :  d2.0.0
| | | --- androidx.annotation] 9 ] & ( D B ~ .:annotat^ K / { N ) Rion:1.0.F 3 i ~0
| | --- androidx.annotation:annota, d o v 8  C tion:1.0.0
| --- anG - [ v p edroidx.versionedparcelable:ve9 & Jrsionedparcelable:1.0.0
| +--- aQ i 3 ( * 5ndroidx.annotation:annotation:1.0.0
| --- androidx.collect~ N ` v # , v j ion:collection:1.0.0 (*)
+--- anG z ] Q 1 m * Idroidx.collection:collection:1.0.0 (*)
+--- androidx.cursoradapter:cursoradapter:1.0.0

那么 Gradle 是怎样确认这些依靠呢?当运用Maven 规范上传组件时,不单单会上传组件的二进制,还会上传一个 pom.xml 文件,依靠信息就在这个文件当中。

由于检查公共的 Maven 服务器有或许需求fq,下面给咱们展现百度app自己建立的服务器的后台,便利了解被上传的二进制在服务器是以怎样的结构寄存的

这个是百度app自己建立的_ ` : Maven 服务器后台,点击一项检查概况:

有上传的二进制 aar,也( : P G [ R E e :有 pom 文件,还有咱们在上传时自界说的/ : ! r ^ O T Z文件 readme

看完远端的 POM 文件,咱们在看看当二进制被下载后在本地是怎样寄存的

下面是一个简略的 POM 文件:

能够看到有两个 dependencm hy,需求留意的是 scope,也会分为 runtime 和 compile,ru^ n 4 q sntime 不会参加编译q k v,但会参加打包,compile 会参加编译和打包

两个实践比方:
一:假定 A 依靠 B,| h ) 3B 依靠 C

B 对 C 运用 implementation 依靠
B 中有类 FooT w H } { J + K 承继于 C中的 Bar
在 A 中运用类 Foo 时会报错找不到类 Barb n n l ^ 6 Y,处理办法只能让 A 再依靠 C,所以应该尽量防止运用承继
二:假定 A 依靠 B,B 依靠 C
BC 是二进制, B 的 POM 中对 C 的依靠是 runtimO ` 2 @ H g Se
在 Gradle 4.4 中,A 仍然能够调用 C 的代码,这个问题在 Gradle 5.0 后被修复

依靠抵触

什么是依靠抵触:

假定 ABf T 4 e u 5 OC 是源码,D 是e J ` 2 6 b I二进制,A 声明依靠 B,A 声明依靠 C,B 声明依靠 D 1.0 版别,C 声明依靠 D 1.1版V I ? T o 7 l别,这时,D 有依靠抵触,需求确认是运用 1.0 仍是 1.1 版别

怎样处理依靠抵触:

  1. 进行编译时,B 编译时依靠 D 的1.0版别,y a 6 d f 8 w /C 编译时依靠 D 的1.1版别,但终究打包到 apk 中 D 的版别是 1.1,由于版别号e # B _ | 8抵触默许挑选最高版别
  2. Gradle 为咱们供给了一系列处理依靠抵触的规矩如:不答应依靠传递,exclude 移除一个依靠,替换一个组件为另一个组件,这些办法就z % ` / – N A d不一一介绍了,按需百度即可
  3. 百度app在此根底o Y p 2 _ = v I上添加规矩:假如终究运用的版别号高于在 version.properties 界说的版别号则报错

留意:

  1. 假定 D 发布 1.2 版别,但 B、C 都没有根据 D 1.2 版a . f C别发布新版别,则终究M ? c z s 0 H打包仍是 D 的 1.1 版别,所以全部组件终究被打包到 APK 包中的版别 U U 1 =都为 version.properties 中界说的版别
  2. 假定 D 的 MavenId 由 D 改成了 E,C 根据 E 发布二进制,B 仍是老样K w f 8 J ~子,在实践打包中会报类重复的过错,原因便是 B 的 POM 文件中依靠的仍是 D,所以需求让 B 根据 D 改名后的 E重新发一个二进制

打包流程
有了前面这些铺垫,让咱们实F I 1 *践看看在履行打包 Task 时,实践还_ d 7 I y m } @履行了哪些 Task。环境装备如下:
Gradle 5.1.1
Andrb Q *oid Gradle Plugin 3.1.2
org.gradle.paralH a i – k . glel=true 敞开并c h ` i l t ` +行编译
release 包 minifyEnabled true

履行指令能够得到如? ( s i ) _ X下图所示输出

# --dry-run 表D o t明不实践履行每个 Task
gradlew assembleRelease+ Q d o A [ * w n --dry-run

Task 很多,接下来为咱们介绍几个要点的 Task,其余没介绍的感兴趣的同学T G p C J U . 2能够找找对应的& 9 ] % L完成类,看看它的完成。

preBuild

描绘:做一些编译前的检查
一个比方:有的人或许遇到下面的过错

"Android dependency "+ displayP [ ! F 5 - [+ "is set to compileOnly/provided which is not supported"

这个的原因便是由前面说过的 compileClasspath 和 runtimeClasspath 引起的。
当一个组件由于不同的依靠装备项导致它的 compileClasspath 比方为 1.1.1版别,但他的 runt0 z D : J Y @ u timeClasspath 是 1.1.2版别,preBuildV ~ F 就会检测出这个问题并报错咱们处理

compileReleaseAidl

类:AidlCompileP E `
描绘:内部运用 AidlProcessor 调用 call 办法运用 build-tool 下的 aidl 履行编译。

各类 geM D P Z % ! }nerate和 merge

这些 Task 答应咱们在整个编译工程中动态的生成一些代码,生成好– a P ] L的资源需求和F X x已有的资源进行兼并,而且需求留意有或许覆盖已有资源,就不再具x ~ P y a a t 1 x体介绍了,

进程

第一步:咱们有 app 工程下的 Java 源文件,N V B还有 AIDL、generate、R.java 等生成的 Java 源文件@ w s I,还有本身依靠,源码子工程的 jar 包、远端 aar 解压的 jar 包等一系列二进制文件,源码文件是 javac 需求编译的内容,二进制文件 .jar/.class 则是当 javac 编译遇到一个类名/ 0 ( i E G等符号时,假如发现在现有的源文件找w n E A B不到,该去哪找的集合,对应的 javac 参数便是 -classX @ K J V R 8path

而这个参数,其实便是 compileClasspath 的一个运用,假如你源文件引用了一个类,它的 jar 包不在 compileClasspath 中,那么在编译时 javac 就会报错找不到符号了

第二步:当源文件被编译成类文件后,Google 供给了 Transform 机制答应咱们对二进制文件在打包前进行修正,比方前面图片中的 :app:trw ) ^ D C 4ansfo8 d ~ WrmClassesWithXXXForRelease SKIPPED 便是咱们自界说的 Transform。经过 :app:t+ 5 ; W DransformClassesAndResourcesWithProguardForReleasC @ 0 ?e SKIPPED 也能够看到 Proguard 也是8 . @ 0 @ v 2 M经过 Tran# 1 q l 7 K 4 .sformo V 6 机制完成的,这里图片中一个 .classM * T n J m [ 文件,一个是 .jar/.class 文件,第一Y y ( p I d `个显然是 javac 编译后的产品,第二个则是 runtimeClasspath,便是那些需求被打包的二进制。信( Q 0 3 J F ~ ~ )任咱们这样就了解了 compileClasspath 和 runtimeClasspath 是怎样影响打包进e I . @ x j M

第三步:当 Transform 处理好全部的 class 文件后,接下来便是将 .class 文件转换为 .dex 文件。值得留意的是,javac 只能发现源码的问题,不会发现那些未参加编译的) B J a % k ]二进制的问题。而在 dex 转换进程中则能够发现比方类重复问e I 3 + $ ? s }题或许一个类,3 & 6 g l e名字不变,可是由 Class 变成了 Interface 这类严峻的代码问题。

第四步:便是将前面的和资源进行打包。对应的类是 PackageApplication,得到这个 Task 后能够对打包的内容进行自界说

结语

虽然只对常见的一些装备供给一个纲要,没有具体介绍 Gradle 和 Android Gradle Plugin 相关内容,比方 Gradle 的生命周期,插件开发,Tr| A r $ h @ y | Aansform 机制等等,可是假如各位看了以后能对整个编译东西链有一个大体的了解,能够在需求的时候明白该从哪个方向去处理问题,便是本文的价值所在了。

参考资料

docs.gradle.org/current/use…

developer.android.com/studio/buil…

github.com/gradle/grad…

android.googlesource.com/platform/to…

特别留意

欢迎同学们参加百度App移动研制团队% # w [ k 2 p B *,咱们崇尚务实、自在、敞开的技能文化。这里有完善的根底能力渠道,有各范畴的大牛,更有生长的时机。参加咱们,你得# w l s H `到的不只是是一份作业,更是一个改变国际的时机。

现在有AU c R xndroid/iOS/FE北上深各级别岗位,能够经过百度招聘官网查询相关职位,或直接发简历到shenhang@baidu.com