开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

通俗易懂:说说 Python 里的线程安全、原子操作

发表于 2020-05-14

首发于微信公众号:Python编程时光

在线博客地址:python.iswbm.com/en/latest/c…


在并发编程时,如果多个线程访问同一资源,我们需要保证访问的时候不会产生冲突,数据修改不会发生错误,这就是我们常说的 线程安全 。

那什么情况下,访问数据时是安全的?什么情况下,访问数据是不安全的?如何知道你的代码是否线程安全?要如何访问数据才能保证数据的安全?

本篇文章会一一回答你的问题。

  1. 线程不安全是怎样的?

要搞清楚什么是线程安全,就要先了解线程不安全是什么样的。

比如下面这段代码,开启两个线程,对全局变量 number 各自增 10万次,每次自增 1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码from threading import Thread, Lock

number = 0

def target():
global number
for _ in range(1000000):
number += 1

thread_01 = Thread(target=target)
thread_02 = Thread(target=target)
thread_01.start()
thread_02.start()

thread_01.join()
thread_02.join()

print(number)

正常我们的预期输出结果,一个线程自增100万,两个线程就自增 200 万嘛,输出肯定为 2000000 。

可事实却并不是你想的那样,不管你运行多少次,每次输出的结果都会不一样,而这些输出结果都有一个特点是,都小于 200 万。

以下是执行三次的结果

1
2
3
python复制代码1459782
1379891
1432921

这种现象就是线程不安全,究其根因,其实是我们的操作 number += 1 ,不是原子操作,才会导致的线程不安全。

  1. 什么是原子操作?

原子操作(atomic operation),指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会切换到其他线程。

它有点类似数据库中的 事务。

在 Python 的官方文档上,列出了一些常见原子操作

1
2
3
4
5
6
7
8
9
10
11
python复制代码L.append(x)
L1.extend(L2)
x = L[i]
x = L.pop()
L1[i:j] = L2
L.sort()
x = y
x.field = y
D[x] = y
D1.update(D2)
D.keys()

而下面这些就不是原子操作

1
2
3
4
python复制代码i = i+1
L.append(L[-1])
L[i] = L[j]
D[x] = D[x] + 1

像上面的我使用自增操作 number += 1,其实等价于 number = number + 1,可以看到这种可以拆分成多个步骤(先读取相加再赋值),并不属于原子操作。

这样就导致多个线程同时读取时,有可能读取到同一个 number 值,读取两次,却只加了一次,最终导致自增的次数小于预期。

当我们还是无法确定我们的代码是否具有原子性的时候,可以尝试通过 dis 模块里的 dis 函数来查看

当我们执行这段代码时,可以看到 number += 1 这一行代码,由两条字节码实现。

  • BINARY_ADD :将两个值相加
  • STORE_GLOBAL: 将相加后的值重新赋值

每一条字节码指令都是一个整体,无法分割,他实现的效果也就是我们所说的原子操作。

当一行代码被分成多条字节码指令的时候,就代表在线程线程切换时,有可能只执行了一条字节码指令,此时若这行代码里有被多个线程共享的变量或资源时,并且拆分的多条指令里有对于这个共享变量的写操作,就会发生数据的冲突,导致数据的不准确。

为了对比,我们从上面列表的原子操作拿一个出来也来试试,是不是真如官网所说的原子操作。

这里我拿字典的 update 操作举例,代码和执行过程如下图

从截图里可以看到,info.update(new) 虽然也分为好几个操作

  • LOAD_GLOBAL:加载全局变量
  • LOAD_ATTR: 加载属性,获取 update 方法
  • LOAD_FAST:加载 new 变量
  • CALL_FUNCTION:调用函数
  • POP_TOP:执行更新操作

但我们要知道真正会引导数据冲突的,其实不是读操作,而是写操作。

上面这么多字节码指令,写操作都只有一个(POP_TOP),因此字典的 update 方法是原子操作。

  1. 实现人工原子操作

在多线程下,我们并不能保证我们的代码都具有原子性,因此如何让我们的代码变得具有 “原子性” ,就是一件很重要的事。

方法也很简单,就是当你在访问一个多线程间共享的资源时,加锁可以实现类似原子操作的效果,一个代码要嘛不执行,执行了的话就要执行完毕,才能接受线程的调度。

因此,我们使用加锁的方法,对例子一进行一些修改,使其具备原子性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
python复制代码from threading import Thread, Lock


number = 0
lock = Lock()


def target():
global number
for _ in range(1000000):
with lock:
number += 1

thread_01 = Thread(target=target)
thread_02 = Thread(target=target)
thread_01.start()
thread_02.start()

thread_01.join()
thread_02.join()

print(number)

此时,不管你执行多少遍,输出都是 2000000.

  1. 为什么 Queue 是线程安全的?

Python 的 threading 模块里的消息通信机制主要有如下三种:

  1. Event
  2. Condition
  3. Queue

使用最多的是 Queue,而我们都知道它是线程安全的。当我们对它进行写入和提取的操作不会被中断而导致错误,这也是我们在使用队列时,不需要额外加锁的原因。

他是如何做到的呢?

其根本原因就是 Queue 实现了锁原语,因此他能像第三节那样实现人工原子操作。

原语指由若干个机器指令构成的完成某种特定功能的一段程序,具有不可分割性;即原语的执行必须是连续的,在执行过程中不允许被中断。

关注公众号,获取最新干货!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【译】【24K Star】 放弃 Dagger 拥抱 Ko

发表于 2020-05-14

前言

  • 原标题: Koin vs Dagger, Say hello to Koin
  • 原文地址: blog.usejournal.com/android-koi…
  • 原文作者:Farshid ABZ

作者这篇文章到目前为止已经收到了 2.4k+ 的赞,冲上了 Medium 热门,非常好的一篇文章,我也使用 Koin + kotlin + databinding 结合着 inline、reified 强大的特性封装了基础库,包含了 DataBindingActivity、DataBindingFragment、DataBindingDialog、DataBindingListAdapter 等等, 正在陆续添加新的组件。

  • 文章:https://juejin.cn/post/6844904131803480071
  • GitHub:https://github.com/hi-dhl/JDataBinding

通过这篇文章你将学习到以下内容,将在译者思考部分会给出相应的答案

  • Dagger 和 Koin 优势劣势对比?应该选择 Dagger 还是 Koin?
  • koin 语法特性?
  • Koin 为什么可以做到无代码生成、无反射?
  • Inline 修饰符做什么用的?如何正确使用?带来的性能损失?
    • 只是用 Inline 修饰符,为什么编译器会给我们一个警告?
    • 为什么编译器建议 inline 修饰符需要和 lambda 表达式一起使用呢?
    • 什么时候应该使用 inline 修饰符?
  • Reified 修饰符做什么用?如何使用?
  • Koin 带来性能损失的那些事?
  • Kotlin 用 5 行代码实现快排算法?

这篇文章涉及很多重要的知识点,请耐心读下去,我相信应该会给大家带来很多不一样的东西。

译文

当我正在反复学习 Dagger 的时候,我遇见了 Koin,Koin 不仅节省了我的时间,还提高了效率,将我从复杂 Dagger 给释放出来了。

这篇文章将会告诉你什么是 Koin,与 Dagger 对比有那些优势,以及如何使用 Koin。

是什么 Koin

Koin 是为 Kotlin 开发者提供的一个实用型轻量级依赖注入框架,采用纯 Kotlin 语言编写而成,仅使用功能解析,无代理、无代码生成、无反射。

Dagger vs Koin

为了正确比较这两种方式,我们用 Dagger 和 Koin 去实现了一个项目,项目的架构都是 MVVM,其中用到了 retrofit 和 LiveData,包含了 1 个Activity、4 个 fragments、5 个 view models、1 个 repository 和 1 个 web service 接口, 这应该是一个小型项目的基础架构了

先来看一下 DI 包下的结构,左边是 Dagger,右边是 Koin

如你所见配置 Dagger 需要很多文件 而 Koin 只需要 2 个文件,例如 用 Dagger 注入 1 个 view models 就需要 3 个文件(真的需要用这么多文件吗?)

比较 Dagger 和 Koin 代码行数

我使用 Statistic 工具来统计的,反复对比了项目编译前和编译后,Dagger 和 Koin 生成的代码行数,结果是非常吃惊的

正如你看到的 Dagger 生成的代码行比 Koin 多两倍

Dagger 和 Koin 编译时间怎么样呢

每次编译之前我都会先 clean 然后才会 rebuild,我得到下面这个结果

1
2
3
4
5
6
7
复制代码Koin:
BUILD SUCCESSFUL in 17s
88 actionable tasks: 83 executed, 5 up-to-date

Dagger:
BUILD SUCCESSFUL in 18s
88 actionable tasks: 83 executed, 5 up-to-date

我认为这个结果证明了,如果是在一个更大、更真实的项目中,这个代价是非常昂贵。

Dagger 和 Koin 使用上怎么样呢

如果你想在 MVVM 和 Android Support lib 中使用 Dagger 你必须这么做。

首先在 module gradle 中 添加 Dagger 依赖。

1
2
3
复制代码kapt "com.google.dagger:dagger-compiler:$dagger_version"
kapt "com.google.dagger:dagger-android-processor:$dagger_version"
implementation "com.google.dagger:dagger:$dagger_version"

然后创建完 modules 和 components 文件之后, 需要在 Application 中 初始化 Dagger(或者其他方式初始化 Dagger)。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码Class MyApplication : Application(), HasActivityInjector { 
@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Activity>
override fun activityInjector() = dispatchingAndroidInjector
fun initDagger() {
DaggerAppComponent
.builder()
.application(this)
.build()
.inject(this)
}
}

所有的 Activity 继承 BaseActivity,我们需要实现 HasSupportFragmentInjector 和 inject DispatchingAndroidInjector。

对于 view models,我们需要在 BaseFragment 中注入 ViewModelFactory,并实现 Injectable。

但这并不是全部。还有更多的事情要做。

对于每一个 ViewModel、Fragment 和 Activity 我们需要告诉 DI 如何注入它们,正如你所见我们有 ActivityModule、FragmentModule、和 ViewModelModule。

我们来看一下下面的代码

1
2
3
4
5
6
7
复制代码@Module
abstract class ActivityModule {
@ContributesAndroidInjector(modules = [FragmentModule::class])
abstract fun contributeMainActivity(): MainActivity

//Add your other activities here
}

Fragments 如下所示:

1
2
3
4
5
6
7
8
9
10
11
复制代码@Module
abstract class FragmentModule {
@ContributesAndroidInjector
abstract fun contributeLoginFragment(): LoginFragment

@ContributesAndroidInjector
abstract fun contributeRegisterFragment(): RegisterFragment

@ContributesAndroidInjector
abstract fun contributeStartPageFragment(): StartPageFragment
}

ViewModels 如下所以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码@Module
abstract class ViewModelModule {

@Binds
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory

@Binds
@IntoMap
@ViewModelKey(loginViewModel::class)
abstract fun bindLoginFragmentViewModel(loginViewModel: loginViewModel): ViewModel

@Binds
@IntoMap
@ViewModelKey(StartPageViewModel::class)
abstract fun bindStartPageViewModel(startPageViewModel: StartPageViewModel): ViewModel
......
}

所以你必须在 DI modules 中添加这些 Fragments、Activities 和 ViewModels。

那么在 Koin 中如何做

你需要在 module gradle 中添加 Koin 依赖

1
复制代码implementation "org.koin:koin-android-viewmodel:$koin_version"

然后我们需要创建 module 文件,稍后我告诉你怎么做,实际上我们并不需要像 Dagger 那么多文件。

Dagger 还有其他问题

学习 Dagger 成本是很高的,如果有人加入你的项目或者团队,他/她不得不花很多时间学习 Dagger,我使用 Dagger 两年了,到现在还不是很了解,每次我开始学习 Andorid 新技术的时候,我不得不去搜索和学习如何用 Dagger 实现新技术。

来看一下 Koin 代码

首先我们需要添加 Koin 依赖,如下所示:

1
复制代码implementation "org.koin:koin-android-viewmodel:$koin_version"

我们使用的是 koin-android-viewmodel 库,因为我们希望在 MVVM 中使用它,当然还有其他的依赖库。

添加完依赖之后,我们来实现第一个 module 文件,像 Dagger 一样,可以在一个单独的文件中实现每个模块,但是由于代码简单,我决定在一个文件中实现所有模块,你也可以把它们分开。

首先我们需要了解一下 koin 语法特性

  • get(): 解析 Koin 模块中的实例,调用 get() 函数解析所请求组件需要的实例,这个 get() 函数通常用于构造函数中,注入构造函数值
  • factory:声明这是一个工厂组件,每次请求都为您提供一个新实例
  • single:采用单例设计模式
  • name:用于命名定义,当您希望具有不同类型的同一个类的多个实例时,需要使用它

我们没有创建具有多个注释和多个组件的许多文件,而是为 DI 注入每个类的时候,提供一个简单、可读的文件。

了解完 koin 语法特性之后,我们来解释下面代码什么意思

1
复制代码private val retrofit: Retrofit = createNetworkClient()

createNetworkClient 方法创建 Retrofit 实例,设置 baseUrl,添加 ConverterFactory 和 Interceptor

1
2
复制代码private val generalApi: GeneralApi =  retrofit.create(GeneralApi::class.java)
private val authApi: AuthApi = retrofit.create(AuthApi::class.java)

AuthApi 和 GeneralApi 是 retrofit 接口

1
2
3
4
复制代码val viewModelModule = module {
viewModel { LoginFragmentViewModel(get()) }
viewModel { StartPageViewModel() }
}

