Build Script完成的工作有两个:插件引入 和 属性配置,即对 Project
对象进行进一步的配置,生成Task的有向无环图。
① 插件引入
Gradle自身
并没有提供编译打包的功能,它只是一个 负责定义流程和规则的框架
,具体的编译工作都是由 插件
来完成的,比如编译Java用Java插件,编译Kotlin用Kotlin插件。
所以插件到底是什么?→ 答:定义Task,并具体执行这些Task的模板。
插件的两种类型:
- 脚本插件:存在于另一个脚本文件中的一段脚本代码;
- 二进制插件(编译成字节码):实现Plugin接口,通过编程的方式操作构建过程(项目或Jar包形式);
Gradle会内置一些核心插件,并提供简单名字,如 “java”,没在其中的插件则需采用完整名字,如:“org.jetbrains.kotlin:kotlin-gradle-plugin”,这个又称插件id,唯一不可重复!引入方式区别如下:
// 内置插件引入
apply plugin: ‘kotlin-android’
// 也可以使用plugins,不过有些插件不能指定版本,有些必须指定,要注意!
// 下面这种写法是Kotlin中的中缀表达式,apply→ 是否立即应用插件
plugins {
id(“org.jetbrains.kotlin.jvm”) version “1.3.71”
id(“org.jetbrains.kotlin.jvm”) version “1.3.71” apply false
java
build-scan
}
// 非内置插件引入,会将对应Jar文件放到Gradle的classpath下
buildscript {
repositories {
jcenter()
}
dependencies {
classpath “org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72”
}
}
② 属性配置
一旦应用了某个插件,就可以使用此插件提供的DSL进行配置,以此干预模块的构建过程。以Android构建为例:
// 引入android.application插件 → 为Project对象添加一个android{}配置闭包
apply plugin: ‘com.android.application’
android {
compileSdkVersion 29 // 使用API 29编译此模块
// 编译时的一些配置
defaultConfig {
applicationId “com.example.test”
minSdkVersion 26
targetSdkVersion 29
versionCode 1
versionName “1.0”
}
// 签名配置
signingConfigs {
release {
storeFile file(‘test.jks’)
storePassword ‘123456’
keyAlias ‘test’
keyPassword ‘123456’
}
}
// 构建类型配置
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile(‘proguard-android-optimize.txt’), ‘proguard-rules.pro’
signingConfig signingConfigs.release
}
}
// 编译选项配置
compileOptions{
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
除了插件另外引入的属性DSL外,Project对象也提供了很多用于配置构建的DSL,如 dependencies
配置编译依赖项,更多可以点进Project源码中自行查看。
另外根目录Build Script还可以使用一个 ext
属性用于Project间的数据共享、统一模块依赖版本。
// 根目录build.gradle配置
ext {
applicationId = “xxx.xxx.xxx”
buildToolsVersion = “28.0.3”
compileSdkVersion = 28
minSdkVersion = 22
…
}
// 子模块build.gradle使用
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
…
}
0x2、依赖规则
Gradle会声明每个依赖项的适用范围,可以理解为 分组
,如:有些依赖在编译时用到,有些则在运行时用到,Gradle通过 Configuration
来表示这个范围(分组),不同的Configuration通过不同的name区分。
许多Gradle插件会预置一些Configuration添加到你的项目中,如Java插件:
有点懵,没关系,看下Android内置的这些Configuration你就懂了(2.x是废弃的):
// 对应2.x的 compile,既参与编译又参与打包
implementation → 当前模块依赖,但不向其他模块暴露此依赖,编译时只能在本模块访问;
api → 当前模块依赖,且向其他模块暴露此依赖,同compile;
举例区分下:
模块A、B,如果A依赖了Gson库,B依赖A,使用implementation,B用不了Gson,使用api,B可以使用Gson。
// 对应2.x的provided和apk,用的较少
compileOnly → 编译时有效,不会参与打包;
runtimeOnly → 运行时有效,不会参与编译;
annotationProcessor → 注解处理器依赖
testCompile → 对应2.x的testImplementation,只在单元测试代码的编译以及最终打包测试apk时有效;
debugCompile → 对应2.x的debugImplementation,只在debug模式的编译和最终的debug apk打包时有效;
releaseCompile → releaseImplementation,只在release的编译和最终的release apk打包时有效;
附:四种依赖方式
// ① 本地library依赖
implementation project(“:mylibrary”)
// ② 本地二进制依赖
implementation files(‘libs/xxx.jar’, ‘libs/yyy.jar’) // 依赖特定库
implementation fileTree(dir: ‘libs’, include: [‘*.jar’]) // 依赖目录下的库
// ③ 远程二进制依赖
implementation(‘io.reactivex:rxandroid:1.2.1’)
// ④ AAR包依赖
implementation(name: ‘me.leolin:ShortcutBadger’, ext: ‘aar’) // 本地
implementation ‘me.leolin:ShortcutBadger:1.1.17@aar’ // 远程
当然,你也可以自定义一个Configuration,示例如下:
allprojects {
// 配置maven仓库地址
repositories {
maven { url “https://maven.aliyun.com/repository/jcenter” }
}
}
// 定义一个名为myDependency的Configuration
configurations {
myDependency
}
// 为自定义Configuration添加依赖
dependencies {
myDependency(‘io.reactivex:rxjava:1.1.9’)
myDependency(‘io.reactivex:rxandroid:1.2.1’)
}
// 打印自定义Configuration下载依赖后的文件地址
task showMyDependency {
println configurations.myDependency.asPath
}
终端键入:gradle showMyDependency,输出结果如下:
还可以调 extendsFrom
方法来继承另一个Configuration的所有dependencies,比如implementation就继承了compile。
最后还得提提两个标志,默认都为true:
- canBeResolved:编译时依赖
- canBeConsumed:运行时依赖
0x3、依赖创建的过程
Tips:跟源码了解下原理,不感兴趣可以直接跳过,不影响后续学习~
1. 依赖识别
build.gradle
处点开 dependencies
,定位到了 Project
类:
看注释:传递的闭包由此Project的 DependencyHandler
执行,定位到此接口:
可以看到每个方法都返回 Dependency
实例,点开这个接口:
噢吼,接口定义了获取group、name、version的方法,回到 DependencyHandler
的 add()
方法,最多可传递三个参数:
implementation(io.reactivex:rxandroid:1.2.1) {
transitive = false
}
Dependency add(String configurationName, Object dependencyNotation, Closure configureClosure);
// 参数一一对应:
implementation、io.reactivex:rxandroid:1.2.1、后面跟着的大括号(依赖配置闭包)
AS只能跟到这里,接着用VS Code打开Gradle的源码,全局搜下 implements DependencyHandler
,定位到 DefaultDependencyHandler
类:
该类除了实现 DependencyHandler
接口外,还实现了一个**MethodMixIn
** 接口:
在讲解这个接口前,我们先来了解下Groovy语言的两个特性:invokeMethod 和 methodMissing,先介绍下前者:
package groovy.reflect
class InvokeTest1 {
def hello() {
‘执行Hello方法’
}
def invokeMethod(String name, Object args) {
return “未知方法
n
a
m
e
(
name(
name({args.join(‘,’)})”
}
static main(args) {
def it = new InvokeTest1()
println it.hello()
println it.foo(“test”, 28)
}
}
// 运行输出:
// 执行Hello方法
// 未知方法 foo(test, 28)
对于一个对象的方法调用,类中有此方法就分发给此方法,如果不能分派,就调用invokeMethod方法,而methodMissing同样能实现上面的效果:
package groovy.reflect
class InvokeTest1 {
def hello() {
‘执行Hello方法’
}
def methodMissing(String name, Object args) {
return “未知方法
n
a
m
e
(
name(
name({args.join(‘,’)})”
}
static main(args) {
def it = new InvokeTest1()
println it.hello()
println it.foo(“test”, 28)
}
}
输出结果相同,而在Groovy中invokeMethod是用来 分发一个对象的所有方法 (已实现和未实现)的,要借助 GroovyInterceptable
接口。而methodMissing则只能 分发一个类未实现的方法,无论它是否实现了GroovyInterceptable接口。
总结下就是:invokeMethod管理所有方法,methodMissing只管理类所有的未实现方法!
弄懂后回到 MethodMixIn
接口,其实就是Gradle对methodMissing的封装,类想要实现这个特性,只需实现此接口,接口中定义了一个抽象方法 getAdditionalMethods()
返回一个 MethodAccess
对象:
定义了两个方法:判断某Method是否存在,动态执行Method,如出一辙,可以,跟下 getAdditionalMethods
重写处:
跟下哪里给 dynamicMethods
属性赋值:
跟下 DynamicAddDependencyMethods
:
参数个数判断,最后都调用到 dependencyAdder.add()
,而 DependencyAdder
是一个内部接口,跟下哪里实现了:
实际上还是调用的 DefaultDependencyHandler
的 doAdd()
方法:
判断dependencyNotation是否为Configuration对象,如果存在,就让当前的configuration对象继承dependencyNotation,即将添加到dependencyNotation的依赖都添加到configuration中。
2. 依赖创建
往下一点,可以看到 DefaultDependencyHandler
调用 create()
方法创建了一个 Dependency
的实例,跟下:
跟下:DefaultDependencyFactory → createDependency()
调用 dependencyNotationParser
实例的 parseNotation()
创建了 Dependency
实例,往上跟下:
构造方法里设置了这个参数,跟下哪里传入的:
跟下:DependencyNotationParser → parser()
可以看到好几种类型的 NotationConverter
(依赖转换器):
// ① DependencyStringNotationConverter、DependencyMapNotationConverter 针对下面这种:
implementation(io.reactivex:rxandroid:1.2.1) {
transitive = false
}
// ② DependencyFilesNotationConverter 针对下面这种:
implementation fileTree(dir:‘libs’, include:[‘*.jar’])
// ③ DependencyProjectNotationConverter 针对下面这种:
implementation project(“:applemodule”)
// ④ DependencyClasspathNotationConverter 针对claspath依赖的情况
所以就是利用各种类型的转换器,解析成各种不同的依赖,点开没个转换器,可以看到生成的依赖有这两种:SelfResolvingDependency
和 ProjectDependency
,打开前者:
注释写道:SelfResolvingDependency是独立于Repository,可以自解析的依赖。而后者则依赖于另一个项目:
那就来跟一跟吧~
3. ProjectDependency
跟下:DependencyProjectNotationConverter
跟下:DefaultProjectDependencyFactory → create()
instantiator.newInstance 用于实例化一个具有DSL特性的对象,此处返回了一个 ProjectDependency
实例,而另外一个create()方法,则传多了一个configuration名称。
关于依赖创建的过程先了解到这里,继续往下走涉及到artifacts的东西,后续章节在继续跟,先总结下:
- ① DependencyHandler没有实现implementation、api这类方法(插件实现),利用MethodMissing机制间接调用这些方法;
- ② 不同的依赖声明由不同的转换器进行转换,最后转换为SelfResolvingDependency和ProjectDependency两类依赖对象;
0x4、依赖冲突解决
来到实用解决问题环节,在模块化,或者依赖别人开源库的时候,依赖冲突问题总是避无可避~
① xxx Not Fount
- 编译期:一般就是没有依赖正确的库导致;
- 运行期:一般是使用了compileOnly导致某些库只在编译时依赖;
② Program type already present com.xxx.XXX
可以点击右侧的Gradle面板中的:Tasks → android → android dependencies 查看依赖树:
也可以执行下述命令将依赖树输出到特定文件中,方便检索:
./gradlew :app:dependencies > dependencies.txt
还可以分情况查看,如:
gradlew :module_base:dependencies --configuration api
查看指定库的依赖情况
/gradlew :app:dependencyInsight –dependency 指定库 –configuration compile
使用build scan分析依赖,生成HTML可读性和界面好康些~
./gradlew build --scan
接着就是根据编译报错结果定位到出问题的类,然后在依赖树中找到对应冲突的包了,接着是各类处理冲突的方法决策了。
③ 排除 & 禁用依赖传递
打开上面的依赖树,会发现有些依赖标注了 *
号,表示这个依赖被忽略了。
这就涉及到了 传递依赖
,Gradle解析一个库时,会自动下载它的依赖库,以及此依赖库的依赖库(递归),然后再处理这些众多依赖库的版本匹配,就很容易出现依赖冲突的问题了。
一种简单的解决方法就是:引用依赖库时,排除某个引起冲突的依赖库,到他时不往下传递,如:
implementation(“io.reactivex.rxjava2:rxandroid:2.1.1”) {
exclude(group = “io.reactivex.rxjava2”, module = “rxjava”)
exclude(group = “io.reactivex.rxjava2”)
}
// 全局配置排除
configurations {
compile.exclude module: ‘xxx’
all*.exclude group:‘xxx.xxx’, module: ‘XXX’
}
另一种就是 禁用依赖传递
,示例如下:
implementation(“io.reactivex.rxjava2:rxandroid:2.1.1”) {
transitive = false
}
// 全局禁用
configurations.all {
transitive = false
}
④ 强制使用当前版本
通过添加 isForce == true
强制使用特定版本,同一个模块多个版本都被force,以第一个为准:
dependencies {
implementation(“io.reactivex.rxjava2:rxjava:2.2.6”) {
isForce = true
}
implementation(“io.reactivex.rxjava2:rxjava:2.2.10”) {
isForce = true
}
}
上述的最终版本为2.2.6,force只作用于当前模块,不同模块间force相互独立,还要注意:模块force的某个依赖版本低于主模块的版本低,会发生编译错误,所以尽量不要在lib里force某个依赖的版本。
当然硬要这样搞也可以,主项目里再force一下(高低版本都可),或者是用下述代码替换依赖版本:
configurations.all {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
def requested = details.requested
if (requested.group == ‘com.android.support’) {
if (!requested.name.startsWith(“multidex”)) {
details.useVersion ‘27.1.1’//27.1.1为当前版本号
}
}
}
}
最后
都说三年是程序员的一个坎,能否晋升或者提高自己的核心竞争力,这几年就十分关键。
技术发展的这么快,从哪些方面开始学习,才能达到高级工程师水平,最后进阶到Android架构师/技术专家?我总结了这 5大块;
我搜集整理过这几年阿里,以及腾讯,字节跳动,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
2021年虽然路途坎坷,都在说Android要没落,但是,不要慌,做自己的计划,学自己的习,竞争无处不在,每个行业都是如此。相信自己,没有做不到的,只有想不到的。祝大家2021年万事大吉。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。
[外链图片转存中…(img-5QJcYvOv-1715143794402)]
[外链图片转存中…(img-zDdPdbdu-1715143794403)]
[外链图片转存中…(img-3C5gWvHu-1715143794404)]
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
2021年虽然路途坎坷,都在说Android要没落,但是,不要慌,做自己的计划,学自己的习,竞争无处不在,每个行业都是如此。相信自己,没有做不到的,只有想不到的。祝大家2021年万事大吉。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!