0%

Homebrew

gradle-completion

1
2
3
4
5
6
7
8
9
10
11
12
13
14
brew install gradle-completion
echo $fpath | grep "/usr/local/share/zsh/site-functions"
# 执行结果
Add the following line to your ~/.bash_profile:
[[ -r "/usr/local/etc/profile.d/bash_completion.sh" ]] && . "/usr/local/etc/profile.d/bash_completion.sh"

Bash completion has been installed to:
/usr/local/etc/bash_completion.d
==> gradle-completion
Bash completion has been installed to:
/usr/local/etc/bash_completion.d

zsh completions have been installed to:
/usr/local/share/zsh/site-functions

Gradle 升级日志

5.0 包含了 Kotlin DSL 生产级支持,依赖版本对齐(类似 Maven BOM),任务超时,Java 11 支持

  • Kotlin DSL 1.0

  • 依赖版本对齐:

    • 同一逻辑组(platform)下的不同模块在依赖树中可以有相同的版本.也可以导入 Maven BOMs 定义 platform.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    dependencies {
    // 导入 BOM.此文件中版本将覆盖依赖树中的其他版本
    implementation(enforcedPlatform("org.springframework.boot:spring-boot-dependencies:1.5.8.RELEASE"))
    // 使用上面定义的版本
    implementation("com.google.code.gson:gson")
    implementation("dom4j:dom4j")
    // 覆盖上面的版本
    implementation("org.codehaus.groovy:groovy:1.8.6")
    }
  • Gradle build 初始化特性

  • 可搜索文档

  • 任务超时

  • 解析依赖时 HTTP 重试

  • 性能特性

    • Gradle 可以作为一个低优先级的进程启动: –priority low 或者 org.gradle.priority=low.可以保证 IDE/Browser 不卡,即使是一个很消耗资源的 gradle 任务.
    • 多任务输出属性不再禁用缓存.当使用 @OutputFilesOutputDirectories 标记一个Iterable类型,Gradle 通常会对该任务禁用缓存且输出如下信息:
    1
    Declares multiple output files for the single output property 'outputFiles' via @OutputFiles,@OutputDirectories or TaskOutputs.files()

    现在不会再阻止缓存了.对该任务禁用缓存的唯一理由可能是如果输出包含文件树.

  • Java 11 运行时支持

  • 插件授权特性

  • Provider 追踪产品任务:Provider API 实例提供了追踪某值和任务或生成该值的任务。

  • Gradle Native 特性

  • 推广特性

Gradle 编译语言指南

gradle 可以使用不同的语言或 DSL 来执行编译任务

基本概念

理解以下概念可以帮助书写 gradle 脚本.
首先,gradle 脚本是一种可配置脚本.当脚本执行时,会生成一个特定类型的配置对象.例如 build script 执行时,会生成一个类型为 Project 的配置对象.这个对象作为脚本的代理对象被使用.下面显示了 gradle 脚本对应的代理对象.

脚本类型 代理对象
build script Project
init script Gradle
settings script settings

在脚本内可以使用这些代理对象的属性和方法.

build script 结构

一段编译脚本由 0 个或多个语句和脚本块组成。语句可以包含方法调用,属性赋值,定义局部变量。一个脚本块是一个接受 closure 作为参数的方法调用.这个 closure 可以看作是一个当其执行时配置了一些代理对象的可配置 closure. 最顶层的脚本块如下

脚本块 描述
allprojects{} 配置此项目及其所有子项目
artifacts{} 配置此项目的发布产物
buildscript{} 配置此项目的 build script classpath
configurations{} 配置此项目的依赖配置项
dependencies{} 配置此项目的依赖
repositories{} 配置此项目的仓库
sourceSets{} 配置此项目的 source set
subprojects{} 配置此项目的子项目
publishing{} 配置由 publishing plugin 添加的 publishingExtension

一段编译脚本同时也是一个 groovy 脚本,所以可以包含能出现在 groovy 脚本中的元素,如 方法定义,类定义

核心类型

以下是在 gradle 脚本中常用的核心类型

类型 描述
Project 此接口是你在你的编译脚本中最常使用的 API。通过 Project,你可以使用 gradle 的所有特性
Task 一个 task 代表一个编译过程中的一个单一原子任务,如编译 class,生成 javadoc
Gradle 表示 gradle 的调用
Settings 声明实例化和配置要参与构建的 Project 实例的层次结构所需的配置
Script 此接口由所有的 gradle 脚本实现并添加一些 gradle 特定的方法。当你编译脚本类时将会实现此接口,你可以在脚本中使用此接口声明的方法和属性
JavaToolChain 一组用于编译 Java 源文件的工具
SourceSet 表示 Java 源文件和资源文件的逻辑组
SourceSetOutput 所有输出目录的集合(编译的类,处理过的资源等),SourceSetOutput 继承自 FileCollection
SourceDirectorySet 表示一组由一系列源文件目录组成的源文件。以及相关的 include 和 exclude pattern
IncrementalTaskInputs 提供通过增量任务访问任意需要被处理文件的途径
Configuration 表示一组产物及其依赖。从 ConfigurationContainer 中可以获取更多关于对 configuration 声明依赖或者管理 configuration 的信息
ResolutingStrategy 定义依赖解析的策略.例如强制固定依赖版本号,替代,冲突解决方案或快照
ArtifactResolutingQuery 发起可以解析特定组件的指定软件产物的查询的生成器
ComponentSelection 表示一个模块的组件选择器和某个组件选中规则中评估的候选版本的元组
ComponentSelectionRules 表示组件选中规则的容器.规则可以作为一个 configuration 的解析规则一部分被使用,而且独立的组件可以明确接受或拒绝该规则。既不接受也不拒绝的组件将被指定到默认的版本匹配规则
ExtensionAware 在运行时可以和其他对象一起被扩展的对象
ExtraPropertiesExtension Gradle 域对象的附加 ad-hoc 属性
PluginDependenciesSpec 在脚本中使用的声明插件的 DSL
PluginDependencySpec 插件依赖的可变规范
PluginManagementSpec 配置插件如何被解析
ResourceHandler 提供访问特定资源的工具方法的途径,例如创建各种资源的工厂方法
TextResourceFactory 创建由字符串、文件、存档实体等资源提供的 TextResources

type-safe model accessors

执行时机:plugins{} 块后,script 脚本之前.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// build.gradle.kts
plugins {
`java-library`
}

dependencies {
api("junit:junit;4.12")
implementation("junit:junit:4.12")
testImplementation("junit:junit:4.12")
}

configurations {
implementation {
resolutinStrategy.failOnVersionConflict()
}
}

sourceSets {
main {
java.srcDir("src/core/java")
}
}

java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}

tasks {
test {
testLogging.showExceptions = true
}
}

了解 type-safe accessors 不可用时要执行的操作

下面的脚本明确使用了 apply() 方法应用插件.编译脚本无法使用 type-safe accessors,因为 apply() 方法是在这个编译脚本内调用的。你可以采用其他技巧,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// build.gradle.kts
apply(plugin = "java-library")