在 module 文件中声明为 viewModel, Koin 将会向 ViewModelFactory 提供 viewModel,将其绑定到当前组件。

正如你所见,在 LoginFragmentViewModel 构造函数中有调用了 get() 方法,get() 会解析一个 LoginFragmentViewModel 需要的参数,然后传递给 LoginFragmentViewModel,这个参数就是 AuthRepo。

最后在 Application onCreate 方法中添加如下代码

1
复制代码startKoin(this, listOf(repositoryModule, networkModule, viewModelModule))

这里只是调用 startKoin 方法,传入一个上下文和一个希望用来初始化 Koin 的模块列表。

现在使用 ViewModel 比使用纯 ViewModel 更容易,在 Fragment 和 Activity 视图中添加下面的代码

1
复制代码private val startPageViewModel: StartPageViewModel by viewModel()

通过这段代码,koin 为您创建了一个 StartPageViewModel 对象,现在你可以在 Fragment 和 Activity 中使用 view model

译者思考

作者总共从以下 4 个方面对比了 Dagger 和 Kotlin:

  • 文件数量:基于 mvvm 架构,分别使用了 Dagger 和 koltin 作为依赖注入框架,初始化 Dagger 时至少需要 9 个文件,而 koltin 只需要 2 个文件,Dagger 文件数量远超过 koltin
  • 代码行数:作者使用了 Statistic 工具,反复对比了项目编译前和编译后,Dagger 和 Koin 生成的代码行数,如下图所示

  • 反复的对比了 Dagger 和 Koin 编译时间,结果如下所示 koin 比 Dagger 快
1
2
3
4
5
6
7
复制代码Koin:
BUILD SUCCESSFUL in 17s
88 actionable tasks: 83 executed, 5 up-to-date

Dagger:
BUILD SUCCESSFUL in 18s
88 actionable tasks: 83 executed, 5 up-to-date
  • 学习成本巨大,如果使用了 Dagger 朋友,应该和作者的感觉是一样的,Dagger 学习的成本是非常高的,如果项目中引入了 Dagger 意味着团队每个人都要学习 Dagger,无疑这个成本是巨大的,而且使用起来非常的复杂

注意:作者在 Application 中调用 startKoin 方法初始化 Koin 的模块列表,是 Koin 1X 的方式,Koin 团队在 2x 的时候做了很多改动(下面会介绍),初始化 Koin 的模块列有所改动,代码如下所示:

1
2
3
4
5
6
7
8
复制代码startKoin {
// Use Koin Android Logger
androidLogger()
// declare Android context
androidContext(this@MainApplication)
// declare modules to use
modules(module1, module2 ...)
}

Koin 为什么可以做到无代码生成、无反射

Koin 作为一个轻量级依赖注入框架,为什么可以做到无代码生成、无反射?因为 kotlin 强大的语法糖(例如 Inline、Reified 等等)和函数式编程,我们先来看一段代码。

koin-projects/koin-core/src/main/kotlin/org/koin/dsl/Module.kt

案例一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码//  typealias 是用来为已经存在的类型重新定义名字的
typealias ModuleDeclaration = Module.() -> Unit

fun module(createdAtStart: Boolean = false, override: Boolean = false, moduleDeclaration: ModuleDeclaration): Module {
// 创建 Module
val module = Module(createdAtStart, override)
// 执行匿扩展函数
moduleDeclaration(module)
return module
}

// 如何使用
val mModule: Module = module {
single { ... }
factory { ... }
}

Module 是一个 lambda 表达式,才可以在 “{}” 里面自由定义 single 和 factory,会等到你需要的时候才会执行。

案例二

1
2
3
4
5
6
7
8
9
复制代码inline fun <reified T : ViewModel> Module.viewModel(
qualifier: Qualifier? = null,
override: Boolean = false,
noinline definition: Definition<T>
): BeanDefinition<T> {
val beanDefinition = factory(qualifier, override, definition)
beanDefinition.setIsViewModel()
return beanDefinition
}

内联函数支持具体化的类型参数,使用 reified 修饰符来限定类型参数,以在函数内部访问它了,由于函数是内联的,不需要反射,通过上面两个案例,说明了为什么 Koin 可以做到无代码生成、无反射。建议大家都去看看 Koin 的源码,能够从中学到很多技巧,后面我会花好几篇文章分析 Koin 源码。

Inline 修饰符带来的性能损失

Inline (内联函数) 的作用:提升运行效率,调用被 inline 修饰符的函数,会把里面的代码放到我调用的地方。

如果阅读过 Koin 源码的朋友,应该会发现 inline 都是和 lambda 表达式和 reified 修饰符配套在一起使用的,如果只使用 inline 修饰符标记函数会怎么样?

只使用 inline 修饰符会有性能问题,在这篇文章 Consider inline modifier for higher-order functions 也分析了只使用 inline 修饰符为什么会带来性能问题,并且 Android Studio 也会给一个大大大的警告。

编译器建议我们在含有 lambda 表达式作为形参的函数中使用内联,既然 Inline 修饰符可以提升运行效率,为什么编译器会给我们一个警告? 为什么编译器建议 inline 修饰符需要和 lambda 表达式一起使用呢?

1. 既然 Inline 修饰符可以提升运行效率,为什么编译器会给我们一个警告?

刚才我们说过调用被 inline 修饰符的函数,会把里面的代码放到我调用的地方,来看一下下面这段代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码inline fun twoPrintTwo() {
print(2)
print(2)
}

inline fun twoTwoPrintTwo() {
twoPrintTwo()
twoPrintTwo()
}

inline fun twoTwoTwoPrintTwo() {
twoTwoPrintTwo()
twoTwoPrintTwo()
}

fun twoTwoTwoTwoPrintTwo() {
twoTwoTwoPrintTwo()
twoTwoTwoPrintTwo()
}

执行完最后一个方法 twoTwoTwoTwoPrintTwo,反编译出来的结果是非常令人吃惊的,结果如下所示:

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
复制代码public static final void twoTwoTwoTwoPrintTwo() {
byte var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print();
}

这显示了使用 Inline 修饰符的主要问题,当我们过度使用它们时,代码会快速增长。这就是为什么 IntelliJ 在我们使用它的时候会给出警告。

2. 为什么编译器建议 inline 修饰符需要和 lambda 表达式一起使用呢?

因为 JVM 是不支持 lambda 表达式的,非内联函数中的 Lambda 表达式会被编译为匿名类,这对性能开销是非常巨大的,而且它们的创建和使用都较慢,当我们使用 inline 修饰符时,我们根本不需要创建任何其他类,来看一下下面代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码fun main(args: Array<String>) {
var a = 0
// 带 inline 的 Lambda 表达式
repeat(100_000_000) {
a += 1
}
var b = 0

// 不带 inline 的 Lambda 表达式
noinlineRepeat(100_000_000) {
b += 1
}
}

编译结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码// Java 代码
public static final void main(@NotNull String[] args) {
int a = 0;
// 带 inline 的 Lambda 表达式, 会把里面的代码放到我调用的地方
int times$iv = 100000000;
int var3 = 0;

for(int var4 = times$iv; var3 < var4; ++var3) {
++a;
}

// 不带 inline 的 Lambda 表达式,会被编译为匿名类
final IntRef b = new IntRef();
b.element = 0;
noinlineRepeat(100000000, (Function1)(new Function1() {
public Object invoke(Object var1) {
++b.element;
return Unit.INSTANCE;
}
}));
}

那么我们应该在什么时候使用 inline 修饰符呢?

使用 inline 修饰符时最常见的场景就是把函数作为另一个函数的参数时(高阶函数),例如 filter、map、joinToString 或者一些独立的函数 repeat。

如果没有函数类型作为参数,也没有 reified 实化类型参数时,不应该使用 inline 修饰符了。

从分析 Koin 源码,inline 应该 lambda 表达式或者 reified 修饰符配合在一起使用的,另外 Android Studio 越来越智能了,如果在不正确的地方使用,会有一个大大大的警告。

Reified 修饰符,具体化的类型参数

reified (具体化的类型参数):使用 reified 修饰符来限定类型参数,结合着 inline 修饰符具体化的类型参数,可以直接在函数内部访问它。

我想分享两个使用 Reified 修饰符很常见的例子 reified-type-parameters,使用 Java 是不可能实现的。

案例一:

1
2
3
4
5
6
复制代码inline fun <reified T> Gson.fromJson(json: String) = 
fromJson(json, T::class.java)

// 使用
val user: User = Gson().fromJson(json)
val user = Gson().fromJson<User>(json)

案例二:

1
2
复制代码inline fun <reified T: Activity> Context.startActivity(vararg params: Pair<String, Any?>) =
AnkoInternals.internalStartActivity(this, T::class.java, params)

Koin 带来性能损失的那些事

思考了很久需不需要写这部分内容,因为在 Koin 2x 的版本的时候已经修复了,这是官方的链接 News from the trenches — What’s next for Koin?,后来想想还是写写吧,作为自己的一个学习笔记。

这个源于有个人开了一个 Issue(Bad performance in some Android devices) 现在已经被关闭了,他指出了当 Dependency 数量越来越多的时候,Koin 效能会越来越差,而且还做了一个对比如下图所示:

如果使用过 Koin 1x 的朋友应该会感觉到,引入 Koin 1x 冷启动时间边长了,而且在有大量依赖的时候,查找的时间会有点长,后来 Koin 团队也发现确实存在这个问题,到底是怎么回事呢?

他们在 BeanRegistry 类中维护了一个列表,用来存储了 BeanDefinition,然后使用 Kotlin 的 filter 函数找出对应的 BeanDefinition,所以找出一个 Definition 时间复杂度是 O(n),如果平均有 M 层 Dependency,那么时间复杂度会变成 O(m*n)。

Koin 团队的解决方案是用了 HashMap,使用空间换取时间,查找一个 Definition 时间复杂度变成了 O(1),优化之后的结果如下:

Koin 2x 不仅在性能优化上有很大的提升,也拓展了很多新的特性,例如 FragmentFactory 能够依赖注入到 Fragments 中就像 ViewModels 一样,还有自动拆箱等等,在后面的文章会详细的分析一下。

Kotlin 用 5 行代码实现快排算法

我想分享一个快速排序算法,这是一个很酷的函数编程的例子 share cool examples,当我看到这段代码的时候惊呆了,居然还可以这么写。

1
2
3
4
5
6
7
8
9
10
复制代码fun <T : Comparable<T>> List<T>.quickSort(): List<T> = 
if(size < 2) this
else {
val pivot = first()
val (smaller, greater) = drop(1).partition { it <= pivot}
smaller.quickSort() + pivot + greater.quickSort()
}

// 使用 [2,5,1] -> [1,2,5]
listOf(2,5,1).quickSort() // [1,2,5]

最后分享一个译者自己撸的导航网站

译者基于 Material Design 的响应式框架,撸了一个 “为互联网人而设计 国内国外名站导航“ ,收集了国内外热门网址,涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android开发等等导航网站,如果你有什么好的建议,也可以留言,点击前去浏览 如果对你有帮助,请帮我点个赞,感谢

ps: 网站中的地址如果有原作者不希望展示的,可以留言告诉我,我会立刻删除

国际资讯网址大全

Android 网址大全

参考文献

  • My favorite examples of functional programming in Kotlin
  • koin 官网:https://insert-koin.io/
  • Effective Kotlin: Consider inline modifier for higher-order functions

结语

致力于分享一系列 Android 系统源码、逆向分析、算法、翻译相关的文章,目前正在翻译一系列欧美精选文章,不仅仅是翻译,更重要的是翻译背后对每篇文章思考,如果你喜欢这片文章,请帮我点个赞,感谢,期待与你一起成长。

计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,目前已经包含了 App Startup、Paging3、Hilt 等等,正在逐渐增加其他 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请帮我点个赞,我会陆续完成更多 Jetpack 新成员的项目实践。

算法

由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序

  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……

每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一起来学习,期待与你一起成长

Android 10 源码系列

正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库

  • 0xA01 Android 10 源码分析:APK 是如何生成的
  • 0xA02 Android 10 源码分析:APK 的安装流程
  • 0xA03 Android 10 源码分析:APK 加载流程之资源加载
  • 0xA04 Android 10 源码分析:APK 加载流程之资源加载(二)
  • 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用
  • 0xA06 Android 10 源码分析:WindowManager 视图绑定以及体系结构
  • 更多

Android 应用系列

  • 如何高效获取视频截图
  • 如何在项目中封装 Kotlin + Android Databinding
  • [译][Google工程师] 刚刚发布了 Fragment 的新特性 “Fragment 间传递数据的新方式” 以及源码分析

工具系列

  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 关于 adb 命令你所需要知道的
  • 10分钟入门 Shell 脚本编程

逆向系列

  • 基于 Smali 文件 Android Studio 动态调试 APP
  • 解决在 Android Studio 3.2 找不到 Android Device Monitor 工具

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Autowired、Resource和Inject的区

发表于 2020-05-13

基本介绍

Spring中我们可以使用以下三个自动装配的注解进行依赖注入:

注解 所在包 来源
@Autowired org.springframework.beans.factory.annotation.Autowired Spring自带的注解
@Resource javax.annotation.Resource JSR-250标准的注解
@Inject javax.inject.Inject JSR-330标准的注解
  • @Autowired注解默认是按照类型(byType)装配依赖对象,默认情况下它要求依赖对象必须存在,当没有找到相应bean的时候,IOC容器就会报错。不过@Autowired有个required属性,可以配置为false,如果配置为false之后,当没有找到相应bean的时候就注入null,系统不会抛错。如果我们想使用按照名称(byName)来装配,可以结合@Qualifier注解一起使用。
  • @Resource默认按照名字(byName)装配依赖对象,由JAVAEE提供,需要导入包javax.annotation.Resource。@Resource有两个重要的属性:name和type,而Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以,如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不制定name也不制定type属性,这时将通过反射机制使用byName自动注入策略。
  • @Inject注解默认也是按照类型(byType)装配依赖对象,如果需要按名称进行装配,则需要配合@Named注解。@Inject 注解没有 required 属性,因此在找不到合适的依赖对象时 inject 会系统会报错失败。
    使用 @Inject 需要添加如下依赖:
1
2
3
4
5
复制代码  <dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>

实现原理

注解处理器

在Spring框架内部实现当中,注解实现注入主要是通过bean后置处理器BeanPostPocessor接口的实现类来生效的。BeanPostProcessor后置处理器是在spring容器启动时,创建bean对象实例后,马上执行的,对bean对象实例进行加工处理。
  • @Autowired是通过BeanPostProcessor接口的实现类AutowiredAnnotationBeanPostProcessor来实现对bean对象对其他bean对象的依赖注入的;
  • @Resource和@Inject是通过BeanPostProcessor接口的实现类CommonAnnotationBeanPostProcessor来实现的,顾名思义即公共注解CommonAnotation,CommonAnnotationBeanPostProcessor是Spring中统一处理JDK中定义的注解的一个BeanPostProcessor。该类会处理的注解还包括@PostConstruct,@PreDestroy等。

注解处理器的激活条件

AutowiredAnnotationBeanPostProcessor和CommonAnnotationBeanPostProcessor添加到Spring容器的BeanPostProcessor的条件,即激活这些处理器的条件如下:

1. 基于xml的Spring配置

在对应的Spring容器的配置xml文件中,如applicationContext.xml,添加<context:annotation-config />和<context:component-scan />,或者只使用<context:component-scan />。
两者的区别是:<context:annotation-config />只查找并激活已经存在的bean,如通过xml文件的bean标签生成加载到Spring容器的,而不会去扫描如@Controller等注解的bean,查找到之后进行注入;而<context:component-scan />除了具有<context:annotation-config />的功能之外,还会去加载通过basePackages属性指定的包下面的,默认为扫描@Controller,@Service,@Component,@Repository注解的类。不指定basePackages则是类路径下面,或者如果使用注解@ComponentScan方式,则是当前类所在包及其子包下面。

2. 基于配置类的Spring配置

如果是基于配置类而不是基于applicationContext.xml来对Spring进行配置,如SpringBoot,则在内部使用的IOC容器实现为AnnotationConfigApplicationContext或者其派生类,在AnnotationConfigApplicationContext内部会自动创建和激活以上的BeanPostProcessor。
如果同时存在基于xml的配置和配置类的配置,而在注入时间方面,基于注解的注入先于基于XML的注入,所以基于XML的注入会覆盖基于注解的注入。

三个注解的异同

  1. @Autowired是Spring自带的,@Inject和@Resource都是JDK提供的,其中@Inject是JSR330规范的实现,@Resource是JSR250规范的实现。
  2. @Autowired和@Inject基本是一样的,因为两者都是使AutowiredAnnotationBeanPostProcessor来处理依赖注入。但是@Resource不一样,它使用的是CommonAnnotationBeanPostProcessor来处理依赖注入。当然,两者都是BeanPostProcessor。
  3. @Autowired和@Inject主要区别是@Autowired可以设置required属性为false,而@Inject并没有这个设置选项。
  4. @Resource默认是按照byName进行注入,而@Autowired和@Inject默认是按照byType进行注入。
  5. @Autowired通过@Qualifier指定注入特定bean,@Resource可以通过参数name指定注入bean,@Inject需要@Named注解指定注入bean。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

请勿过度依赖Redis的过期监听

发表于 2020-05-13

Redis过期监听场景

业务中有类似等待一定时间之后执行某种行为的需求 , 比如30分钟之后关闭订单 . 网上有很多使用Redis过期监听的Demo , 但是其实这是个大坑 , 因为Redis不能确保key在指定时间被删除 , 也就造成了通知的延期 . 不多说 , 跑个测试

测试情况

先说环境 , redis 运行在Docker容器中 ,分配了 一个cpu以及512MB内存, 在Docker中执行 redis-benchmark -t set -r 100000 -n 1000000 结果如下:

1
2
3
4
5
6
7
8
复制代码====== SET ======
1000000 requests completed in 171.03 seconds
50 parallel clients
3 bytes payload
keep alive: 1
host configuration "save": 3600 1 300 100 60 10000
host configuration "appendonly": no
multi-thread: no

其实这里有些不严谨 benchmark 线程不应该在Docker容器内部运行 . 跑分的时候大概 benchmark 和redis 主线程各自持有50%CPU

测试代码如下:

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
复制代码@Service
@Slf4j
public class RedisJob {
@Autowired
private StringRedisTemplate stringRedisTemplate;

public DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public LocalDateTime end = LocalDateTime.of(LocalDate.of(2020, 5, 12), LocalTime.of(8, 0));

@Scheduled(cron = "0 56 * * * ?")
public void initKeys() {
LocalDateTime now = LocalDateTime.now();
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
log.info("开始设置key");
LocalDateTime begin = now.withMinute(0).withSecond(0).withNano(0);
for (int i = 1; i < 17; i++) {
setExpireKey(begin.plusHours(i), 8, operations);
}
log.info("设置完毕: " + Duration.between(now, LocalDateTime.now()));
}

private void setExpireKey(LocalDateTime expireTime, int step, ValueOperations<String, String> operations) {
LocalDateTime localDateTime = LocalDateTime.now().withNano(0);
String nowTime = dateTimeFormatter.format(localDateTime);
while (expireTime.getMinute() < 55) {
operations.set(nowTime + "@" + dateTimeFormatter.format(expireTime), "A", Duration.between(expireTime, LocalDateTime.now()).abs());
expireTime = expireTime.plusSeconds(step);
}
}
}

大概意思就是每小时56分的时候 , 会增加一批在接下来16小时过期的key , 过期时间间隔8秒 , 且过期时间都在55分之前

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
复制代码@Slf4j
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {

public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}

public DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Autowired
private StringRedisTemplate stringRedisTemplate;


@Override
public void onMessage(Message message, byte[] pattern) {
String keyName = new String(message.getBody());
LocalDateTime parse = LocalDateTime.parse(keyName.split("@")[1], dateTimeFormatter);
long seconds = Duration.between(parse, LocalDateTime.now()).getSeconds();
stringRedisTemplate.execute((RedisCallback<Object>) connection -> {
Long size = connection.dbSize();
log.info("过期key:" + keyName + " ,当前size:" + size + " ,滞后时间" + seconds);
return null;
});
}
}

这里是监测到过期之后打印当前的dbSize 以及滞后时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码@Bean
public RedisMessageListenerContainer configRedisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(100);
executor.setMaxPoolSize(100);
executor.setQueueCapacity(100);
executor.setKeepAliveSeconds(3600);
executor.setThreadNamePrefix("redis");
// rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CALLER_RUNS:不在新线程中执行任务,而是由调用者所在的线程来执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
// 设置Redis的连接工厂
container.setConnectionFactory(connectionFactory);
// 设置监听使用的线程池
container.setTaskExecutor(executor);
// 设置监听的Topic
return container;
}

设置Redis的过期监听 以及线程池信息 ,

最后的测试结果是当key数量小于1万的时候 , 基本上都可以在10s内完成过期通知 , 但是如果数量到3万 , 就有部分key会延迟120s . 顺便贴一下我最新的日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码2020-05-13 22:16:48.383  : 过期key:2020-05-13 11:56:02@2020-05-13 22:14:08 ,当前size:57405 ,滞后时间160
2020-05-13 22:16:49.389 : 过期key:2020-05-13 11:56:02@2020-05-13 22:14:32 ,当前size:57404 ,滞后时间137
2020-05-13 22:16:49.591 : 过期key:2020-05-13 10:56:02@2020-05-13 22:13:20 ,当前size:57403 ,滞后时间209
2020-05-13 22:16:50.093 : 过期key:2020-05-13 20:56:00@2020-05-13 22:12:32 ,当前size:57402 ,滞后时间258
2020-05-13 22:16:50.596 : 过期key:2020-05-13 07:56:03@2020-05-13 22:13:28 ,当前size:57401 ,滞后时间202
2020-05-13 22:16:50.697 : 过期key:2020-05-13 20:56:00@2020-05-13 22:14:32 ,当前size:57400 ,滞后时间138
2020-05-13 22:16:50.999 : 过期key:2020-05-13 19:56:00@2020-05-13 22:13:44 ,当前size:57399 ,滞后时间186
2020-05-13 22:16:51.199 : 过期key:2020-05-13 20:56:00@2020-05-13 22:14:40 ,当前size:57398 ,滞后时间131
2020-05-13 22:16:52.205 : 过期key:2020-05-13 15:56:01@2020-05-13 22:16:24 ,当前size:57397 ,滞后时间28
2020-05-13 22:16:52.808 : 过期key:2020-05-13 06:56:03@2020-05-13 22:15:04 ,当前size:57396 ,滞后时间108
2020-05-13 22:16:53.009 : 过期key:2020-05-13 06:56:03@2020-05-13 22:16:40 ,当前size:57395 ,滞后时间13
2020-05-13 22:16:53.110 : 过期key:2020-05-13 20:56:00@2020-05-13 22:14:56 ,当前size:57394 ,滞后时间117
2020-05-13 22:16:53.211 : 过期key:2020-05-13 06:56:03@2020-05-13 22:13:44 ,当前size:57393 ,滞后时间189
2020-05-13 22:16:53.613 : 过期key:2020-05-13 15:56:01@2020-05-13 22:12:24 ,当前size:57392 ,滞后时间269
2020-05-13 22:16:54.317 : 过期key:2020-05-13 15:56:01@2020-05-13 22:16:00 ,当前size:57391 ,滞后时间54
2020-05-13 22:16:54.517 : 过期key:2020-05-13 18:56:00@2020-05-13 22:15:44 ,当前size:57390 ,滞后时间70
2020-05-13 22:16:54.618 : 过期key:2020-05-13 21:56:00@2020-05-13 22:14:24 ,当前size:57389 ,滞后时间150
2020-05-13 22:16:54.819 : 过期key:2020-05-13 17:56:00@2020-05-13 22:14:40 ,当前size:57388 ,滞后时间134
2020-05-13 22:16:55.322 : 过期key:2020-05-13 10:56:02@2020-05-13 22:13:52 ,当前size:57387 ,滞后时间183
2020-05-13 22:16:55.423 : 过期key:2020-05-13 07:56:03@2020-05-13 22:14:16 ,当前size:57386 ,滞后时间159

可以看到 ,当数量到达5万的时候 , 大部分都已经滞后了两分钟 , 对于业务方来说已经完全无法忍受了

总结

可能到这里 , 你会说Redis 给你挖了一个大坑 , 但其实这些都在文档上写的明明白白

  • How Redis expires keys
  • Timing of expired events

尤其是在 Timing of expired events 中 , 明确的说明了 “Basically expired events are generated when the Redis server deletes the key and not when the time to live theoretically reaches the value of zero.” , 这两个文章读下来你会感觉 , 卧槽Redis的过期策略其实也挺’Low’的

其实公众号看多了 , 你会发现大部分Demo都是互相抄来抄去 , 以及翻译官方Demo . 建议大家还是谨慎一些 , 真要使用的话 , 最好读一下官方文档 , 哪怕用百度翻译也要有一些自己的理解 .

文章比较枯燥 , 感谢大家耐心阅读 , 如有建议 恳请留言.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

View Binding 与Kotlin委托属性的巧妙结合,

发表于 2020-05-13

前言

最近看到一篇使用Kotlin委托属性来消除使用ViewBinding过程中样板代码的文章,觉得不错,因此翻译给大家,原文地址:

proandroiddev.com/make-androi…

正文

ViewBinding 是Android Studio 3.6中添加的一个新功能,更准确的说,它是DataBinding 的一个更轻量变体,为什么要使用View Binding 呢?答案是性能。许多开发者使用Data Binding库来引用Layout XML中的视图,而忽略它的其他强大功能。相比来说,自动生成代码ViewBinding其实比DataBinding 性能更好。但是传统的方式使用View Binding 却不是很好,因为会有很多样板代码(垃圾代码)。

View Binding 的传统使用方式

让我们看看Fragment 中“ViewBinding”的用法。我们有一个布局资源profile.xml。View Binding 为布局文件生成的类叫ProfileBinding,传统使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码class ProfileFragment : Fragment(R.layout.profile) {

private var viewBinding: ProfileBinding? = null

override fun onViewCreated(view: View, savedState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewBinding = ProfileBinding.bind(view)
// Use viewBinding
}

override fun onDestroyView() {
super.onDestroyView()
viewBinding = null
}
}

有几点我不太喜欢:

  • 创建和销毁viewBinding的样板代码
  • 如果有很多Fragment,每一个都要拷贝一份相同的代码
  • viewBinding 属性是可空的,并且可变的,这可不太妙

怎么办呢?用强大Kotlin来重构它。

Kotlin 委托属性结合ViewBinding