dependencies {
"api"("junit:junit:4.12")
"implementation"("junit:junit:"4.12")
"testImplementation"("junit:junit:4.12")
}

configurations {
"implementation" {
resolutinStrategy.failOnVersionConflict()
}
}

configure<SourceSetContainer> {
named("main") {
java.srcDir("src/core/java")
}
}

configure<JavaPluginConvention> {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}

tasks {
named<Test>("test") {
testLogging.showException = true
}
}

下列情况下,type-safe accessor 不可用

  • 使用 apply(plugin = id) 应用插件
  • 项目 build script
  • 通过 apply(from = “script-plugin.gradle.kts”) 应用的脚本插件
  • 跨项目配置的插件

产物配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// build.gradle.jts
apply(plugin = "java-library")'

dependencies {
"api"("junit:junit:4.12")
"implementation"("junit:junit:4.12")
"testImplementation"("junit:junit:4.12")
}

configurations {
"implementation" {
resolutinStrategy.failOnVersionConflict()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// settings.gradle.kts
pluginManagement {
repositories {
google()
gradlePluginPortal()
}

resolutionStrategy {
eachPlugin {
if (requested.id.namespace == 'com.android') {
useModule('com.android.tools.build:gradle:${requested.version}')
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// build.gradle.kts
val check by tasks.existing
val myTask1 by tasks.registering

val compileJava by tasks.existing(JavaCompile::class)
val myCopy1 by tasks.registering(Copy::class)

va; assemble by tasks.existing {
dependsOn(myTask1)
}

val myTask2 by tasks.registering {
description = "some meaningful words"
}

val test by tasks.existing(Test::class) {
testLogging.showStackTrace = true
}

val myCopy2 by tasks.registering(Copy::class) {
from("source")
into("destination")
}

升级 Gradle Wrapper

如果已经有基于gradlew wrapper的项目,可以通过运行wrapper任务来指定需要的gradle版本.

1
./gradlew wrapper --gradle-version=5.3 --distribution-type=bin

当然没必要使用gradle wrapper来安装gradle.调用gradlewgradlew.bat将会下载并缓存指定版本的 Gradle.

1
./gradlew taska

CLI 自动补全

1
2
brew install gradle-completion
echo $fpath | grep "/usr/local/share/zsh/site-functions"

设计 gradle 插件

对 gradle 新手来说,实现 gradle 插件可能是一个坑:组织管理插件逻辑,测试、调试插件代码等.可以在获取到更多的信息.

###

通过指定 subproject 的路径,可以将本地任何路径下的代码导入工程中,供本地开发调试。

1
2
include ':lib'
project(':lib').projectDir = new File('xxx/xxx/lib')

com.android.application 插件

  • applicationVariants
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//  AppExtension 继承自 BaseExtension 唯一扩展的成员变量,它的参数类型是 DefaultDomainObjectSet,这是不同 buildType 及 Flavor 的集合,applicationVariants 最长的是它的 all 方法,如修改 apk 名字
def buildTime() {
return new Date().format("yyyy-MM-dd",TimeZone.getTimeZone("UTC"))
}

android {
applicationVariants.all { variant ->
variant.outputs.each { output ->
def outputFile = output.outputFile
if (outputFile != null && outputFile.name.endsWith('.apk')) {
def fileName = "${variant.buildType.name}-${variant.versionName}-${buildTime()}.apk"
output.outputFile = new File(output.outputFile.parent,fileName)
}
}
}
}
  • defaultConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
defaultConfig {
applicationId '**.**.**'
applicationIdSuffix '.two' //applicationId的后缀,可以用在想同时安装运行两个Flavor包的时候,比如同时安装debug包和Release包做一些对比。
minSdkVersion 14
minSdkVersion 14
targetSdkVersion 28
versionCode 1
versionName '1.0'
versionNameSuffix '.0' // versionName后缀
consumerProguardFiles 'proguard-rules.pro' //用于Library中,可以将混淆文件输出到aar中,供Application混淆时使用。
dimension 'api'
//给渠道一个分组加维度的概念,比如你现在有三个渠道包,分成免费和收费两种类型,
//可以添加一个dimension, 打渠道包的时候会自动打出6个包,而不需要添加6个渠道,
// 详细的说明可见 https://developer.android.com/studio/build/build-variants.html#flavor-dimensions。
externalNativeBuild { //ndk的配置,AS2.2之后推荐切换到cmake的方式进行编译。
cnamke {
cppFlags '-frtti --fexceptions'
arguments '-DANDROID_ARM_NEON=TRUE'
buildStagingDirectory './outputs/cmake'
path 'CMakeLists.txt'
version '3.7.1'
}
ndkBuild {
path 'Android.mk'
buildStagingDirectory './outputs/ndk-build'
}
}

javaCompileOptions {
annotationProcessorOptions { // 注解的配置
includeCompileClasspath true // 使用注解功能
arguments = [ eventBusIndex : 'org.greenrobot.eventbusperf.MyEventBusIndex' ] // AbstractProcessor 中可以读取到该参数
classNames
}
}

manifestPlaceholders = [key: 'value'] // manifest 占位符,定义参数给 manifest 调用,如不同的渠道id
multiDexEnabled true // 启用 multiDex
multiDexKeepFile file('multiDexKeep.txt') // 手动拆包,将具体的类放在主 dex
mutliDexKeepProguard file('multiDexKeep.pro') // 支持 proguard 语法,进行一些模糊匹配.

ndk {
abiFilterss 'x86','x86_64','armeabi' // 只保留特定的 abi 输出到 apk
}

proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' // 混淆文件的列表,如默认的 android 混淆文件及本地的 proguard 文件,切记不要遗漏 android 混淆文件,否则导致一些默认安卓组件无法找到

signingConfig {
storeFile file('debug.keystore') // 签名文件路径
storePassword 'android' // 签名文件密码
keyAlias 'androiddebugkey' // 别名
keyPassword 'android'
}

buildConfigField('boolean','IS_RELEASE','false') // 在代码中可以通过 BuildConfig.IS_RELEASE 调用。默认 false
resValue('string','appname','demo') // 在 res/value 中添加 <string name="appname" translatable="false">demo</string>
resConfigs 'cn','hdpi' // 指定特定资源,可以结合 productFlavors 实现不同渠道的最小的 apk 包.
}
  • productFlavors: 渠道包的列表,可覆盖 defaultConfig 的参数配置,形成自己的风味
  • flavorDimensionList: 添加纬度的定义
  • resourcePrefix: 在模块化开发中给每个模块指定一个特定的资源前缀,避免多模块使用相同的文件名后合并冲突,在 build.gradle 指定此配置后,AS 会检查不合法的资源命名并报错
  • buildTypes: 默认有 debug 和 release。
1
2
3
4
5
6
7
8
9
10
11
12
debug {
applicationIdSuffix '.debug'
versionNameSuffix '.1'
debugable true // 生成的 apk 是否可调试,debug -> true,release -> false
jniDebuggable true // 是否可以调试 NDK 代码,使用 lldb 进行 c/c++ 代码调试
crunchPngs true // 是否开启 png 优化,会对 png 图片做一次最优压缩,影响编译速度, debug -> false,release -> true
embedMicroApp true // Android Wear 支持
minifyEnabled true // 是否开启混淆
renderscriptDebuggable false // 是否开启渲染脚本
renderscriptOptimLevel 5 // 渲染脚本等级 默认 5
zipAlignEnabled true // 是否 zip 对齐优化,默认 true
}
  • ndkDirectory: 也可在 local.properties 中配置 ndk.dir=/Users/shuttle/Library/Android/sdk
  • sdkDirectory: ..
  • aaptOptions: 资源打包工具
1
2
3
4
5
6
aaptOptions {
additionalParameters '--rename-manifest-package','cct.cn.gradle.lsn13','-S','src/main/res2','--auto-add-overlay' // appt 执行时的额外参数
cruncherEnabled true // 对 png 进行优化检查
ignoreAssets '*.jpg' // 对 res 目录下的资源文件进行排除,把 res 文件下的所有 .jpg 文件打包到 apk 中
noCompress '.jpg' // 对所有 jpg 文件不压缩
}
  • adbExecutable: adb 路径
  • adbOptions
1
2
3
4
adbOptions {
installOptions '-r','-d' // 调用 adb install 命令时默认传递的参数
timeOutInMs 1000 // 执行 adb 命令的超时时间
}
  • compileOptions
1
2
3
4
5
6
compileOptions {
encoding 'UTF-8' // java 源文件的编码格式,默认 utf8
incrmental true // java编译是否使用 gradle 增量编译
sourceCompatibility JavaVersion.VERSION_1_7 // java 源文件编译的 jdk 版本
targetCompatibility JavaVersion.VERSION_1_7 // 编译出的 class 版本
}
  • dataBinding
1
2
3
4
5
dataBinding {
enabled = true
version = "1.0"
addDefaultAdapters = true
}
  • defaultPublishConfig: 指定发布的渠道及 BuildType 类型。在 Library 中使用,默认 Release.
  • signingConfigs: 签名配置列表,供不同渠道和 buildType 使用.
  • lintOptions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
lintOptions {
quiet true // true -> 不报告分析的进度
abortOnError false // true -> 发现错误时终止 gradle
ignoreWarnings true // true -> 只报告错误
absolutePaths true // true -> 当有错误时显示文件的全路径或绝对路径
checkAllWarnings true // true -> 检查所有问题,包括默认不检查问题
warningsAsErrors true // true -> 将所有警告视为错误
disable 'TypeographyFractions','TypographyQUotes' // 不检查给定问题 id
enable 'RtlHardCoded','RtlCompat','RtlEnabled' // 检查给定问题 id
check 'NewApi','InlinedApi' // 仅 检查给定问题 id
noLines true // 如果为 true,则在错误报告的输出中不包含源代码行
showAll true // true -> 对一个错误的问题显示它所在的所有地方,而不会截短列表等等。
lintConfig file('default-lint.xml') // 重置 lint 配置(使用默认的严重性等设置)
textReport true // true -> 生成一个问题的纯文本报告(default -> false)
textOuput 'stdout' // 默认写入输出结果的位置,可能是一个文件或 stdout
xmlReport false // true -> 生成 xml 报告,jenkins 可以使用
xmlOutput file("lint-report.xml") // 写入报告的文件,默认 line-results.xml
htmlReport true // html 报告
htmlOutput file('lint-report.html') // 写入报告的路径,可选(默认为构建目录下的 lint-results.html)
checkReleaseBuilds true // true -> 将使所有 release 构建都以 issus 的严重性级别为 fatal (serverity=false)的设置来运行 lint,且如果发现了致命(fatal)的问题,将会中止构建(由上面提到的 abortOnError 控制)
fatal 'NewApi','InlineApi' // 设置给定问题的严重级别(serverity)为 fatal(即将会在 release 构建期间检查,即使 lint 要检查的问题没有包含在代码中)
error 'Wakelock','TextViewEdits' // 设置给定问题的严重级别为 error
warning 'ResourceAsColor' // 设置给定问题的严重级别为 warning
ignore 'TypographyQUotes' // 设置给定问题的严重级别(serverity)为 ignore (和不检查该问题一样)
}
  • dexOptions: 热修复差分包
1
2
3
4
5
6
7
8
9
dexOptions {
additionalParameters '--minimal-main-dex','--set-max-idx-number=10000' // dx 命令附加参数
javaMaxHeapSize '2048m' // 执行 dx 时 Java 虚拟机可用的最大内存大小
jumboMode true // 开启大模式,所有的 class 打到一个 dex 中,可以忽略 65535 方法数的限制, 大于14版本可用
keepRuntimeAnnotatedClasses true // 在 dex 中是否保留 Runtime 注解,默认 true
maxProcessCount 4 // dex 中的进程数,默认 4
threadCount 4 // 默认线程数
preDexLibraries true // 对 library 预编译,提高编译效率,但 clean 时较慢,默认 true
}
  • packagingOptions
1
2
3
4
5
packagingOptions {
pickFirsts = ['META-INF/LICENSE'] // 当有重复文件时,打包会报错。此配置会使用第一个匹配的文件打包进入 apk
merge 'META-INF/LICENSE' // 重复文件会合并打包
exclue 'META-INF/LICENSE' // 打包时排除匹配文件
}
  • sourceSets
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sourceSets {
main {
res.srcDirs 'src/main/res'
jniLibs.srcDirs = ['libs']
aidl.srcDirs 'src/main/aidl'
assets.srcDirs 'src/main/assets'
java.srcDirs 'src/main/java'
jni.srcDirs 'src/main/jni'
renderscript.srcDirs 'src/main/renderscript'
resources.srcDirs 'src/main/resources'
manifest.srcFile 'src/main/AndroidManifest.xml'
}
// 除了 main,也可给不同的渠道指定不同的配置
free {

}
}
  • splits: google play 按 CPU/屏幕像素密度打包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
splits {
abi {
enable true // 开启 abi 分包
universalApk true // 是否创建一个包含所有有效动态库的 apk
reset() // 清空 defaultConfig 配置
include 'x86','armeabi' // 和 defaultConfig 做和集
eclude 'mips'
}

density {
enable true // 开启 density 分包
reset() // 清空默认
include 'xhdpi','xxhdpi' // 和集
exclude 'mdpi'
}

language {
enable true
include 'en','cn'
}
}
  • variantFilter: 过滤通过 flavor 和 buildType 构建的 apk
1
2
3
4
5
6
7
8
9
variantFilter { variant ->
def buildTypeName = variant.buildType.name
def flavorName = variant.flavors.name

if (flavorName.contains("360") && buildTypeName.contains("debug")) {
// 不生成匹配的 apk
setIgnore(true)
}
}
  • com.android.library
1
2
3
4
android.libraryVariants.all { variant ->
def mergedFlavor = variant.getMergedFlavor()
mergedFlavor.manifestPlaceholers = [hostName:'www.example.com']
}

  • config
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# 修改配置
git config --local # 对某个仓库有效
git config --global # 对当前用户所有仓库有效
git config --system # 对系统所有登录用户有效
# 查看配置
git config --list --local
git config --list --global
git config --list --system
# 打开编辑器修改config
git config -e # 仅对当前仓库有效
# 变更文件名
git mv readme readme.md # 避免 mv x y -> git add -> git rm
git log [分支]
--oneline # 一行显示
-n4 # 指定最新的几次提交
--all # 所有分支的提交
--graph # 图形化显示
# 打开内置帮助网页.
git help --web [log]
# gui
gitk --all
git branch -av
# 在 ./git/refs/heads/xxx 中查看信息
git cat-file -t master # 返回 git object model 类型: blog, tree, commit, tag
-p # 显示所有内容
# 删除分支
git branch -D [fixup]
# 变基
git rebase -i
# 暂存区和最近一次提交的区别
git diff --cached
# 将暂存区的内容丢弃
git reset HEAD -- <file>
# 将暂存区的内容恢复到本地
git checkout -- <file>
# 设置别名
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.st status
git config --global alias.last 'log -1 HEAD'
git config --global alias.unstage 'reset HEAD --'; git unstage fileA === git reset HEAD -- fileA
git config --global alias.visual '!gitk'

# 查看各个分支所指的对象 --decorate
git log --oneline --decorate
git log --oneline --decorate --graph --all
# 跟踪远程分支
git checkout --track origin/serverfix
git checkout -b sf origin/serverfix; sf 分支追踪 origin/serverfix 分支.
# 添加修改正在追踪的上游分支; -u / --set-upstream-to
git branch -u origin/serverfix
# 当设置好跟踪分支后,可通过 @{upstream} 或 @{u} 快捷方式来引用.所有如果 master 跟踪 origin/master。那么 git merge @{u} 可以取代 git merage origin/master.
# 查看所有跟踪分支
git branch -vv
# 删除服务端的分支
git push origin --delete serverfix
#
git init --bare --shared ; # shared 会自动修改该仓库目录的组权限为可写.

# 为服务端配置 SSH 访问
sudo adduser git
su git
cd
mkdir .ssh && chmod 700 .ssh
touch .ssh/authorized_keys && chmod 600 .ssh/authorized_keys
cat /tmp/id_rsa.john.pub >> ~/.ssh/authorized_keys
cat /tmp/id_rsa.josie.pub >> ~/.ssh/authorized_keys
# 限制 git 用户只能访问项目,无法登录远程主机.
cat /etc/shells
which git-shell
sudo vim /etc/shells; # 将 git-shell 添加
sudo chsh git /usr/bin/git-shell; # 限制 git 用户只能利用 SSH 连接对 Git 仓库进行推送和拉去操作,而不能登录机器并取得普通 shell.

# 以守护进程的方式设置 git 协议,不需要配置 SSH 公钥.
git daemon --reuseaddr --base-path=/opt/git/ /opt/git/;
# --reuseaddr 允许服务器在无需等待旧连接超时的情况下重启
# --base-path 允许用户在未完全指定路径的条件下克隆项目,结尾的路径将告诉 git 守护进程从何处寻找仓库来导出.如果有防火墙运行,请开放 9418 端口.
# 空白错误(换行\tab\。。。)
git diff --check
# 部分暂存
git add --patch
#
git merge --squash featureB
# --squash 接受被合并分支上的所有工作,并将其压缩至一个变更及,使仓库成为真正合并发生的状态,而不是生成一个合并提交.
# 打包 archive
git archive master --prefix='project/' | gzip > `git describe master`.tar.gz
git archive master --prefix='project/' --format=zip > `git describe master`.zips
#
git diff --ours
# --theirs
# --base -b

前言

JGit 是一个基于 EDL(BSD 协议的变种)授权的轻量级、实现 Git 版本控制系统功能(常规仓库访问,网络协议,版本控制核心算法)的纯 Java 库.

入门

获取

仓库搜索引擎中搜索 jgit 即可获取各种添加依赖的方式.我现在基本使用的是 gradle 依赖

1
2
implementation("org.eclipse.jgit:org.eclipse.jgit:5.3.0.201903130848-r")
implementation("org.eclipse.jgit:org.eclipse.jgit.http.server:5.3.0.201903130848-r")

JGit 也具有 CLI(功能比 git CLI 少),可以试一下 JGit 的功能.

手动编译 JGit CLI

假设已经 clone EGit 仓库. git clone https://git.eclipse.org/r/jgit/jgit.git 具体查看

1
2
3
4
5
6
7
8
~/src/jgit$ mvn clean install
# 进入 jgit 可执行文件所在文件夹
org.eclipse.jgit.pgm/target/jgit
# 查看 version 命令
prompt$ ./jgit version
jgit version xxxxx
# 如果经常使用 jgit 命令,可以添加执行链接(通常在 /usr/local/bin)
sudo ln -s /path/to/jgit /usr/local/bin/jgit

在 JGit CLI 运行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
prompt$ ./git
# 会列出最常用的命令
jgit --git-dir GIT_DIR --help (-h) --show-stack-trace command [ARG ...]

The most commonly used commands are:
branch List, create, or delete branches
clone Clone a repository into a new directory
commit Record changes to the repository
daemon Export repositories over git://
diff Show diffs
fetch Update remote refs from another repository
init Create an empty git repository
log View commit history
push Update remote repository from local refs
rm Stop tracking a file
tag Create a tag
version Display the version of jgit

# 常用的 debug test 命令
prompt$ ./jgit debug-show-commands
查看仓库

在查看最常用的命令之前,你可能想知道该仓库包含了多少分支,当前分支是那个.使用 branch -v 可以获取所有分支的简略信息,版本号,版本号提交信息的第一行.

1
2
3
4
5
6
7
8
9
10
11
12
13
prompt$ ./jgit branch -v
# master 4d4adfb Git Project import: don't hide but gray out existing projects
# * traceHistory 6b9fe04 [historyView] Add trace instrumentation

# 和 git-log 一样 log 命令显示提交信息
jgit log --author Math --grep tycho master
# 显示 master 分钟中,作者名包含 Math,提交信息包含 tycho 的搜有提交信息.
# commit xxxx
# Author: Math xxxx
# Date: xxx
# Update build to use tycho x.xx.x
# ...
# 大多数的搜索都会精确过滤提交日志,如提交者姓名等
历史图形化

jgit glog

核心概念

API

仓库

Repository 管理所有的项目和引用,管理代码

1
2
3
4
5
val repository = FileRepositoryBuilder()
.setGitDir(File("/my/git/directory"))
.readEnvironment() // 扫描 GIT_* 环境变量
.findGitDir() // 扫描文件系统
.build()

Git 对象

在 Git object model 所有的对象都是由 SHA-1 id 表示.在 JGit 中是由 AnyObjectIdObjectId类表示.
在 Git object model 中定义了四种对象类型:

  • blob: 用于存储文件对象
  • tree: 可以看作一个文件夹,指向其他的 tree 或 blob
  • commit: 指向一个 tree 的提交信息
  • tag: 突出提交信息,通常用来标记特殊的 release 版本.

为了从一个仓库中识别一个对象,只要传入一个正确的 revision 字符串即可

1
val head = repository.resolve("HEAD")

Ref

ref 是一个包含单个对象标识符的变量.对象标识符可以是任何 Git 合法对象(blob,tree,commit,tag)
例如,获取 head 的引用.

1
val HEAD = repository.findRef("refs/heads/master")

RevWalk

RevWalk 遍历 commit graph,并按顺序生成匹配的 commit

1
val revWalk = RevWalk(repository)

RevCommit

RevCommit 表示 Git object model 中的一个 commit

1
val commit = walk.parseCommit(objectIdOfCommit);

RevTag

RevCommit 表示 Git object model 中的一个 Tag

1
val tag = walk.parseCommit(objectIdOfTag);

RevTree

RevCommit 表示 Git object model 中的一个 tree

1
val tree = walk.parseCommit(objectIdOfTree);

参考

虽然 JGit 包含了许多和 Git 仓库交互的低级代码,同时还有一些参考org.eclipse.jgit.apit包中 Git porcelain 命令的高级 API.

添加命令(git-add)

add 命令可以向索引中添加文件,同时可以通过 setter 方法配置

  • addFilepattern()
1
2
3
4
val git = Git(repository)
git.add()
.addFilepattern("/dir")
.call()

提交命令(git-commit)

  • setAuthor()
  • setCommitter()
  • setAll()
1
2
3
4
git.commit()
.setAuthor("author","email")
.setMessage("message")
.call()

tag 命令(git-tag)

  • setName()
  • setMessage()
  • setTagger()
  • setObjectId()
  • setForceUpdate()
  • setSigned(): 暂不支持,会抛异常
1
2
3
git.tag()
.setName("tag")
.call()

log 命令(git-log)

  • add(AnyObjectId start)
  • addRange(AnyObjectId since,AnyObjectId until)
1
2
3
git.log()
.add(head)
.call()

merge 命令(git-merge)

TODO

Ant 任务

JGit 在 org.eclipse.jgit.ant 包中提供了 Ant 任务功能.
添加依赖

1
2
3
4
5
6
7
<taskdef resource="org/eclipse/jgti/ant/ant-tasks.properties">
<classpath>
<pathelement location="path/to/org.eclipse.jgit.ant-VERSION.jar"/>
<pathelement location="path/to/org.eclipse.jgit-VERSION.jar"/>
<pathelement location="path/to/jsch-0.1.44-1.jar"/>
</classpath>
</taskdef>

提供了 git-clone、git-init、git-checkout任务.

git-clone

1
<git-clone uri="http://egit.eclipse.org/jgit.git"/>
  • uri(必须)
  • dest(可选): 克隆的目标文件地址.默认使用基于 uri 路径最后一个组件作为可识别的名称的文件夹.
  • bare(可选): true/false/yes/no 表示是否克隆 bare 仓库. 默认 false
  • branch(可选): 默认 HEAD

git-init

1
<git-init/>
  • dest(可选): 默认 $GIT_DIR 或当前文件夹
  • bare(可选)

git-checkout

1
<git-checkout src="path/to/repo" branch="origin/experimental"/>
  • src(必须)
  • branch(必须)
  • createbranch(可选): true/false/yes/no 是否会创建新 branch。默认 false.
  • force(可选): true/false/yes/no 如果 true/yes,命名的 branch 已存在,已存在 branch 的起点将会被设置到新的起点。如果 false,存在的 branch 不会被改变.默认 false.

git-add

TODO

代码片段

获取某一提交记录的子记录

1
2
3
4
5
6
7
8
PlotWalk revWalk = new PlotWalk(repo());
ObjectId rootId = (branch == null) ? repo().resolve(HEAD) : branch.getObjectId();
RevComment root = revWalk.parseCommit(rootId);
revWalk.markStart(root);
PlotCommitList<PlotLane> plotCommitList = new PlotCommitList<PlotLane>();
plotCOmmitList.source(revWalk);
plotCommitList.fillTo(Integer.MAX_VALUE);
return revWalk;

高级主题

使用 RevWalk 减少内存使用

revision walk 接口和 RevWalk,RevCommit 类轻量级设计。然而当面对相当大的仓库时它们可能仍然需要很多内存。接下来提供了一些方法在遍历修订图(walking the revision graph)时减少内存。

限制遍历修订图(Restrict the walked revision graph)

仅遍历那些必要的图.即如果查找 refs/heads/master 而不是 refs/remotes/origin/master 的提交记录,确保对 refs/heads/master 调用 markStart(),对 refs/remotes/origin/master 调用 markUninteresting(). RevWalk traversal 讲只解析对你有用的提交记录,而且会避免在历史记录中查询.这讲减少内部 object map 的大小,因此减少整体内存占用.

1
2
3
4
5
6
RevWalk walk = new RevWalk(repository);
ObjectId from = repository.resolve("refs/heads/master");
ObjectId to = repository.resolve("refs/remotes/origin/master");

walk.markStart(walk.parseCommit(from));
walk.markUnInteresting(walk.parseCommit(to));

丢弃提交记录内容

setRetainBody(false) 可以用来丢弃提交记录内容,如果你不需要作者,提交者,或信息等.不需要该数据的例子如只使用 RevWalk 完成 branch merge 或使用 git rev-list 完成相关功能.

1
2
RevWalk walk = new RevWalk(repository);
walk.setRetainBody(false);

如果确实需要这些信息,可以考虑拆分你需要的数据然后对 RevCommit 调用 dispose().如果需要长时间使用这些信息,你会发现 JGit 内部使用的内存比你自己处理占用的内存要少,特别是需要全部的信息时。这是因为 JGit 内部使用 byte 数组保存 UTF-8 编码的信息.如果使用 UTF-16 编码的 Java String 占内存将会变大,假设大部分的消息是 US-ASCII 编码的.

1
2
3
4
5
6
RevWalk walk = new RevWalk(repository);
Set<String> authorEmails = new HashSet<String>();
for (RevCommit commit : walk) {
authorEmails.add(commit.getAuthorIdent().getEmailAddress());
commit.dispose();
}

RevWalk 和 RevCommit 的子类

如果需要获取某个提交记录的更多信息,可以考虑使用 RevWalk RevCommit 的子类, RevWalk.createCommit() 构建 RevCommit 子类的实例。然后将更多的信息存入 RevCommit 子类,这样就不需要额外的 HashMap 将 RevCommit 或 ObjectId 转换为 自定义的数据属性.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ReviewedRevision extends RevCommit {
private final Date reviewDate;

private ReviewedRevision(AnyObjectId id,Date reviewDate) {
super(id);
this.reviewDate = reviewDate;
}

public List<String> getReviewedBy() {
return getFooterLines("Reviewed-by");
}

public Date getReviewDate() {
return reviewDate;
}

public static class Walk extends RevWalk {
public Walk(Repository repo) {
super(repo);
}

@Override
protected RevCommit createCommit(AnyObjectId id) {
return new ReviewedRevision(id,getReviewDate(id));
}

private Date getReviewDate(AnyObjectId id) {

}
}
}

遍历修订后清理

RevWalk 无法缩小内部的 object map.如果刚完成了遍历仓库的所有历史,这将会将所有东西加载到 object map,并且无法被释放.如果再不需要这些数据,好的习惯是丢弃这个 RevWalk,然后为下次遍历重新申请新的 RevWalk。这样 GC 就会回收垃圾。另外,重用一个存在 的 object map 比完全重新创建一个新的更快.所以你需要平衡内存回收和用户渴望更快的操作之间的关系.

1
2
3
RevWalk walk = new RevWalk(repository);
for (RevCommit commit : walk) {}
walk.repository();

功能列表

前言

在知乎找到了一个更好的方法: [原文](使用 hexo,如果换了电脑怎么更新博客? - CrazyMilk 的回答 - 知乎
https://www.zhihu.com/question/21193762/answer/79109280)
在这里我整理一下:

  • 创建仓库 xxx.github.io.
  • 创建两个分支: master/hexo.(可在 github 网页或本地创建)
  • git clone git@github.com:xxx/xxx.github.io.git
  • 如果没有在网页创建分支,可以在此处创建.git checkout -b master; git checkout -b hexo
  • 接下来执行 npm install hexo; hexo init; npm i; npm i hexo-deployer-git,此处在 hexo 分支操作
  • 修改 _config.yml 的 deploy 参数,此处在 master 分支操作.
  • 我使用了hexo-next-theme,从 git 上下载后,进入 themes/next 执行 git submodule init,将 next 主题关联,此处在 hexo 分支操作
  • git add .; git commit -m "blahblahblah"; git push origin hexo; 提交网站相关文件.
  • hexo g -d 生成网站并部署到 github.

将 CNAME,图片等文件放入 source 目录下,可保证推送到 github 不会被删除.

⚠️ 👇 胡扯,观看请谨慎!!!

TeamCityJetbrains 公司出品的持续化集成工具,类似Jenkins,界面更加现代化,功能更强大,而且它的 server 和 agent 是分离的,可以指定本机或远程的机器来运行构建策略,其中还有调度队列算法.
hexo 是静态博客生成工具.hexo d -g命令可以自动生成 public 文件夹及 HTML,然后将其推送到 github(在_config.yml中已经配置过).
一般用户可能对theme自定义(修改theme下的_config.yml),当换机或备份时,希望将博客源文件(*.md)及修改的主题配置文件一并备份.而hexo默认只备份public文件夹,所以本文探索使用 CITeamCity将推送到Github的源文件编译生成public文件,这样每次写完文章,只要将其推送到Github,TeamCity会自动生成博客HTML.

获取到该 Key.而实际上在 build 执行时 key 以 ing 被删除了,不可能获取到.这并不意味着绝对安全,只是增加了 key 被盗的难度. agent 必须安全.

teamcity 安装

TeamCity提供了 Docker安装方式,因此请提前安装好Docker.
文件目录如下(请提前创建),teamcity 支持多种数据存储方式,此处使用 mysql 来存储。

1
2
3
4
5
6
7
8
9
10
11
12
# - teamcity
- agent
- conf
- data
- server
- data
- datadir
- opt
- mysql
- backup
- data
dockery-compose.yml

接下来是docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
``yaml
version: "3.3"
services:
teamcity-server:
image: jetbrains/teamcity-server
container_name: teamcity-server
restart: always
ports:
- 8111:8111
volumes:
- $PWD/server/datadir:/data/teamcity_server/datadir
- $PWD/server/opt/logs:/opt/teamcity/logs
- $PWD/server/data:/data/teamcity_server/others # 其他的一些资源,可以从本机复制到 server 上
environment:
# TEAMCITY_SERVER_MEM_OPTS: -Xmx2g -XX:MaxPermSize=270m -XX:ReservedCodeCacheSize=350m
MYSQL_USER: team-user
MYSQL_PASSWORD: team-pwd
MYSQL_ROOT_PASSWORD: teamcity8080
MYSQL_DATABASE: teamcitydb
depends_on:
- db
links:
- db
networks:
- team

db:
image: mysql
container_name: teamcity-db
restart: always
volumes:
- $PWD/mysql:/etc/mysql/conf.d
- $PWD/mysql/backup:/var/lib/mysql # 只有 /var/lib/mysql 对应本地文件为空,才会创建这个数据库,即初次创建时,这个对应的本地文件夹要为空
- $PWD/mysql/data:/others
environment:
MYSQL_USER: team-user
MYSQL_PASSWORD: team-pwd
MYSQL_ROOT_PASSWORD: teamcity8080
MYSQL_DATABASE: teamcitydb
ports:
- 3306:3306
networks:
- team

# <><> agent <><>
teamcity-agent:
image: jetbrains/teamcity-agent
container_name: teamcity-agent
restart: always
volumes:
- $PWD/agent/conf:/data/teamcity_agent/conf
- $PWD/agent/data:/data/teamcity_agent/others
environment:
AGENT_NAME: MacbookPro
SERVER_URL: http://xxx.xxx.x.xxx:8111 # 此处对应的是 TeamCityServer 的IP, localhost/127.0.0.1 都不行,请使用正确的 IP,端口对应上面暴露出来的端口
links:
- teamcity-server
# teamcity_agent 默认的任务环境路径: opt/buildagent/work

networks:
team:
driver: bridge

然后执行 docker-compose up -d 即可。

需要先根据本机 OS 安装docker-compose > docker-compose up -d 生成服务
docker-compose down 解体服务,删除容器,网络
docker-compose start/stop 启动终止服务

  • 打开 http://localhost:8111http://[ip]:8111 即可进入 TeamCity Server web 交互环境.按照提示初始化.登录时默认没有访客创建新用户的权限,所以需要已超级用户权限登录,点击下面的以超级权限登录后提示输入 token,可以进入 TeamCity Server 本地映射文件中查找,或是使用 docker logs teamcity-server 即可看到 token。

  • 创建项目时,TeamCity 默认使用用户名密码连接 Github,当然可以通过上传本地 ssh key 密钥到 TeamCity Server,通过 TeamCity Server 连接.

  • 设置编译步骤,我再次执行了shell 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#! /bin/bash

# 安装 nodejs
VERSION=v10.15.2
DISTRO=linux-x64
function checkNode() {
ISNODESUCCEED=$(node -v)
if [ ISNODESUCCEED != $VERSION ]; then
installNode
else
echo "NodeJS已安装"
fi
}

function insallNode() {
mkdir -p /usr/local/lib/nodejs
tar -xJf /data/teamcity_agent/others/node-$VERSION-$DISTRO # -v 会输出解压日志,此处太多,所以关闭

export PATH=/usr/local/lib/nodejs/node-$VERSION-$DISTRO/bin:$PATH

source ~/.profile

checkNode
}

checkNode

# 安装依赖
npm i
# 生成public文件
./node_modules/hexo/bin/hexo g

# 为 github.io 配置 CNAME
if [ ! -f "/CNAME" ]; then
echo "blog.dang8080.cn" > CNAME
fi

# 配置 git

git config --global credential.helper store # 保存 github 提交者信息,下次不用再输密码
git config --global user.name "Humphrey"
git config --global user.email "dang8080@qq.com"

git add --all
git commit -m "TeamCity CI 提交部署: $(date)"
git push origin master

问题:

所以问题来了,配置完了点击 run 查看 Build log 会发现 push 失败。因为通过 https 向 github 提交代码需要交互式输入用户密码。而此处没有提供,将密码硬编码到此 shell 里提交到 github 也不安全。
或者即使是通过修改 VCS root 使用 git@github.com ssh checkout 的代码,也无法推送到 github.

问题定位:

TeamCity SSH agent 使用本机(Linux/MacOS)的 OpenSSH 管理 SSH,对于 Windows,需要手动安装 OpenSSH (CygWin,MinGW,Git for Windows).
SSH agent 必须添加到 $PATH 中.
第一次连接到远程地址时,SSH agent 会询问是否保存远程地址的 fingerprint 到地址数据库 ~/.ssh/known_hosts中.
为了避免询问,可以提前配置。如果相信该远程地址,可以禁用远程地址检查

对所有的连接都禁用,~/.ssh/config

1
2
3

Host \*
StrictHostKeyChecking no

特定连接,-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no

TeamCity 当前仅支持 PEM 格式的 key.如果使用了其他格式的 key,请将其转换为 PEM.可以在 TeamCity web 界面 Conversions -> Export OpenSSH key 中转换.

OpenSSH 最近版本默认不生成 PEM 格式 key.使用下列方式生成 PEM: ssh-keygen -t rsa -m PEM

上传到 TeamCity Server 的 SSH key 默认保存在 <TeamCity Data Directory>/config/projects/<project>/pluginData/ssh_keys,TeamCity 会追踪此文件夹保证 SSH key 更新。SSH key 适用于本项目及子项目.

在 TeamCity agent checkout 执行时,Git 插件会从 TeamCity Server 下载 SSH key 到 agent.该 key 会暂时保存在 TeamCity agent 的文件系统里,在 fetch/clone 结束后就被删除.

key 被删除的原因是:通过 build 执行的 test 可能会留下恶意代码,之后会访问 TeamCity agent 文件系统,

TeamCity 是不支持 git ssh 推动代码到 github 的.(支持 ssh 传送文件)

方案一:

run 一次之后,执行 docker exec -it teamcity-agent bash 进入 opt/buildagent/work/xxxxx/ 下,手动 git push origin master。这样后续就不用再配置了

方案二:(硬核)

github 需要保存本机的 SSH pub key,才接受 git ssh 推送.那我们就在 TeamCity 生成 ssh key,然后添加到 github.

实践一
1
2
3
4
docekr exec -it teamcity-agent bash
ssh-keygen -t rsa
ssh-add [id_rsa]
# 然后复制 id_rsa.pub 的内容到 github 即可
实践二

连 TeamCity agent bash 也不想进,使用 shell 构建
先安装 expect tcl tk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apt-get update && apt-get install tcl tk expect
# 下面是 shell
#! /usr/bin/expect -f
set context $PWD
# 删除旧key
spawn rm -f "$context/id_rsa" "$context/id_rsa.pub"
expect{}
# 生成新key
spawn ssh-keygen -t rsa
expect{
"*save the key*" { send "$context/id_rsa\r";exp_continue }
"*passphrase*" { send "\r";exp_continue }
"*again*" { send "\r" }
}
spawn ssh-add "$context/id_rsa"
sshcheck=$(ssh -vT git@github.com)
if [[ $sshcheck =~ "successfully authenticated" ]]; then
echo "ssh 配置成功"
else
echo "ssh 配置失败"
fi
# 复制 pub
pubkey=$(cat "$context/id_rsa)
curl -H "Content-Type:application/json" -X POST --data '{ "title":"TeamCityAgentAuto","key":"$pubkey"}'

Overview

Room 2.1 开始支持 Kotlin 协程。DAO 方法可以使用 suspend 标记以确保这些方法不会在主线程中被执行。
Room 使用 Executor(来自框架组件) 作为 Dispatcher 运行 SQL 语句,当然在编译 RoomDatabase 时,你也可以提供自己的 Executor.

当前协程支持正在开发中,更多特性正在计划中

添加依赖

请升级 Room 到 v2.1, 同时 Kotlin v1.3.0+ , Coroutines v1.0.0+

1
implementation "androidx.room:room-coroutines:${versions.room}"

现在就可以使用啦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Dao
interface UsersDao {

@Query("SELECT * FROM users")
suspend fun getUsers(): List<User>

@Query("UPDATE users SET age = age + 1 WHERE userId = :userId)
suspend fun incrementUserAge(userId: String)

@Insert
suspend fun insertUser(user: User)

@Update
suspend fun updateUser(user: User)

@Delete
suspend fun deleteUser(user: User)
}

在调用其他 suspending DAO 函数时,@Transaction 也可以被 suspending

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

@Dao
abstract class UsersDao {
@Transaction
open suspend fun setLoggedInUser(loggedInUser: user) {
deleteUser(loggedInUser)
insertUser(loggedInUser)
}

@Query("DELETE FROM users")
abstract fun deleteUser(user: User)

@Insert
abstract suspend fun insertUser(user: User)
}

根据是否在 transaction 内调用,Room 对 suspending 函数处理逻辑不同.

  • 在 Transaction 中

    在数据库语句被触发的 CoroutineContext 下,Room 不做任何处理。函数调用者应该确保此方法不会在 UI 线程中执行.因为 suspend 函数只能被其他 suspend 函数 或在 coroutine 内调用,所以你不能把 Dispatchers.Main 赋值给 Dispatcher,应该是 Dispatchers.IO 或自定义

  • 不在 Transaction 中

    Room 会确保数据库语句在 Architecutre Components I/O Dispatcher 中触发。该 Dispatcher 在同一个 I/O Executor 的一个后台线程中运行 LiveData

底层

1
2
3
4
5
@Insert
fun insertUserSync(user: User)

@Insert
suspend fun insertUser(user: User)

对于同步 insert,生成的代码开始启动一个 transaction,然后执行 insert,标记 transaction successfull ,终结。
同步方法在被调用处的线程执行.

1
2
3
4
5
6
7
8
9
10
@Override
public void insertUserSync(final User user) {
__db.beginTransaction();
try {
__insertionAdapterOfUser.insert(user);
__db.setTransactionSuccessful();
} finally {
__db.endTransaction();
}
}

suspending 会确保不会在 UI 线程中执行。生成的代码会传递一个 Continuation.在 Callable#call() 中执行和同步相同的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public Object insertUserSuspend(final User user,
final Continuation<? super Unit> p1) {
return CoroutinesRoom.execute(__db, new Callable<Unit>() {
@Override
public Unit call() throws Exception {
__db.beginTransaction();
try {
__insertionAdapterOfUser.insert(user);
__db.setTransactionSuccessful();
return kotlin.Unit.INSTANCE;
} finally {
__db.endTransaction();
}
}
}, p1);
}

CoroutinesRoom.execute 会根据数据库是否 open,当前调用是否在 transaction 内来切换处理 context.

  • is open & in transaction

    仅调用 insert 逻辑

  • not in transaction

    使用 Architecture Components IO Executor 在后台线程执行 insert 逻辑

Overview

Compose 是一个为了定义和运行多容器 Docker 应用的工具。
官方动手示例

特性

  • 一个主机多个隔离环境
  • 当容器创建时保留所有的 volume
  • 只有容器被更改时才触发创建
  • 定义变量和在不同环境中使用

使用场景

  • 自动测试环境
  • 单独主机部署

docker-compose 安装

1
2
3
4
sudo curl -L "https://github.com/docker/compose/releases/download/1.23.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
sudo docker-compose --version

pip 安装

1
pip install docker-compose

卸载

1
2
sudo rm /usr/local/bin/docker-compose
pip uninstall docker-compose

docker-compose 命令

  • -f 指定 一个或多个 compose 文件的名称和路径

    1、使用多个 Compose 文件时,Compose 会把这几个文件合并为一个配置。按顺序合并,后边的覆盖前边的配置。
    2、docker-compose -f docker-compose.yml -f docker-compose.admin.yml run backup_db
    3、使用 -(dash) 作为 -f 的值,将会从输入中读取配置文件名。使用 stdin 时,配置里涉及到的路径都是相对于当前工作上下文路径。
    4、如果不是用 -f,Compose 遍历当前上下文路径和它的父路径查询 docker-compose.ymldocker-compose.override.yml 文件。请至少提供一个 docker-compose.yml 文件。如果所有的文件都在同一个文件夹下,Compose 将合并它们。
    5、docker-compose.override.yml 的配置高于 docker-compose.yml,且为后者提供额外的配置属性。

  • 为单独的 Compose 文件指定路径

    1、使用 -f, 或从命令行输入,或在 shell / 某个环境配置文件中设置 COMPOSE_FILE 环境变量,指定不在当前文件夹的 Compose 文件路径.
    2、docker-compose -f ~/sandbox/rails/docker-compose.yml pull db~/sandbox/rails/docker-compose.yml 文件中获取 db 服务中定义的 postgres db 镜像。

  • -p 指定项目名

    1、如果不指定 -p,Compose 默认使用当前文件夹的名称。

  • 配置环境变量

docker-compose CLI 环境变量

默认内置了几个环境变量可以配置 docker-compose.
DOCKER_ 开头的变量和配置 docker 命令行客户端的变量类似。如果使用 docker-machine的话,eval "$(docker-machine env my-docker-vm)"将为变量设置正确的值。

  • COMPOSE_PROJECT_NAME

    1、设置项目名.该值在启动时会作为容器服务的前缀.比如项目名 myapp,包含两个服务 db 和 web,那么 Compose 启动的容器名为 myapp_db_1 和 myapp_web_1.
    2、默认时项目文件夹的根目录名。

  • COMPOSE_FILE

    1、指定 Compose 文件路径。如果未指定,Compose 将在当前文件夹查询 docker-compose.yml 文件,如果不存在将继续遍历父目录直到找到。
    2、支持使用文件路径分隔符(Linux & MacOS [:] Windows [;]).比如 COMPOSE_FILE=docker-compose.yml:docker-compose.prod.yml,路径分隔符可以通过 COMPOSE_PATH_SEPARATOR 自定义

  • COMPOSE_API_VERSION

    1、Docker API 仅支持来自指定版本客户端的请求。使用 docker-compose 时出现 client and server don't have same version 错误,那么可以通过设置该变量解决。
    2、设置该变量主要针对临时的运行客户端和服务端版本不一致的情况。

  • DOCKER_HOST

    1、为 docker daemon 设置 URL.和 docker 客户端一样,默认为 unix:///var/run/docker.sock

  • DOCKER_TLS_VERIFY

    1、设置空字符以外的任何值时,开启 TLS.

  • DOCKER_CERT_PATH

    1、配置 ca.pem,cert.pem,key.pem 的路径。默认 ~/.docker

  • COMPOSE_HTTP_TIMEOUT

    1、超时时间(秒).默认 60s

  • COMPOSE_TLS_VERSION

    1、TLS 版本.默认 TLSv1. 可供选项 TLSv1, TLSv1_1, TLSv1_2

  • COMPOSE_CONVERT_WINDOWS_PATH

    1、是否开启把 Windos-style 的路径转为 Unix-style 的卷定义。在 Windos 上使用 Docker Machine 和 Docker Toolbox 时总是要设置该变量。默认 0。 true/1 代表开启,false/0 代表关闭.

  • COMPOSE_PATH_SEPARATOR

    1、如果设置,COMPOSE_FILE 值将使用此处的定义作为分隔符。

  • COMPOSE_FORCE_WINDOWS_HOST

    1、如果设置,即使 Compose 运行在 Unix-based 系统上,卷定义也将使用简略语法解析为假设运行在 Windows 上的路径。true/a,false/0

  • COMPOSE_IGNORE_ORPHANS

    1、如果设置,Compose 不会试着对项目的单独容器检测。true/1,false/0

  • COMPOSE_PARALLEL_LIMIT

    1、并行执行的限制。默认 64,绝不能低于 2.

  • COMPOSE_INTERACTIVE_NO_CLI

    1、如果设置,Compose 不会尝试使用 Docker CLI 和 run exec 操作交互。true/1,false/0

Compose file

version 3

build

配置选项在编译期生效。
build 可以指定一个路径

1
2
3
4
version: "3"
services:
webapp:
build: ./dir

或者是一个在指定 context 下的路径对象,同时可包含 Dockerfile 和 args

1
2
3
4
5
6
7
8
version: "3"
services:
webapp:
build:
context: ./dir
dockerfile: Dockerfile-alternate
args:
buildno: 1

如果同时也指定了 image,Compose 使用 webapp 和 image 中指定的可选的 tag 命名最终生成的镜像名

1
2
build: ./dir
image: webapp:tag

swarm mode 不支持。docker stack 命令只接受预编译的镜像.

CONTEXT

可以是包含 Dockerfile 文件的路径,或 git 仓库的 url.
如果是相对路径,那么被解析为相对于当前 Compose 文件的路径。此文件夹同时也是发送到 Docker daemon 的编译上下文。

DOCKERFILE

可选
此处必须指定编译路径

ARGS

只有在编译过程中可访问的环境变量。
首先,在 Dokcerfile 中指定参数:

1
2
3
4
5
ARG buildno
ARG gitcommithash

RUN echo "Build number: $buildno"
RUN echo "Bashed on commit: $gitcommithash"

然后在 build 下给该参数复制(可以是键值对列表):

1
2
3
4
5
6
7
8
9
10
11
build:
context: .
args:
buildno: 1
gitcommithash: cdc3b19

build:
context: .
args:
- buildno=1
- gitcommithash=cdc3b19

你也可以不指定值,它的值就是编译时 Compose 当前运行环境的值.

YAML 布尔值(true,false,yes,no,on,off)应该使用引号应用,这样解析器才能把它们解析为 string

CACHE_FROM

v3.2+

指定 engine 可以缓存的镜像列表

1
2
3
4
5
build:
context: .
cache_from:
- alpine:latest
- corp/webapp:3.14
LABELS

v3.3+

向生成的镜像添加元数据。可以是数组或目录
推荐使用反向 DNS 标记以避免和其他软件冲突

1
2
3
4
5
6
7
8
9
10
11
12
13
build:
context: .
labels:
com.example.description: "Accounting webapp"
com.example.department: "Finance"
com.example.label-with-empty-value: ""

build:
context: .
labels:
- "com.example.description=Accounting webapp"
- "com.example.department=Finance"
- "com.example.label-with-empty-value"
SHM_SIZE

v3.5+

设置编译生成容器 dev/shm' 分区大小。int 值单位为 byte. string 可以携带单位

1
2
3
4
5
6
7
build:
context: .
shm_size: '2gb'

build:
context: .
shm_size: 10000000
TARGET

v3.4+

编译 Dockerfile 中定义的指定版本

1
2
3
build:
context: .
target: prod
cap_add,cap_drop

添加或删除容器的容量。man 7 capabilities 查看可用列表

1
2
3
4
5
cap_add:
- ALL
cap_drop:
- NET_ADMIN
- SYS_ADMIN

swarm mode 无效

cgroup_parent

为容器指定可选的 cgroup parent

1
cgroup_parent: m-executor-abcd

swarm mode 无效

command

覆盖默认的命令

1
command: bundle exec thin -p 3000

也可以是列表

1
command: ["bundle", "exec", "thin", "-p", "3000"]
configs

Tips

  • 共享文件夹,卷,绑定挂载

    如果你的项目不在 Users目录(cd ~),那么你需要共享驱动器或 Dockerfile 所在位置和当前正在使用的卷。如果出现运行时错误表示文件未找到,那么就是一个挂载卷的请求被拒绝,或服务启动失败,试着共享文件或驱动。挂载卷要求共享项目不在 C:\Users (Windows),或 /Users (Mac) 的驱动,并且如果是运行在 Dokcer Desktop for Windows 的 Linux 容器上的所有应用都需要共享。

  • 如果改变了一个服务的 Dockerfile 或者编译文件夹里的内容,运行 docker-compose build重新编译

官方示例

Compose & WordPress

  • 为项目创建空文件夹。该文件夹作为应用的上下文,且只保存构建镜像所需的资源。
  • 创建 docker-compose.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
version: "3.3"

services:
db:
image: mysql:5.7
volumes:
- db_data:/var/lib/mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: somewordpress
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: wordpress
wordpress:
depends_on:
- db
image: wordpress:latest
ports:
- "8000:80"
restart: always
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress
WORDPRESS_DB_NAME: wordpress
volumes:
db_data: {}
# db_data 卷会保存任何 WordPress 对数据库的改变。
# WordPress 通常开放 80 和 443 端口.
  • docker-compose up -d.

    如果使用 Docker Machine, 那么运行 docker-machine ip MACHINE_VM 获取运行地址。如果是 destop 版,http://localhost 即可.

  • docker-compose down 移除容器,默认的网络,保留 WordPress 和数据库。docker-compose down --volumes 全部移除。

原文

view(activity/fragment) 和 ViewModel 交流的比较好的方式是 LiveData observables. view 订阅 LiveData 的改变且随时响应。这适用于连续不断的显示在一个屏幕的数据。
LiveData
但是某些数据却更应该被消费一次,比如 Snackbar 消息,navigation 事件 或 dialog 触发器。
LiveData once
与其试着通过扩展 Architecture Components 扩展或库解决这个问题,不如我们可以直面这是个设计缺陷。我们推荐你把你的事件看作是状态的一部分。本文我们将列举一些常见的错误和推荐的解决方案。

❌ Bad: 1. 对事件使用 LiveData

在 LiveData 对象内部直接持有 Snackbar 消息或 navigation 信号。原则上普通的 LiveData 对象可以这样使用,但实际上会暴露一些问题。
在 master/detail 架构的 app 中,如下是 maters 的 ViewModel

1
2
3
4
5
6
7
8
9
10
// 请不要对事件这样用
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Boolean>()
val navigateToDetails : LiveData<Boolean>
get() = _navigateToDetails

fun userClicksOnButton() {
_navigateToDetails.value = true
}
}

在 View(activity/fragment) 中

1
2
3
myViewModel.navigateToDetails.observe(this,Observer {
if (it) startActivity(DetailsActivity....)
})

此方案的不足在于 _navigateToDetails将会一直为 true,而且不可能回到首屏:

  • 用户点击按钮启动 Details Activity
  • 用户按返回按钮,回到主 Activity
  • 当 activity 进入回退栈时 observers 失活,现在再次激活

从 ViewModel 中调用 navigation 且立即将其设为 false

1
2
3
4
fun userClicksOnButton() {
_navigateToDetails.value = true
_navigateToDetails.value = false
}

但是请注意: LiveData 保存数据但不会保证在接受到事件时发送任何数据。例如,当没有观察者活跃时更新值,那么一个新值将替换原来的值。同时,从不同线程设置属性将会导致竞争状态,此时仅能保证一个观察者被调用。

最主要的问题是,这个方案很难理解而且代码垃圾。所以我们如何保证在 navigation 事件发生时值重置?

❌Better: 2.使用 LiveData wrapper 事件,在观察者中重置属性.

1
2
3
4
5
6
listViewModel.navigateToDetails.observe(this,Observer {
if (it) {
myViewModel.navigateToDetailsHandled()
startActivity(DetailsActivity...)
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ListViewModel: ViewModel {
private val _navigateToDetails = MutableLiveData<Boolean>()

val navigateToDetails: LiveData<Boolean>
get() = _navigateToDetails

fun userClicksOnButton() {
_navigateToDetails.value = true
}

fun navigateToDetailsHandled() {
_navigateToDetails.value = false
}
}

此方案的不足之处在于有些冗余代码

✅ ok:使用 SingleLiveEvent

SingleLiveEvent 只适用于部分场景。只发送和更新一次状态的 LiveData

1
2
3
4
5
6
7
8
9
10
class ListViewModel: ViewModel {
private val _navigateToDetails = SingleLiveEvent<Any>()

val navigateToDetails: LiveData<Any>
get() = _navigateToDetails

fun userClicksOnButton() {
_navigateToDetails.call()
}
}
1
2
3
myViewModel.navigateToDetails.observe(this, Observer {
startActivity(DetailsActivity...)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// SingleLiveEvent
public class SingleLiveEvent<T> extends MutableLiveData<T> {

private static final String TAG = "SingleLiveEvent";

private final AtomicBoolean mPending = new AtomicBoolean(false);

@MainThread
public void observe(LifecycleOwner owner, final Observer<T> observer) {

if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.");
}

// Observe the internal MutableLiveData
super.observe(owner, new Observer<T>() {
@Override
public void onChanged(@Nullable T t) {
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t);
}
}
});
}

@MainThread
public void setValue(@Nullable T t) {
mPending.set(true);
super.setValue(t);
}

/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
public void call() {
setValue(null);
}
}

此方案的不足之处在于只有一个订阅者。如果你有多个观察者,那么只有一个被调用且不保证顺序。
SingleLiveEvent

✅ 推荐:使用 Event Wrapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
open class Event<out T>(private val content: T) {
val hasBeenHandled = false
private set // 只读属性

/**
* 返回 content, 阻止其再次调用
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}

fun peekContent(): T = content
}
1
2
3
4
5
6
7
8
9
10
11
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Event<String>>()

val navigateToDetails : LiveData<Event<String>>
get() = _navigateToDetails


fun userClicksOnButton(itemId: String) {
_navigateToDetails.value = Event(itemId) // Trigger the event by setting a new Event as a new value
}
}
1
2
3
4
5
6
7
8
9
10
11
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Event<String>>()

val navigateToDetails : LiveData<Event<String>>
get() = _navigateToDetails


fun userClicksOnButton(itemId: String) {
_navigateToDetails.value = Event(itemId) // Trigger the event by setting a new Event as a new value
}
}

此方案的优势是用户需要使用 getContentIfNotHandled() 或 peekContent()指定意图。此方法把事件抽象为 state 的一部分:变成仅表示是否被消费的消息。
使用 Event wrapper,可以在单一用户事件上添加多个观察者

结论

总之,把事件作为状态的一部分。
使用这个 EventObserver 在大量事件结束后移除它

1
2
3
4
5
6
7
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
override fun onChanged(event: Event<T>?) {
event?.getContentIfNotHandled()?.let { value ->
onEventUnhandledContent(value)
}
}
}
1
2
3
inline fun <T> LiveData<Event<T>>.observeEvent(owner: LifecycleOwner, crossinline onEventUnhandledContent: (T) -> Unit) {
observe(owner, Observer { it?.getContentIfNotHandled()?.let(onEventUnhandledContent) })
}

LiveData 的优势

  • 确保 UI 和数据状态匹配

    LiveData 遵循观察者模式。

  • 无内存泄漏
  • 不会因为 Activity 被终止而崩溃

    如果观察者处于 inactive 状态,例如 activity 处于回退栈中,那么它不会接收到任何 LiveData 事件.

  • 不用手动处理生命周期事件
  • 时刻更新数据状态

    如果一个观察者的变为 inactive,那么它会在重新 active 时获取最新的数据状态。比如,一个 activiy 如果处于后台,那么它将在重新返回前台时获取到最新的数据。

  • 应对 configuration change

    如果一个 activity 或 fragment 由于 configuration change(设备旋转) 导致重新创建,它会立即获取最新可用的数据.

  • 共享资源

    可以使用单例模式扩展 LiveData,封装系统服务在 app 内共享。

在 ViewModel 对象中保存可以更新 UI 的 LiveData 对象,而不是在 activity 或 fragment 的原因是:

  • 避免 activty 或 framgent 过度膨胀。UI controller 仅负责展示数据而不是保存数据状态
  • 从特性的 activity 或 fragment 中剥离 LiveData 实例,使得 LiveData 对象可以在 configuration change 中存活。

请在主线程中调用 setValue(T) 更新 LiveData 对象.如果是在工作线程,请调用 postValue(T).

不像 SQLite 这样的数据库,ObjectBox 不需要你创建 database schema.这不意味着 ObjectBox 是无 schema 的。为高效起见,ObjectBox 对存储的数据维护了一个元模型(meta model)。此元模型实际上等价于 ObjectBox 的 schema.它包含了所有属性的类型、indexes 等.不同之处在于 ObjectBox 试图自动管理该元模型.某些情况下,这需要你帮忙.

Object 的 IDs 是 @Id 定义的,而 所有 entity 类型的实例都绑定一个 meta model ID.

JSON for consistent IDs

ObjectBox 把一部分元模型保存在 JSON 文件中.此文件应该通过版本控制软件管理,主要原因是:它可以保证 元模型里的 IDs 和 UIDs 跨设备一致.