使用Kotlin委托的属性,我们可以重用部分代码并简化任务(不明白委托属性的,可以看我(译者)以前的文章:一文彻底搞懂Kotlin中的委托),我用它来简化·ViewBinding的用法。用一个委托包装了ViewBinding`的创建和销毁。

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
复制代码class FragmentViewBindingProperty<T : ViewBinding>(
private val viewBinder: ViewBinder<T>
) : ReadOnlyProperty<Fragment, T> {

private var viewBinding: T? = null
private val lifecycleObserver = BindingLifecycleObserver()

@MainThread
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
checkIsMainThread()
this.viewBinding?.let { return it }

val view = thisRef.requireView()
thisRef.viewLifecycleOwner.lifecycle.addObserver(lifecycleObserver)
return viewBinder.bind(view).also { vb -> this.viewBinding = vb }
}

private inner class BindingLifecycleObserver : DefaultLifecycleObserver {

private val mainHandler = Handler(Looper.getMainLooper())

@MainThread
override fun onDestroy(owner: LifecycleOwner) {
owner.lifecycle.removeObserver(this)
viewBinding = null
}
}
}

/**
* Create new [ViewBinding] associated with the [Fragment][this]
*/
@Suppress("unused")
inline fun <reified T : ViewBinding> Fragment.viewBinding(): ReadOnlyProperty<Fragment, T> {
return FragmentViewBindingProperty(DefaultViewBinder(T::class.java))
}

然后,使用我们定义的委托来重构ProfileFragment:

1
2
3
4
5
6
7
8
9
复制代码class ProfileFragment : Fragment(R.layout.profile) {

private val viewBinding: ProfileBinding by viewBinding()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Use viewBinding
}
}

很好,我们去掉了创建和销毁ViewBinding的样板代码,现在只需要声明一个委托属性就可以了,是不是简单了?但是现在还有点问题。

问题来了

在重构之后,onDestroyView 需要清理掉viewBinding中的View。

1
2
3
4
5
6
7
8
9
10
复制代码class ProfileFragment() : Fragment(R.layout.profile) {

private val viewBinding: ProfileBinding by viewBinding()

override fun onDestroyView() {
super.onDestroyView()
// Clear data in views from viewBinding
// ViewBinding inside viewBinding is null
}
}

但是,结果是,我得到的在委托属性内对ViewBinding的引用为null。原因是Fragment的ViewLifecycleOwner通知更新lifecycle的ON_DESTROY事件时机,该事件发生在Fragment.onDestroyView()之前。这就是为什么我仅在主线程上的所有操作完成后才需要清除viewBinding。可以使用Handler.post完成。修改如下:

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
复制代码class FragmentViewBindingProperty<T : ViewBinding>(
private val viewBinder: ViewBinder<T>
) : ReadOnlyProperty<Fragment, T> {

private var viewBinding: T? = null
private val lifecycleObserver = BindingLifecycleObserver()

@MainThread
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
checkIsMainThread()
this.viewBinding?.let { return it }

val view = thisRef.requireView()
thisRef.viewLifecycleOwner.lifecycle.addObserver(lifecycleObserver)
return viewBinder.bind(view).also { vb -> this.viewBinding = vb }
}

private inner class BindingLifecycleObserver : DefaultLifecycleObserver {

private val mainHandler = Handler(Looper.getMainLooper())

@MainThread
override fun onDestroy(owner: LifecycleOwner) {
owner.lifecycle.removeObserver(this)
// Fragment.viewLifecycleOwner call LifecycleObserver.onDestroy() before Fragment.onDestroyView().
// That's why we need to postpone reset of the viewBinding
mainHandler.post {
viewBinding = null
}
}
}
}

这样,就很完美了。

Android的新库ViewBinding是一个去掉项目中findViewByid()很好的解决方案,同时它也替代了著名的Butter Knife。ViewBinding 与Kotlin委托属性的巧妙结合,可以让你的代码更加简洁易读。完整的代码可以查看github:github.com/kirich1409/…

每天都有干货文章持续更新,可以微信搜索「 技术最TOP 」第一时间阅读,回复【思维导图】【面试】【简历】有我准备一些Android进阶路线、面试指导和简历模板送给你

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

字节码编程,Byte-buddy篇二《监控方法执行耗时动态获

发表于 2020-05-13

作者:小傅哥

博客:bugstack.cn

❝
沉淀、分享、成长,让自己和他人都能有所收获!

❞

一、前言

「案例」是剥去外衣包装展示出核心功能的最佳学习方式!

就像是我们研究字节码编程最终是需要应用到实际场景中,例如:实现一款非入侵的全链路最终监控系统,那么这里就会包括一些基本的核心功能点;方法执行耗时、出入参获取、异常捕获、添加链路ID等等。而这些一个个的功能点,最快的掌握方式就是去实现他最基本的功能验证,这个阶段基本也是技术选型的阶段,验证各项技术点是否可以满足你后续开发的需求。否则在后续开发中,如果已经走了很远的时候再发现不适合,那么到时候就很麻烦了。

在前面的ASM、Javassist 章节中也有陆续实现过获取方法的出入参信息,但实现的方式还是偏向于字节码控制,尤其ASM,更是需要使用到字节码指令将入参信息压栈操作保存到局部变量用于输出,在这个过程中需要深入了解Java虚拟机规范,否则很不好完成这一项的开发。但!ASM也是性能最牛的。其他的字节码编程框架都是基于它所开发的。「关于这部分系列文章可以访问链接进行专题系列的学习」:bugstack.cn/itstack/its…

「那么」,本章节我们会使用 Byte-buddy 来实现这一功能,在接下来的操作中你会感受到这个字节码框架的魅力,它的API更加高级也更符合普遍易接受的操作方式进行处理。

二、开发环境

  1. JDK 1.8.0
  2. byte-buddy 1.10.9
  3. byte-buddy-agent 1.10.9
  4. 本章涉及源码在:itstack-demo-bytecode-2-02,可以关注「公众号」:bugstack虫洞栈,回复源码下载获取。你会获得一个下载链接列表,打开后里面的第17个「因为我有好多开源代码」,记得给个Star!

三、案例目标

在这里我们定义一个类并创建出等待被监控的方法,当方法执行时监控方法的各项信息;执行耗时、出入参信息等。

1
2
3
4
5
6
7
8
复制代码public class BizMethod {

public String queryUserInfo(String uid, String token) throws InterruptedException {
Thread.sleep(new Random().nextInt(500));
return "德莱联盟,王牌工程师。小傅哥(公众号:bugstack虫洞栈),申请出栈!";
}

}
  • 我们这里模拟监控并没有使用 Javaagent 去做字节码加载时的增强,主要为了将「最核心」的内容体现出来。后续的章节会陆续讲解各个核心功能的组合使用,做出一套监控系统。

四、技术实现

在技术实现的过程中,我会陆续的将需要监控的内容一步步完善。这样将一个总体的内容进行拆解后,方便学习和理解。

1. 创建监控主体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码@Test
public void test_byteBuddy() throws Exception {
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
.subclass(BizMethod.class)
.method(ElementMatchers.named("queryUserInfo"))
.intercept(MethodDelegation.to(MonitorDemo.class))
.make();

// 加载类
Class<?> clazz = dynamicType.load(ApiTest.class.getClassLoader())
.getLoaded();

// 反射调用
clazz.getMethod("queryUserInfo", String.class, String.class).invoke(clazz.newInstance(), "10001", "Adhl9dkl");
}
  • 这一部分是 Byte Buddy 的模版代码,定义需要被加载的类和方法;BizMethod.class、ElementMatchers.named(“queryUserInfo”),这一步也就是让程序可以定位到你的被监控内容。
  • 接下来就是最重要的一部分「委托」;MethodDelegation.to(MonitorDemo.class),最终所有的监控操作都会被 MonitorDemo.class 类中的方法进行处理。
  • 最后就是类的加载和反射调用,这部分主要用于每次的测试验证。查找方法,传递对象和入参信息

2. 监控方法耗时

如上一步所述这里主要需要使用到,委托类进行控制监控信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码public class MonitorDemo {

@RuntimeType
public static Object intercept(@SuperCall Callable<?> callable) throws Exception {
long start = System.currentTimeMillis();
try {
return callable.call();
} finally {
System.out.println("方法耗时:" + (System.currentTimeMillis() - start) + "ms");
}
}

}
  • 这里面包括几个核心的知识点;@RuntimeType:定义运行时的目标方法。@SuperCall:用于调用父类版本的方法。
  • 定义好方法后,下面有一个 callable.call(); 这个方法是调用原方法的内容,返回结果。而前后包装的。
  • 最后在finally中,打印方法的执行耗时。System.currentTimeMillis() - start

「测试结果:」

1
2
3
复制代码方法耗时:419ms

Process finished with exit code 0

3. 获取方法信息

获取方法信息的过程其实就是在获取方法的描述内容,也就是你编写的方法拆解为各个内容进行输出。那么为了实现这样的功能我们需要使用到新的注解 @Origin Method method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码@RuntimeType
public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Exception {
long start = System.currentTimeMillis();
Object resObj = null;
try {
resObj = callable.call();
return resObj;
} finally {
System.out.println("方法名称:" + method.getName());
System.out.println("入参个数:" + method.getParameterCount());
System.out.println("入参类型:" + method.getParameterTypes()[0].getTypeName() + "、" + method.getParameterTypes()[1].getTypeName());
System.out.println("出参类型:" + method.getReturnType().getName());
System.out.println("出参结果:" + resObj);
System.out.println("方法耗时:" + (System.currentTimeMillis() - start) + "ms");
}
}
  • @Origin,用于拦截原有方法,这样就可以获取到方法中的相关信息。
  • 这一部分的信息相对来说比较全,尤其也获取到了参数的个数和类型,这样就可以在后续的处理参数时进行循环输出。

「测试结果:」

1
2
3
4
5
6
7
8
复制代码方法名称:queryUserInfo
入参个数:2
入参类型:java.lang.String、java.lang.String
出参类型:java.lang.String
出参结果:德莱联盟,王牌工程师。小傅哥(公众号:bugstack虫洞栈),申请出栈!
方法耗时:490ms

Process finished with exit code 0

4. 获取入参内容

当我们能获取入参的基本描述以后,再者就是获取入参的内容。在一段方法执行的过程中,如果可以在必要的时候拿到当时入参的信息,那么就可以非常方便的进行排查异常快速定位问题。在这里我们会用到新的注解;@AllArguments 、@Argument(0),一个用于获取全部参数,一个获取指定的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码@RuntimeType
public static Object intercept(@Origin Method method, @AllArguments Object[] args, @Argument(0) Object arg0, @SuperCall Callable<?> callable) throws Exception {
long start = System.currentTimeMillis();
Object resObj = null;
try {
resObj = callable.call();
return resObj;
} finally {
System.out.println("方法名称:" + method.getName());
System.out.println("入参个数:" + method.getParameterCount());
System.out.println("入参类型:" + method.getParameterTypes()[0].getTypeName() + "、" + method.getParameterTypes()[1].getTypeName());
System.out.println("入参内容:" + arg0 + "、" + args[1]);
System.out.println("出参类型:" + method.getReturnType().getName());
System.out.println("出参结果:" + resObj);
System.out.println("方法耗时:" + (System.currentTimeMillis() - start) + "ms");
}
}
  • 与上面的代码块相比,多了参数的获取和打印。主要知道这个方法就可以很方便的获取入参的内容。

「测试结果:」

1
2
3
4
5
6
7
8
9
复制代码方法名称:queryUserInfo
入参个数:2
入参类型:java.lang.String、java.lang.String
入参内容:10001、Adhl9dkl
出参类型:java.lang.String
出参结果:德莱联盟,王牌工程师。小傅哥(公众号:bugstack虫洞栈),申请出栈!
方法耗时:405ms

Process finished with exit code 0

5. 其他注解汇总

除了以上为了获取方法的执行信息使用到的注解外,Byte Buddy 还提供了很多其他的注解。如下;

注解 说明
@Argument 绑定单个参数
@AllArguments 绑定所有参数的数组
@This 当前被拦截的、动态生成的那个对象
@Super 当前被拦截的、动态生成的那个对象的父类对象
@Origin 可以绑定到以下类型的参数:Method 被调用的原始方法 Constructor 被调用的原始构造器 Class 当前动态创建的类 MethodHandle MethodType String 动态类的toString()的返回值 int 动态方法的修饰符
@DefaultCall 调用默认方法而非super的方法
@SuperCall 用于调用父类版本的方法
@Super 注入父类型对象,可以是接口,从而调用它的任何方法
@RuntimeType 可以用在返回值、参数上,提示ByteBuddy禁用严格的类型检查
@Empty 注入参数的类型的默认值
@StubValue 注入一个存根值。对于返回引用、void的方法,注入null;对于返回原始类型的方法,注入0
@FieldValue 注入被拦截对象的一个字段的值
@Morph 类似于@SuperCall,但是允许指定调用参数

6. 常用核心API

  1. ByteBuddy
* 流式API方式的入口类
* 提供Subclassing/Redefining/Rebasing方式改写字节码
* 所有的操作依赖DynamicType.Builder进行,创建不可变的对象
  1. ElementMatchers(ElementMatcher)
* 提供一系列的元素匹配的工具类(named/any/nameEndsWith等等)
* ElementMatcher(提供对类型、方法、字段、注解进行matches的方式,类似于Predicate)
* Junction对多个ElementMatcher进行了and/or操作
  1. DynamicType(动态类型,所有字节码操作的开始,非常值得关注)
* Unloaded(动态创建的字节码还未加载进入到虚拟机,需要类加载器进行加载)
* Loaded(已加载到jvm中后,解析出Class表示)
* Default(DynamicType的默认实现,完成相关实际操作)
  1. Implementation(用于提供动态方法的实现)
* FixedValue(方法调用返回固定值)
* MethodDelegation(方法调用委托,支持两种方式: Class的static方法调用、object的instance method方法调用)
  1. Builder(用于创建DynamicType,相关接口以及实现后续待详解)
* MethodDefinition
* FieldDefinition
* AbstractBase

五、总结

  • 在这一章节的实现过程来看,只要知道相关API就可以很方便的解决我们的监控方法信息的诉求,他所处理的方式非常易于使用。而在本章节中也要学会几个关键知识点;委托、方法注解、返回值注解以及入参注解。
  • 当我们学会了监控的核心功能,在后续与Javaagent结合使用时就可以很容易扩展进去,而不是看到了陌生的代码。对于这一部分非入侵的入侵链路监控,也是目前比较热门的话题和需要探索的解决方案,就像最近阿里云也举办了类似的编程挑战赛。首届云原生编程挑战赛1:实现一个分布式统计和过滤的链路追踪
  • 关于字节码编程专栏已经完成了大部分文章的编写,包括如下文章;(「学习链接」:https://bugstack.cn/itstack/itstack-demo-bytecode.html)
+ [`字节码编程,Byte-buddy篇一《基于Byte Buddy语法创建的第一个HelloWorld》`](https://bugstack.cn/itstack/itstack-demo-bytecode.html)
+ [`字节码编程,Javassist篇五《使用Bytecode指令码生成含有自定义注解的类和方法》`](https://bugstack.cn/itstack/itstack-demo-bytecode.html)
+ [`字节码编程,Javassist篇四《通过字节码插桩监控方法采集运行时入参出参和异常信息》`](https://bugstack.cn/itstack/itstack-demo-bytecode.html)
+ [`字节码编程,Javassist篇三《使用Javassist在运行时重新加载类「替换原方法输出不一样的结果」》`](https://bugstack.cn/itstack/itstack-demo-bytecode.html)
+ [`字节码编程,Javassist篇二《定义属性以及创建方法时多种入参和出参类型的使用》`](https://bugstack.cn/itstack/itstack-demo-bytecode.html)
+ [`字节码编程,Javassist篇一《基于javassist的第一个案例helloworld》`](https://bugstack.cn/itstack/itstack-demo-bytecode.html)
+ [`ASM字节码编程 | 用字节码增强技术给所有方法加上TryCatch捕获异常并输出`](https://bugstack.cn/itstack/itstack-demo-bytecode.html)
+ [`ASM字节码编程 | JavaAgent+ASM字节码插桩采集方法名称以及入参和出参结果并记录方法耗时`](https://bugstack.cn/itstack/itstack-demo-bytecode.html)
+ [`ASM字节码编程 | 如果你只写CRUD,那这种技术你永远碰不到`](https://bugstack.cn/itstack/itstack-demo-bytecode.html)
  • 「最佳的学习体验和方式」是,在学习和探索的过程中不断的对知识进行深度的学习,通过一个个实践的方式让知识成结构化和体系的建设。

六、彩蛋

CodeGuide | 程序员编码指南 Go!

本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!

CodeGuide | 程序员编码指南

CodeGuide | 程序员编码指南

bugstack虫洞栈
沉淀、分享、成长,让自己和他人都能有所收获!
作者小傅哥多年从事一线互联网Java开发,从19年开始编写工作和学习历程的技术汇总,旨在为大家提供一个较清晰详细的核心技能学习文档。如果本文能为您提供帮助,请给予支持(关注、点赞、分享)!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

都2020年了,还在用if(obj!=null)做非空判断?

发表于 2020-05-12

轻松实战性理解Optional

1.前言

相信不少小伙伴已经被java的NPE(Null Pointer Exception)所谓的空指针异常搞的头昏脑涨,
有大佬说过“防止 NPE,是程序员的基本修养。”但是修养归修养,也是我们程序员最头疼的问题之一,那么我们今天就要尽可能的利用Java8的新特性 Optional来尽量简化代码同时高效处理NPE(Null Pointer Exception 空指针异常)

2.认识Optional并使用

简单来说,Opitonal类就是Java提供的为了解决大家平时判断对象是否为空用 会用 null!=obj 这样的方式存在的判断,从而令人头疼导致NPE(Null Pointer Exception 空指针异常),同时Optional的存在可以让代码更加简单,可读性跟高,代码写起来更高效.

1
2
3
4
5
6
7
8
复制代码   常规判断:
//对象 人
//属性有 name,age
Person person=new Person();
if (null==person){
return "person为null";
}
return person;
1
2
3
4
5
复制代码   使用Optional:
//对象 人
//属性有 name,age
Person person=new Person();
return Optional.ofNullable(person).orElse("person为null");

测试展示类Person代码(如果有朋友不明白可以看一下这个):

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
复制代码
public class Person {
private String name;
private Integer age;

public Person(String name, Integer age) {
this.name = name;
this.age = age;
}

public Person() {
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}
}

下面,我们就高效的学习一下神奇的Optional类!

2.1 Optional对象创建

首先我们先打开Optional的内部,去一探究竟
先把几个创建Optional对象的方法提取出来

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
复制代码public final class Optional<T> {
private static final Optional<?> EMPTY = new Optional<>();
private final T value;
//我们可以看到两个构造方格都是private 私有的
//说明 我们没办法在外面去new出来Optional对象
private Optional() {
this.value = null;
}
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}
//这个静态方法大致 是创建出一个包装值为空的一个对象因为没有任何参数赋值
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
//这个静态方法大致 是创建出一个包装值非空的一个对象 因为做了赋值
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
//这个静态方法大致是 如果参数value为空,则创建空对象,如果不为空,则创建有参对象
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
}

再做一个简单的实例展示
与上面对应

1
2
3
4
5
6
7
复制代码        // 1、创建一个包装对象值为空的Optional对象
Optional<String> optEmpty = Optional.empty();
// 2、创建包装对象值非空的Optional对象
Optional<String> optOf = Optional.of("optional");
// 3、创建包装对象值允许为空也可以不为空的Optional对象
Optional<String> optOfNullable1 = Optional.ofNullable(null);
Optional<String> optOfNullable2 = Optional.ofNullable("optional");

我们关于创建Optional对象的内部方法大致分析完毕
接下来也正式的进入Optional的学习与使用中

2.2 Optional.get()方法(返回对象的值)

get()方法是返回一个option的实例值
源码:

1
2
3
4
5
6
复制代码    public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}

也就是如果value不为空则做返回,如果为空则抛出异常 “No value present”
简单实例展示

1
2
3
复制代码        Person person=new Person();
person.setAge(2);
Optional.ofNullable(person).get();

2.3 Optional.isPresent()方法(判读是否为空)

isPresent()方法就是会返回一个boolean类型值,如果对象不为空则为真,如果为空则false
源码:

1
2
3
复制代码 public boolean isPresent() {
return value != null;
}

简单的实例展示:

1
2
3
4
5
6
7
8
9
10
复制代码       
Person person=new Person();
person.setAge(2);
if (Optional.ofNullable(person).isPresent()){
//写不为空的逻辑
System.out.println("不为空");
}else{
//写为空的逻辑
System.out.println("为空");
}

2.4 Optional.ifPresent()方法(判读是否为空并返回函数)

这个意思是如果对象非空,则运行函数体
源码:

1
2
3
4
5
复制代码  public void ifPresent(Consumer<? super T> consumer) {
//如果value不为空,则运行accept方法体
if (value != null)
consumer.accept(value);
}

看实例:

1
2
3
复制代码        Person person=new Person();
person.setAge(2);
Optional.ofNullable(person).ifPresent(p -> System.out.println("年龄"+p.getAge()));

如果对象不为空,则会打印这个年龄,因为内部已经做了NPE(非空判断),所以就不用担心空指针异常了

2.5 Optional.filter()方法(过滤对象)

filter()方法大致意思是,接受一个对象,然后对他进行条件过滤,如果条件符合则返回Optional对象本身,如果不符合则返回空Optional
源码:

1
2
3
4
5
6
7
8
9
复制代码    public Optional<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate);
//如果为空直接返回this
if (!isPresent())
return this;
else
//判断返回本身还是空Optional
return predicate.test(value) ? this : empty();
}

简单实例:

1
2
3
复制代码        Person person=new Person();
person.setAge(2);
Optional.ofNullable(person).filter(p -> p.getAge()>50);

2.6 Optional.map()方法(对象进行二次包装)

map()方法将对应Funcation函数式接口中的对象,进行二次运算,封装成新的对象然后返回在Optional中
源码:

1
2
3
4
5
6
7
8
9
10
复制代码 public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
//如果为空返回自己
if (!isPresent())
return empty();
else {
//否则返回用方法修饰过的Optional
return Optional.ofNullable(mapper.apply(value));
}
}

实例展示:

1
2
3
复制代码        Person person1=new Person();
person.setAge(2);
String optName = Optional.ofNullable(person).map(p -> person.getName()).orElse("name为空");

2.7 Optional.flatMap()方法(Optional对象进行二次包装)

map()方法将对应Optional< Funcation >函数式接口中的对象,进行二次运算,封装成新的对象然后返回在Optional中
源码:

1
2
3
4
5
6
7
8
复制代码    public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));
}
}

实例:

1
2
3
复制代码        Person person=new Person();
person.setAge(2);
Optional<Object> optName = Optional.ofNullable(person).map(p -> Optional.ofNullable(p.getName()).orElse("name为空"));

2.8 Optional.orElse()方法(为空返回对象)

常用方法之一,这个方法意思是如果包装对象为空的话,就执行orElse方法里的value,如果非空,则返回写入对象
源码:

1
2
3
4
复制代码    public T orElse(T other) {
//如果非空,返回value,如果为空,返回other
return value != null ? value : other;
}

实例:

1
2
3
复制代码        Person person1=new Person();
person.setAge(2);
Optional.ofNullable(person).orElse(new Person("小明", 2));

2.9 Optional.orElseGet()方法(为空返回Supplier对象)

这个与orElse很相似,入参不一样,入参为Supplier对象,为空返回传入对象的.get()方法,如果非空则返回当前对象
源码:

1
2
3
复制代码    public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}

实例:

1
2
3
复制代码         Optional<Supplier<Person>> sup=Optional.ofNullable(Person::new);
//调用get()方法,此时才会调用对象的构造方法,即获得到真正对象
Optional.ofNullable(person).orElseGet(sup.get());

说真的对于Supplier对象我也懵逼了一下,去网上简单查阅才得知 Supplier也是创建对象的一种方式,简单来说,Suppiler是一个接口,是类似Spring的懒加载,声明之后并不会占用内存,只有执行了get()方法之后,才会调用构造方法创建出对象
创建对象的语法的话就是Supplier<Person> supPerson= Person::new;

需要使用时supPerson.get()即可

2.10 Optional.orElseThrow()方法(为空返回异常)

这个我个人在实战中也经常用到这个方法,方法作用的话就是如果为空,就抛出你定义的异常,如果不为空返回当前对象,在实战中所有异常肯定是要处理好的,为了代码的可读性
源码:

1
2
3
4
5
6
7
复制代码    public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}

实例:
这个就贴实战源码了

1
2
3
复制代码//简单的一个查询
Member member = memberService.selectByPhone(request.getPhone());
Optional.ofNullable(member).orElseThrow(() -> new ServiceException("没有查询的相关数据"));

2.11 相似方法进行对比分析

可能小伙伴看到这,没用用过的话会觉得orElse()和orElseGet()还有orElseThrow()很相似,map()和flatMap()好相似
哈哈哈不用着急,都是从这一步过来的,我再给大家总结一下不同方法的异同点
orElse()和orElseGet()和orElseThrow()的异同点

方法效果类似,如果对象不为空,则返回对象,如果为空,则返回方法体中的对应参数,所以可以看出这三个方法体中参数是不一样的
orElse(T 对象)
orElseGet(Supplier < T >对象)
orElseThrow(异常)

map()和orElseGet的异同点

方法效果类似,对方法参数进行二次包装,并返回,入参不同
map(function函数)
flatmap(Optional< function >函数)

具体要怎么用,要根据业务场景以及代码规范来定义,下面可以简单看一下我在实战中怎用使用神奇的Optional

3.实战场景再现

场景1:
在service层中
查询一个对象,返回之后判断是否为空并做处理

1
2
3
4
复制代码        //查询一个对象
Member member = memberService.selectByIdNo(request.getCertificateNo());
//使用ofNullable加orElseThrow做判断和操作
Optional.ofNullable(member).orElseThrow(() -> new ServiceException("没有查询的相关数据"));

场景2:
我们可以在dao接口层中定义返回值时就加上Optional
例如:
我使用的是jpa,其他也同理

1
2
3
复制代码public interface LocationRepository extends JpaRepository<Location, String> {
Optional<Location> findLocationById(String id);
}

然在是Service中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码public TerminalVO findById(String id) {
//这个方法在dao层也是用了Optional包装了
Optional<Terminal> terminalOptional = terminalRepository.findById(id);
//直接使用isPresent()判断是否为空
if (terminalOptional.isPresent()) {
//使用get()方法获取对象值
Terminal terminal = terminalOptional.get();
//在实战中,我们已经免去了用set去赋值的繁琐,直接用BeanCopy去赋值
TerminalVO terminalVO = BeanCopyUtils.copyBean(terminal, TerminalVO.class);
//调用dao层方法返回包装后的对象
Optional<Location> location = locationRepository.findLocationById(terminal.getLocationId());
if (location.isPresent()) {
terminalVO.setFullName(location.get().getFullName());
}
return terminalVO;
}
//不要忘记抛出异常
throw new ServiceException("该终端不存在");
}

实战场景还有很多,包括return时可以判断是否返回当前值还是跳转到另一个方法体中,什么的还有很多,如果大家没有经验的小伙伴还想进行学习,可以评论一下我会回复大家

4.Optional使用注意事项

Optional真么好用,真的可以完全替代if判断吗?
我想这肯定是大家使用完之后Optional之后可能会产生的想法,答案是否定的
举一个最简单的栗子:
例子1:
如果我只想判断对象的某一个变量是否为空并且做出判断呢?

1
2
3
4
5
6
7
8
9
复制代码Person person=new Person();
person.setName("");
persion.setAge(2);
//普通判断
if(StringUtils.isNotBlank(person.getName())){
//名称不为空执行代码块
}
//使用Optional做判断
Optional.ofNullable(person).map(p -> p.getName()).orElse("name为空");

我觉得这个例子就能很好的说明这个问题,只是一个很简单判断,如果用了Optional我们还需要考虑包装值,考虑代码书写,考虑方法调用,虽然只有一行,但是可读性并不好,如果别的程序员去读,我觉得肯定没有if看的明显

5.jdk1.9对Optional优化

首先增加了三个方法:
or()、ifPresentOrElse() 和 stream()。
or() 与orElse等方法相似,如果对象不为空返回对象,如果为空则返回or()方法中预设的值。
ifPresentOrElse() 方法有两个参数:一个 Consumer 和一个 Runnable。如果对象不为空,会执行 Consumer 的动作,否则运行 Runnable。相比ifPresent()多了OrElse判断。
**stream()**将Optional转换成stream,如果有值就返回包含值的stream,如果没值,就返回空的stream。

因为这个jdk1.9的Optional具体我没有测试,同时也发现有蛮好的文章已经也能让大家明白jdk1.9的option的优化,我就不深入去说了。

作者的话

互相尊重,互相进步,很感谢大家的无私精神。才能让我们中国IT越来越进步
也非常愿意虚心听取更多大佬的意见和建议,和大家一起交流进步
我是你们的好朋友 樊亦凡

如果大家觉得还不错,希望可以给我一个赞,非常感谢!
非常想和大家交朋友,和大家一起进步!
一个每天进步一点点的程序员

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go面试复盘备忘录

发表于 2020-05-11

文档年久失修,内容有些地方不一定准确,且看且珍惜

一个小厂的面试,记录一下,答案不对的,请帮忙更正下

go部分

map底层实现

map底层通过哈希表实现

深入阅读map: Go专家编程-map

slice和array的区别

array是固定长度的数组,使用前必须确定数组长度

array特点:

  • go的数组是值类型,也就是说一个数组赋值给另一个数组,那么实际上就是真个数组拷贝了一份,需要申请额外的内存空间
  • 如果go中的数组做为函数的参数,那么实际传递的参数是一份数组的拷贝,而不是数组的指针
  • array的长度也是Type的一部分,这样就说明[10]int和[20]int是不一样的

slice特点:

  • slice是一个引用类型,是一个动态的指向数组切片的指针
  • slice是一个不定长的,总是指向底层的数组array的数据结构

区别:

  • 声明时:array需要声明长度或者…
  • 做为函数参数时:array传递的是数组的副本,slice传递的是指针

struct和OOP使用中有什么区别

请仔细看文章,是以OOP的角度来类比,不是说go有OOP

首先OOP的特点:继承、封装、多态

评论中很多人说组合之类的事情;

继承

概念:一个对象获得另一个对象的属性的过程

  • java只有单继承,接口多实现
  • go可以实现多继承
    • 一个struct嵌套了另一个匿名struct,那么这个struct可以直接访问匿名机构提的方法,从而实现集成
    • 一个struct嵌套了另一个命名的struct,那么这个模式叫做组合
    • 一个struct嵌套了多个匿名struct,那么这个结构可以直接访问多个匿名struct的方法,从而实现多重继承

封装

概念:自包含的黑盒子,有私有和公有的部分,公有可以被访问,私有的外部不能访问

  • java中访问权限控制通过public、protected、private、default关键字控制
  • go通过约定来实现权限控制。变量名首字母大写,相当于public,首字母小写,相当于private。在同一个包中访问,相当于default。由于在go中没有继承,所以就没有protected

多态

概念:允许用一个接口在访问同一类动作的特性

  • java中的多态是通过extends class或implements interface实现
  • go中的interface通过合约方式实现,只要某个struct实现了某个interface中的所有方法,那么它就隐式的实现了该接口

聊聊你对channel的理解

channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channel可发送数据的类型。

channel有哪些状态

channel有三种状态:

  1. nil,未初始化的状态,只进行了声明,或者手动赋值为nil
  2. active,正常的channel,可读可写
  3. closed,已关闭

channel可进行三种操作:

  1. 读
  2. 写
  3. 关闭

这三种操作和状态可以组合出九种情况:

操作 nil的channel 正常channel 已关闭channel
<-ch (读) 阻塞 成功或阻塞 读到零值
ch<- (写) 阻塞 成功或阻塞 panic
close(ch) (关闭) panic 成功 panic

在并发状态下map如何保证线程安全

go的map并发访问是不安全的,会出现未定义行为,导致程序退出。

go1.6之前,内置的map类型是部分goroutine安全的,并发的读没有问题,并发的写可能有问题。go1.6之后,并发的读写map会报错。

对比一下Java的ConcurrentHashMap的实现,在map的数据非常大的情况下,一把锁会导致大并发的客户端争抢一把锁,Java的解决方案是shard,内部使用多个锁,每个区间共享一把锁,这样减少了数据共享一把锁的性能影响

go1.9之前,一般情况下通过sync.RWMutex实现对map的并发访问控制,或者单独使用锁都可以。

go1.9之后,实现了sync.Map,类似于Java的ConcurrentHashMap。

sync.Map的实现有几个优化点:

  1. 空间换时间。通过冗余的两个数据结构(read,dirty),实现加锁对性能的影响
  2. 使用只读数据(read),避免读写冲突
  3. 动态调整,miss次数多了之后,将dirty数据提升为read
  4. double-checking
  5. 延迟删除。删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据
  6. 优先从read读取、更新、删除,因为对read的读取不需要锁

聊聊你对gc的理解

内存管理

go实现的内存管理简单的说就是维护一块大的全局内存,每个线程(go中为P)维护一块小的私有内存,私有内存不足再从全局申请。

  • go程序启动时申请一块大内存,并划分成spans、bitmap、arena区域
  • arean区域按页划分成一个个小块
  • span管理一个或多个页
  • mcentral管理多个span供线程申请使用
  • mcache作为线程私有资源,资源来源于mcentral

更多说明参阅引用说明1

垃圾回收

常见的垃圾回收算法:

  • 引用计数:对每个对象维护一个引用计数,当引用该对象的对象被销毁时,引用计数减一,当引用计数为0时回收该对象。
    • 优点:对象可以很快地被回收,不会出现内存耗尽或达到某个阈值时才回收。
    • 缺点:不能很好的处理循环引用,而且实时的维护引用计数,也有一定的代价。
    • 代表语言:Python、PHP、Swift
  • 标记-清除:从根变量遍历所有引用的对象,引用对象标记为”被引用“,没有被标记的进行回收。
    • 优点:解决了引用计数的缺点
    • 缺点:需要STW(Stop The World),就是停掉所有的goroutine,专心做垃圾回收,待垃圾回收结束后再恢复goroutine,这回导致程序短时间的暂停。
    • 代表语言:Go(三色标记法)
  • 分代收集:按照对象生命周期的长短划分不同的代空间,生命周期长的放入老年代,而短的放入新生代,不同代有不同的回收算法和回收频率。
    • 优点:回收性能好
    • 缺点:回收算法复杂
    • 代表语言:Java
Go垃圾回收的三色标记法

三色标记法只是为了描述方便抽象出来的一种说法,实际上对象并没有颜色之分。这里的三色对应了垃圾回收过程中对象的三种状态:

  • 灰色:对象还在标记队列中等待
  • 黑色:对象已被标记,gcmarkBits对应的位为1(对象不会在本次GC中被清理)
  • 白色:对象未被标记,gcmarkBits对应的位为0(对象将会在本次GC中被清理)

垃圾回收优化2

写屏障(Write Barrier)

前面说过STW目的是防止GC扫描时内存变化而停掉goroutine,而写屏障就是让goroutine与GC同时运行的手段。虽然写屏障不能完全消除STW,但是可以大大减少STW的时间。

写屏障类似一种开关,在GC的特定时机开启,开启后指针传递时会把指针标记,即本轮不回收,下次GC时再确定。

GC过程中新分配的内存会被立即标记,用的并不是写屏障技术,也即GC过程中分配的内存不会在本轮GC中回收。

辅助GC(Mutator Assist)

为了防止内存分配过快,在GC执行过程中,如果goroutine需要分配内存,那么这个goroutine会参与一部分GC的工作,即帮助GC做一部分的工作,这个机制叫做Mutator Assist。

垃圾回收触发时机3

内存分配量达到阈值出发GC

每次内存分配时都会检查当前内存分配量是否已达到阈值,如果达到阈值则立即启动GC。

1
复制代码阈值 = 上次GC内存分配量 * 内存增长率

内存增长率由环境变量GOGC控制,默认为100,即每当内存扩大一倍时启动GC。

定期触发GC

默认情况下,最长2分钟触发一次GC,这个间隔在src/runtime/proc.go:forcegcperiod变量中被声明:

1
2
3
4
5
6
go复制代码// forcegcperiod is the maximum time in nanoseconds between garbage
// collections. If we go this long without a garbage collection, one
// is forced to run.
//
// This is a variable for testing purposes. It normally doesn't change.
var forcegcperiod int64 = 2 * 60 * 1e9
手动触发

程序代码中也可以使用runtime.GC()来手动触发GC,这主要用于GC性能测试和统计。

GC性能优化

GC性能与对象数量负相关,对象越多GC性能越差,对程序影响越大。

所以GC性能优化的思路之一就是减少对象分配个数,比如对象复用或使用大对象组合多个小对象等等。

另外,由于内存逃逸现象,有些隐式的内存分配也会产生,也有可能成为GC的负担。

内存逃逸现象4:变量分配在栈上需要能在编译器确定它的作用域,否则就会被分配到堆上。而堆上动态分配内存比栈上静态分配内存,开销大很多。

go通过go build -gcflags=m命令来观察变量逃逸情况5

更多逃逸场景:逃逸场景

逃逸分析的作用:

  1. 逃逸分析的好处是为了减少GC的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要GC标记清除。
  2. 逃逸分析完后可以确定哪些变量可以分配在栈上,栈的分配比堆快,性能好(逃逸的局部变量会分配在堆上,而没有发生逃逸的则由编译器分配到栈上)
  3. 同步消除,如果你定义的对象在方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行

逃逸总结

  • 栈上分配内存比在堆中分配内存有更高的效率
  • 栈上分配的内存不需要GC处理
  • 堆上分配的内存使用完毕会交给GC处理
  • 逃逸分析的目的是决定内存分配到堆还是栈
  • 逃逸分析在编译阶段完成

go方法传参比起python、java有什么区别

参考文档:go的参数传递细节

go中的函数的参数传递采用的是值传递

gin

聊聊你对gin的理解

gin是一个go的微框架,封装优雅,API友好。快速灵活。容错方便等特点。

其实对于go而言,对web框架的依赖远比Python、Java之类的小。本身的net/http足够简单,而且性能也非常不错,大部分的框架都是对net/http的高阶封装。所以gin框架更像是一些常用函数或者工具的集合。使用gin框架发开发,可以提升效率,并同意团队的编码风格。

gin的路由组件为什么高性能

路由树

gin使用高性能路由库httprouter6

在Gin框架中,路由规则被分成了9课前缀树,每一个HTTP Method对应一颗前缀树,树的节点按照URL中的 / 符号进行层级划分

gin.RouterGroup

RouterGroup是对路由树的包装,所有的路由规则最终都是由它来进行管理。Engine结构体继承了RouterGroup,所以Engine直接具备了RouterGroup所有的路由管理功能。

gin数据绑定

gin提供了很方便的数据绑定功能,可以将用户传过来的参数自动跟我们定义的结构体绑定在一起。

这是也我选择用gin的重要原因。

gin数据验证

在上面数据绑定的基础上,gin还提供了数据校验的方法。gin的数据验证是和数据绑定结合在一起的。只需要在数据绑定的结构体成员变量的标签添加binding规则即可。这又省了大量的验证工作,对用惯AspCoreMVC、Spring MVC的程序员来说是完美的替代框架。

gin的中间件

gin中间件利用函数调用栈后进先出的特点,完成中间件在自定义处理函数完成后的处理操作。

redis

为什么redis高性能

  • 纯内存操作,内存的读写速度非常快。
  • 单线程7,省避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用去考虑各种锁的问题,不存在加锁释放锁的操作,因为没有出现死锁。
  • 高效的数据结构,Redis的数据结构是专门进行设计的
  • 使用多路I/O复用模型,非阻塞IO
  • 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM机制,因为一般的系统调用系统函数的话,会需要一定的时间去移动和请求;

为什么redis要采用单线程

官方答复:因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器的内存大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章的采用单线程方案了

多路I/O复用模型,非阻塞IO

Linux下的select、poll和epoll就是干这个的。目前最先进的就是epoll。将用户socket对应的fd注册进epoll,然后epoll帮你监听那些socket上有消息到达,这样就避免了大量的无用操作。此时的socket采用非阻塞的模式。这样,这个过程只在调用epoll的时候才会阻塞,收发客户端消息是不会阻塞的,整个进程或线程就被充分利用起来,这也就是事件驱动。

常用的5数据结构

  • String:缓存、计数器、分布式锁等
  • List:链表、队列、微博关注人时间轴列表等
  • Hash:用户信息、Hash表等
  • Set:去重、赞、共同好友等
  • Zset:访问量排行、点击量排行榜等

redis作为消息队列的可靠性如何保证

参照RabbitMQ的ACK机制,消费端提供消费回馈。

RabbitMQ

image-20200511112528827

RabbitMQ如何保证消息的可靠性

生产端

有两种方案:事务消息、消息确认

事务消息会严重损耗RabbitMQ的性能,所以基本不会使用。所以一般使用异步的消息确认方式保证发送的消息一定到达RabbitMQ

消费端

消息确认(ACK),当Customer使用autoAck=true的方式订阅RabbitMQ节点消息的时候,可能由于网络原因也可能由于Customer处理消息的时候出现异常,亦或是服务器宕机,都有可能丢失消息。

而当autoAck=true的时候,RabbitMQ会自动把发出去的消息设置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正的消费到了这些消息。

为了避免这种情况下丢失消息,RabbitMQ提供了消费端确认的方式处理消息,所以需要设置autoAck=false

MQ本身

以上都是应用级别保证消息的可靠性,虽然已经极大的提高了应用的安全性,但是当RabbitMQ节点重启、宕机等情况依旧会导致消息丢失,所以还需要设置队列的持久性。消息的持久性,保证节点宕机或者重启后能恢复消息。

如果出现单点问题,消息还是会丢失。所以可以对于关键的消息设置镜像队列和集群保证消息服务的高可用。

MongoDB

MongoDB是一个通用的、面向文档的分布式数据库

MongoDB索引的数据结构

MongoDB的默认引擎WiredTiger使用B树做为索引底层的数据结构,但是除了B树外,还支持LSM树做为可选的底层数据存储结构。

MongoDB索引是特殊的数据结构,索引存储在一个易于遍历读取的数据集合中,索引是对数据库表中一列或多列的值进行排序的一种结构。

MongoDB为什么默认选择B树而不是MySQL默认的B+树

首先是应用场景:

  • 做为非关系型数据库,MongoDB对于遍历数据的需求没有关系型数据库那么强,它追求的是读写单个记录的性能
  • 大多数的数据库面对的都是读多写少的场景,B树与LSM树在该场景下有更大的优势

MySQL中使用的B+树是因为B+树只有叶结点会存储数据,将树种的每一个叶结点通过指针连接起来就能实现顺序遍历,而遍历数据库在关系型数据库中非常常见

MongoDB和MySQL在多个不同数据结构之间选择的最终目的就是减少查询需要的随机IO次数,MySQL认为遍历数据的查询是非常常见的,所以它选择B+树作为底层数据结构。而MongoDB认为查询单个数据记录远比遍历数据更加常见,由于B树的非叶结点也可以存储数据,所以查询一条数据所需要的平均随机IO次数会比B+树少,使用B树的MongoDB在类似的场景中的查询速度就会比MySQL快。这里并不是说MongoDB并不能对数据进行遍历,我们在MongoDB中也可以使用范围查询来查询一批满足对应条件的记录,只是需要的时间会比MySQL长一些。

MongoDB作为非关系型的数据库,它从集合的设计上就使用了完全不同的方法,如果我们仍然使用传统的关系型数据库的表设计思路来思考MongoDB中集合的设计,写出的查询可能会带来相对比较差的性能。

MongoDB中推荐的设计方法,是使用嵌入文档8。

MongoDB的索引有哪些,区别是什么

MongoDB支持多种类型的索引,包括单字段索引、复合索引、多key索引、文本索引等,每种类型的索引有不同的使用场景。

  • **单字段索引:**能加速对指定字段的各种查询请求,是最常见的索引形式,MongoDB默认创建的id索引也是这种类型。
  • **复合索引:**是单字段索引的升级版,它针对多个字段联合创建索引,先按第一个字段排序,第一个字段相同的文档第二个字段排序,以此类推。
  • **多key索引:**当索引的字段为数组时,创建出的索引称为多key索引,多key索引会为数组的每个元素建立一条索引。
  • **哈希索引:**是指按照某个字段的hash值来建立索引,目前主要用于MongoDB Sharded Cluster的Hash分片,hash索引只能满足字段完全匹配的查询,不能满足范围查询等。
  • **地理位置索引:**能很好的解决O2O的应用场景,比如『查找附近的美食』、『查找某个区域内的车站』等。
  • **文本索引:**能解决快速文本查找的需求,比如有一个博客文章集合,需要根据博客的内容来快速查找,则可以针对博客内容建立文本索引。

Nginx

文档:www.aosabook.org/en/nginx.ht…

img

引用文章9

为什么Nginx高性能

Nginx运行过程

  1. 多进程:一个 Master 进程、多个 Worker 进程
  2. Master 进程:管理 Worker 进程
  3. 对外接口:接收外部的操作(信号)
  4. 对内转发:根据外部的操作的不同,通过信号管理 Worker
  5. 监控:监控 worker 进程的运行状态,worker 进程异常终止后,自动重启 worker 进程
  6. Worker 进程:所有 Worker 进程都是平等的
  7. 实际处理:网络请求,由 Worker 进程处理;
  8. Worker 进程数量:在 nginx.conf 中配置,一般设置为核心数,充分利用 CPU 资源,同时,避免进程数量过多,避免进程竞争 CPU 资源,增加上下文切换的损耗。

HTTP连接建立和请求处理过程

  • Nginx启动时,Master进程,加载配置文件
  • Master进程,初始化监听的socket
  • Master进程,fork出多个Worker进程
  • Worker进程,竞争新的连接,获胜方通过三次握手,建立Socket连接,并处理请求

TCP/UDP

TCP

Tcp三次握手

image-20200511141254856

图片来自于《图解HTTP》

image-20200511142015251

  • 客户端-发送带有SYN标志的数据包 - 一次握手-服务端
    • 第一次握手:Client什么都不能确认;Server确认了对方发送正常,自己接收正常
  • 服务端-发送带有SYN/ACK标志的数据包 - 二次握手-客户端
    • 第二次握手:Client确认了:自己发送、接收正常、对方发送、接收正常;Server确认了;对方发送、自己接收正常
  • 客户端-发送带有ACK标志的数据包 - 三次握手-服务端
    • 第三次握手:Client确认了:自己发送、接收正常,对方发送、接收正常;Server确认了:自己发送、接收正常,对方发送、接收正常

所以需要三次握手才能确认双方收发功能都正常。

Tcp四次挥手

image-20200511142037378

断开一个TCP连接则需要“四次挥手”:

  • 客户端-发送一个FIN,用来关闭客户端到服务器的数据传送
  • 服务器-收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号
  • 服务器-关闭与客户端的连接,发送一个FIN给客户端
  • 客户端-发回ACK报文确认,并将确认序号设置为收到序号加1

任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认或进入半关闭状态。

当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了TCP连接。

UDP

UDP在传送数据之前不需要先建立连接,远程主机在收到UDP报文后,不需要给出任何确认。虽然UDP不提供可靠交付,但在某些情况下UDP是一种最有效的工作方式(一般用于即时通信,比如:QQ语言、QQ视频、直播等等)

长连接/短连接10

TCP本身没有长短连接的区别,长短与否,取决于我们怎么用它。

  • **短连接:**每次通信时,创建Socket;一次通信结束,调用socket.close(),这就是一般意义上的短连接。
    • 短连接的好处是管理起来比较简单,存在的连接都是可用的连接,不需要额外的控制手段。
  • **长连接:**每次通信完毕后,不会关闭连接,这样可以做到连接的复用。
    • 长连接的好处是省去了创建连接的耗时,性能好。

Footnotes

  1. rainbowmango.gitbook.io/go/chapter0… ↩
  2. rainbowmango.gitbook.io/go/chapter0… ↩
  3. rainbowmango.gitbook.io/go/chapter0… ↩
  4. Go 内存逃逸详细分析 ↩
  5. Go内存逃逸分析 ↩
  6. httprouter ↩
  7. redis的单线程指的是网络请求模块使用了一个线程,即一个线程处理所有的网络请求,其他模块仍用了多个线程 ↩
  8. 为什么MongoDB使用B树 ↩
  9. segmentfault.com/a/119000002… ↩
  10. www.cnkirito.moe/tcp-talk/ ↩

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

协程 Flow 最佳实践 基于 Android 开发者峰

发表于 2020-05-11

本文介绍了我们在开发 2019 Android 开发者峰会 (ADS) 应用时总结整理的 Flow 最佳实践 (应用源码已开源),我们将和大家共同探讨应用中的每个层级将如何处理数据流。

ADS 应用的架构遵守 Android 官方的推荐架构指南,我们在其中引入了 Domain 层 (用以囊括各种 UseCases 类) 来帮助分离焦点,进而保持代码的精简、复用性、可测试性。

2019 ADS 应用的架构

更多关于应用架构指南的分层设计 (Data 层、Domain 层、UI 层),请参考示例应用 | Plaid 2.0 重构。
如同许多 Android 应用一样,ADS 应用从网络或缓存懒加载数据。我们发现,这种场景非常适合 Flow。挂起函数 (suspend functions) 更适合于一次性操作。为了使用协程,我们将重构分为两次 commit 提交: 第一次迁移了一次性操作,第二次将其迁移至数据流。

在本文中,您将看到我们把应用从 “在所有层级使用 LiveData“,重构为 “只在 View 和 ViewModel 间使用 LiveData 进行通讯,并在应用的底层和 UserCase 层架构中使用协程”。

优先使用 Flow 来暴露数据流 (而不是 Channel)

您有两种方法在协程中处理数据流: 一种是 Flow API,另一种是 Channel API。Channels 是一种同步原语,而 Flows 是为数据流模型所设计的: 它是订阅数据流的工厂。不过我们可以使用 Channels 来支持 Flows,这一点我们稍后再说。

相较于 Channel,Flow 更灵活,并提供了更明确的约束和更多操作符。

由于末端操作符 (terminal operator) 会触发数据流的执行,同时会根据生产者一侧流操作来决定是成功完成操作还是抛出异常,因此 Flows 会自动地关闭数据流,您基本不会在生产者一侧泄漏资源;而一旦 Channel 没有正确关闭,生产者可能不会清理大型资源,因此 Channels 更容易造成资源泄漏。

应用数据层负责提供数据,通常是从数据库中读取,或从网络获取数据,例如,示例是一个数据源接口,它提供了一个用户事件数据流:

1
2
3
复制代码interface UserEventDataSource {
fun getObservableUserEvent(userId: String): Flow<UserEventResult>
}

如何将 Flow 应用在您的 Android 应用架构中

1. UseCase 层和 Repository 层

介于 View/ViewModel 和数据源之间的层 (在我们的例子中是 UseCase 和 Repository) 通常需要合并来自多个查询的数据,或在 ViewModel 层使用之前转化数据。就像 Kotlin sequences 一样,Flow 支持大量操作符来转换数据。目前已经有大量的可用的操作符,同时您也可以创建您自己的转换器 (比如,使用 transform 操作符)。不过 Flow 在许多的操作符中暴露了 suspend lambda 表达式,因此在大多数情况下没有必要通过自定义转换来完成复杂任务,可以直接在 Flow 中调用挂起函数。

在 ADS 应用中,我们想将 UserEventResult 和 Repository 层中的会话数据进行绑定。我们利用 map 操作符来将一个 suspend lambda 表达式应用在从数据源接收到的每一个 Flow 的值上:

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
复制代码
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
class DefaultSessionAndUserEventRepository(
private val userEventDataSource: UserEventDataSource,
private val sessionRepository: SessionRepository
) : SessionAndUserEventRepository {

override fun getObservableUserEvent(
userId: String?,
eventId: SessionId
): Flow<Result<LoadUserSessionUseCaseResult>> {
// 处理 userId

// 监听用户事件,并将其与 Session 数据进行合并
return userEventDataSource.getObservableUserEvent(userId, eventId).map { userEventResult ->
val event = sessionRepository.getSession(eventId)

// 将 Session 和用户数据进行合并,并传递结果
val userSession = UserSession(
event,
userEventResult.userEvent ?: createDefaultUserEvent(event)
)
Result.Success(LoadUserSessionUseCaseResult(userSession))
}
}
}

2. ViewModel

在利用 LiveData 执行 UI ↔ ViewModel 通信时,ViewModel 层应该利用末端操作符来消费来自数据层的数据流 (比如: collect、first 或者是 toList) 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
// 真实代码的简化版
class SessionDetailViewModel(
private val loadUserSessionUseCase: LoadUserSessionUseCase,
...
): ViewModel() {

private fun listenForUserSessionChanges(sessionId: SessionId) {
viewModelScope.launch {
loadUserSessionUseCase(sessionId).collect { loadResult ->
}
}
}
}

完整代码可以参考这里:
github.com/google/iosc…

如果您需要将 Flow 转化为 LiveData,则可以使用 AndroidX lifecycle library 提供的 Flow.asLiveData() 扩展函数 (extension function)。这个扩展函数非常便于使用,因为它共享了 Flow 的底层订阅,同时根据观察者的生命周期管理订阅。此外,LiveData 可以为后续添加的观察者提供最新的数据,其订阅在配置发生变更的时候依旧能够生效。下面利用一段简单的代码来演示如何使用这个扩展函数:

1
2
3
4
5
6
7
复制代码
class SimplifiedSessionDetailViewModel(
private val loadUserSessionUseCase: LoadUserSessionUseCase,
...
): ViewModel() {
val sessions = loadUserSessionUseCase(sessionId).asLiveData()
}

特别说明: 这段代码不是 ADS 应用的,它只是用来演示如何使用 Flow.asLiveData()。

具体实现时,该在何时使用 BroadcastChannel 或者 Flow

回到数据源的实现,要怎样去实现之前暴露的 getObservableUserEvent 函数?我们考虑了两种实现: flow 构造器,或 BroadcastChannel 接口,这两种实现应用于不同的场景。

1. 什么时候使用 Flow ?

Flow 是一种 “冷流”(Cold Stream)。”冷流” 是一种数据源,该类数据源的生产者会在每个监听者开始消费事件的时候执行,从而在每个订阅上创建新的数据流。一旦消费者停止监听或者生产者的阻塞结束,数据流将会被自动关闭。

Flow 非常适合需要开始/停止数据的产生来匹配观察者的场景。

您可以利用 flow 构造器来发送有限个/无限个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码val oneElementFlow: Flow<Int> = flow {
// 生产者代码开始执行,流被打开
emit(1)
// 生产者代码结束,流将被关闭
}
val unlimitedElementFlow: Flow<Int> = flow {
// 生产者代码开始执行,流被打开
while(true) {
// 执行计算
emit(result)
delay(100)
}
// 生产者代码结束,流将被关闭
}

Flow 通过协程取消功能提供自动清理功能,因此倾向于执行一些重型任务。请注意,这里提到的取消是有条件的,一个永不挂起的 Flow 是永不会被取消的: 在我们的例子中,由于 delay 是一个挂起函数,用于检查取消状态,当订阅者停止监听时,Flow 将会停止并清理资源。

2. 什么时候使用 BroadcastChannel

Channel 是一个用于协程间通信的并发原语。BroadcastChannel 基于 Channel,并加入了多播功能。

可能在这样一些场景里,您可能会考虑在数据源层中使用 BroadcastChannel:

如果生产者和消费者的生命周期不同或者彼此完全独立运行时,请使用 BroadcastChannel。

如果您希望生产者有独立的生命周期,同时向任何存在的监听者发送当前数据的时候,BroadcastChannel API 非常适合这种场景。在这种情况下,当新的监听者开始消费事件时,生产者不需要每次都被执行。

您依然可以向调用者提供 Flow,它们不需要知道具体的实现。您可以使用 BroadcastChannel.asFlow() 这个扩展函数来将一个 BroadcastChannel 作为一个 Flow 使用。

不过,关闭这个特殊的 Flow 不会取消订阅。当使用 BroadcastChannel 的时候,您必须自己管理生命周期。BroadcastChannel 无法感知到当前是否还存在监听者,除非关闭或取消 BroadcastChannel,否则将会一直持有资源。请确保在不需要 BroadcastChannel 的时候将其关闭。同时请注意关闭后的 BroadcastChannel 无法再次被使用,如果需要,您需要重新创建实例。

接下来,我们将分享如何使用 BroadcastChannel API 的示例。

3. 特别说明

部分 Flow 和 Channel API 仍处于实验阶段,很可能会发生变动。在一些情况下,您可能会正在使用 Channel,不过在未来可能会建议您使用 Flow。具体来讲,StateFlow 和 Flow 的 share operator 方案可能在未来会减少 Channel 的使用。

将数据流中基于回调的 API 转化为协程

包含 Room 在内的很多库已经支持将协程用于数据流操作。对于那些还不支持的库,您可以将任何基于回调的 API 转换为协程。

1. Flow 的实现

如果您想将一个基于回调的流 API 转换为使用 Flow,您可以使用 channelFlow 函数 (当然也可以使用 callbackFlow,它们都基于相同的实现)。channelFlow 将会创建一个 Flow 的实例,该实例中的元素将传递给一个 Channel。这样可以允许我们在不同的上下文或并发中提供元素。

以下示例中,我们想要把从回调中拿到的元素发送到 Flow 中:

  1. 利用 channelFlow 构造器创建一个可以把回调注册到第三方库的流;
  2. 将从回调接收到的所有数据传递给 Flow;
  3. 当订阅者停止监听,我们利用挂起函数 “awaitClose” 来解除 API 的订阅。
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
复制代码/* Copyright 2019 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */
override fun getObservableUserEvent(userId: String, eventId: SessionId): Flow<UserEventResult> {
// 1) 利用 channelFlow 创建一个 Flow
return channelFlow<UserEventResult> {

val eventDocument = firestore.collection(USERS_COLLECTION)
.document(userId)
.collection(EVENTS_COLLECTION)
.document(eventId)

// 1) 将回调注册到 API 上
val subscription = eventDocument.addSnapshotListener { snapshot, _ ->
val userEvent = if (snapshot.exists()) {
parseUserEvent(snapshot)
} else { null }

// 2) 将数据发送到 Flow
channel.offer(UserEventResult(userEvent))
}
// 3) 请不要关闭数据流,在消费者关闭或者 API 调用 onCompleted/onError 函数之前,请保证数据流
// 一直处于打开状态。
// 当数据流关闭后,请取消第三方库的订阅。
awaitClose { subscription.remove() }
}
}

详细代码可以参考这里:

github.com/google/iosc…

2. BroadcastChannel 实现

对于使用 Firestore 跟踪用户身份认证的数据流,我们使用了 BroadcastChannel API,因为我们希望注册一个有独立生命周期的 Authentication 监听者,同时也希望能向所有正在监听的对象广播当前的结果。

转化回调 API 为 BroadcastChannel 相比转化为 Flow 要略复杂一点。您可以创建一个类,并设置将实例化后的 BroadcastChannel 作为变量保存。在初始化期间,注册回调,像以前一样将元素发送到 BroadcastChannel:

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
复制代码/* Copyright 2019 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */
class FirebaseAuthStateUserDataSource(...) : AuthStateUserDataSource {

private val channel = ConflatedBroadcastChannel<Result<AuthenticatedUserInfo>>()

private val listener: ((FirebaseAuth) -> Unit) = { auth ->
// 数据处理逻辑

// 将当前的用户 (数据) 发送给消费者
if (!channel.isClosedForSend) {
channel.offer(Success(FirebaseUserInfo(auth.currentUser)))
} else {
unregisterListener()
}
}

@Synchronized
override fun getBasicUserInfo(): Flow<Result<AuthenticatedUserInfo>> {
if (!isListening) {
firebase.addAuthStateListener(listener)
isListening = true
}
return channel.asFlow()
}
}
  • 详细代码可以参考这里
    github.com/google/iosc…

测试小建议

为了测试 Flow 转换 (就像我们在 UseCase 和 Repository 层中所做的那样),您可以利用 flow 构造器返回一个假数据,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
object FakeUserEventDataSource : UserEventDataSource {
override fun getObservableUserEvents(userId: String) = flow {
emit(UserEventsResult(userEvents))
}
}
class DefaultSessionAndUserEventRepositoryTest {
@Test
fun observableUserEvents_areMappedCorrectly() = runBlockingTest {
// 准备一个 repo
val userEvents = repository
.getObservableUserEvents("user", true).first()
// 对接收到的用户事件进行断言
}
}

为了成功完成测试,一个比较好的做法是使用 take 操作符来从 Flow 中获取一些数据,使用 toList 作为末端操作符来从数组中获取结果。示例如下:

1
2
3
4
5
6
7
8
9
复制代码
class AnotherStreamDataSourceImplTest {
@Test
fun `Test happy path`() = runBlockingTest {
//准备好 subject
val result = subject.flow.take(1).toList()
// 断言结果和预期的一致
}
}

take 操作符非常适合在获取到数据后关闭 Flow。在测试完毕后不关闭 Flow 或 BroadcastChannel 将会导致内存泄漏以及测试结果不一致。

注意: 如果在数据源的实现是通过 BroadcastChannel 完成的,那么上面的代码还不够。您需要自己管理数据源的生命周期,并确保 BroadcastChannel 在测试开始之前已经启动,同时需要在测试结束后将其关闭,否则将会导致内存泄漏。

  • 在这里获取更多信息
    github.com/manuelvicnt…

协程测试的最佳实践在这里依然适用。如果您在测试代码中创建新的协程,则可能想要在测试线程中执行它来确保测试获得执行。

您也可以通过视频回顾 2019 Android 开发者峰会演讲 —— 在 Android 上测试协程:

点击查看视频:v.qq.com/x/page/d303…

总结

  • 因为 Flow 所提供的更加明确的约束和各种操作符,我们更建议向消费者暴露 Flow 而不是 Channel;
  • 使用 Flow 时,生产者会在每次有新的监听者时被执行,同时数据流的生命周期将会被自动处理;
  • 使用 BroadcastChannel 时,您可以共享生产者,但需要自己管理它的生命周期;
  • 请考虑将基于回调的 API 转化为协程,以便在您的应用中更好、更惯用地集成 API;
  • 使用 take 和 toList 操作符可以简化 Flow 的相关代码测试。

2019 ADS 应用在 GitHub 开源,请访问下方链接在 GitHub 上查看更详细的代码实现: github.com/google/iosc…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试官:你能写个LRU缓存吗?

发表于 2020-05-11

0. 前情提要

面试官: 你能手写个LRU缓存吗?

你: LRU是什么东西?(一脸懵逼状)

面试官: LRU全称Least Recently Used(最近最少使用),用来淘汰不常用数据,保留热点数据。

你写了5分钟,然而只写了个get和put方法体,里面逻辑实在不知道咋写。

面试官: 今天的面试先到这吧,有其他面试我们会再联系你。

我信你个鬼,你个糟老头子坏滴很,还联系啥,凉凉了。

别担心,再有人问你LRU,就把这篇文章丢给他,保证当场发offer。

1. 实现思路

目的是把最不常用的数据淘汰掉,所以需要记录一下每个元素的访问次数。最简单的方法就是把所有元素按使用情况排序,最近使用的,移到末尾。缓存满了,就从头部删除。

2. 使用哪种数据结构实现?

常用的数据结构有数组、链表、栈、队列,考虑到要从两端操作元素,就不能使用栈和队列。

每次使用一个元素,都要把这个元素移到末尾,包含一次删除和一次添加操作,使用数组会有大量的拷贝操作,不适合。

又考虑到删除一个元素,要把这个元素的前一个节点指向下一个节点,使用双链接最合适。

链表不适合查询,因为每次都要遍历所有元素,可以和HashMap配合使用。

双链表 + HashMap

3. 代码实现

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
复制代码import java.util.HashMap;
import java.util.Map;

/**
* @author yideng
*/
public class LRUCache<K, V> {

/**
* 双链表的元素节点
*/
private class Entry<K, V> {
Entry<K, V> before;
Entry<K, V> after;
private K key;
private V value;
}

/**
* 缓存容量大小
*/
private Integer capacity;
/**
* 头结点
*/
private Entry<K, V> head;
/**
* 尾节点
*/
private Entry<K, V> tail;
/**
* 用来存储所有元素
*/
private Map<K, Entry<K, V>> caches = new HashMap<>();

public LRUCache(int capacity) {
this.capacity = capacity;
}

public V get(K key) {
final Entry<K, V> node = caches.get(key);
if (node != null) {
// 有访问,就移到链表末尾
afterNodeAccess(node);
return node.value;
}
return null;
}

/**
* 把该元素移到末尾
*/
private void afterNodeAccess(Entry<K, V> e) {
Entry<K, V> last = tail;
// 如果e不是尾节点,才需要移动
if (last != e) {
// 删除该该节点与前一个节点的联系,判断是不是头结点
if (e.before == null) {
head = e.after;
} else {
e.before.after = e.after;
}

// 删除该该节点与后一个节点的联系
if (e.after == null) {
last = e.before;
} else {
e.after.before = e.before;
}

// 把该节点添加尾节点,判断尾节点是否为空
if (last == null) {
head = e;
} else {
e.before = last;
last.after = e;
}
e.after = null;
tail = e;
}
}

public V put(K key, V value) {
Entry<K, V> entry = caches.get(key);
if (entry == null) {
entry = new Entry<>();
entry.key = key;
entry.value = value;
// 新节点添加到末尾
linkNodeLast(entry);
caches.put(key, entry);
// 节点数大于容量,就删除头节点
if (this.caches.size() > this.capacity) {
this.caches.remove(head.key);
afterNodeRemoval(head);
}
return null;
}
entry.value = value;
// 节点有更新就移动到未节点
afterNodeAccess(entry);
caches.put(key, entry);
return entry.value;
}

/**
* 把该节点添加到尾节点
*/
private void linkNodeLast(Entry<K, V> e) {
final Entry<K, V> last = this.tail;
if (head == null) {
head = e;
} else {
e.before = last;
last.after = e;
}
tail = e;
}

/**
* 删除该节点
*/
void afterNodeRemoval(Entry<K, V> e) {
if (e.before == null) {
head = e.after;
} else {
e.before.after = e.after;
}

if (e.after == null) {
tail = e.before;
} else {
e.after.before = e.before;
}
}

}

4. 其实还有更简单的实现

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
复制代码import java.util.LinkedHashMap;
import java.util.Map;

/**
* @author yideng
*/
public class LRUCache<K, V> extends LinkedHashMap<K, V> {

// 最大容量
private final int maximumSize;

public LRUCache(final int maximumSize) {
// true代表按访问顺序排序,false代表按插入顺序
super(maximumSize, 0.75f, true);
this.maximumSize = maximumSize;
}

/**
* 当节点数大于最大容量时,就删除最旧的元素
*/
@Override
protected boolean removeEldestEntry(final Map.Entry eldest) {
return size() > this.maximumSize;
}
}

**为啥继承了LinkedHashMap,重写了两个方法,就实现了LRU?

下篇带你手撕LinkedHashMap源码,到时你会发现LinkedHashMap的源码和上面一灯写的LRU逻辑竟然有惊人的相似。**

)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…814815816…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%