Kotlin协程Coroutines入门到实战:(一)理解异步回调的本质

Kotlin协程入门到实战全部三篇文章:

  1. Kotlin协程Coroutines入门到实战:(一)理解异步回调的本质
  2. Kotlin协程Coroutines入门到实战:(二)Coroutines初体验
  3. Kotlin协程Coroutines入门到实战:(三)Coroutines+Retrofit+ViewModel+LiveData实现MVVM客户端架构

学习了Kotlin协程之后感觉协程是个可以化腐朽为神奇的东西,但是如果习惯了之前的编程方式,刚接触Kotlin协程的话理解起来还是比较吃力的。这里我总结了自己对于Kotlin协程的理解和学习经验,希望对大家的学习有所帮助。

整个系列分为三部分,希望大家可以耐心看完。

1.什么是异步

我记得小学二年级碰到过一个让我受益终身的数学题:烧开水需要15分钟,洗碗需要5分钟,扫地需要5分钟,请问做完这三件事,总共需要几分钟?从此我做什么事,都事先想想先后顺序,看看可不可以一井去做。
长大后才知道这就是异步的用法,它其实已经渗透到你的生活中。

上面这段话节选自:余叶《代码里的世界观——通往架构师之路》,这段话中揭示了异步的本质。异步意昧着同时进行一个以上彼此目的不同的任务

如果上面三个任务一个一个按部就班地去做的话,你可能总共需要25分钟。但是烧开水的时候并不需要你在一旁一直等待着,如果你利用烧开水的时间去完成洗碗和扫地的任务,你只需要15分钟就可以完成以上三个任务。
Kotlin协程Coroutines入门到实战:(一)理解异步回调的本质_第1张图片
接着试着对着Android(或者其他任何UI开发框架)的线程模型类比一下:你是主线程。烧开水是个耗时操作(比如网络请求),洗碗和扫地是与视图相关的非耗时操作。洗碗和扫地必须由你本人亲自完成(视图相关的工作只能交给主线程),烧开水可以交给电磁炉完成,你只需要按下电磁炉的开关(可以类比成网络请求的发起)。没有使用异步也就意味着你在烧开水的时候一直在旁边等待着,无法完成其他工作,这也就意味着Android在等待网络请求的时候主线程阻塞,视图卡顿无法交互,这在Android中当然是不允许的。所以必须使用异步的方式,你(主线程)在按下电磁炉开关(发起网络请求)之后就继续完成洗碗扫地(视图交互)等其他任务了。

所以异步很好理解,就是同时进行着不同的任务就叫做异步。

2.为什么需要回调

当你按下电磁炉的按钮,并地利用这段时间完成了扫地的任务,你感到很有成就感。心里想:异步机制真好,原来25分钟的工作如今只需要15分钟就能完成,以后我任何的工作都要异步地完成。不过你并没有高兴太久,在洗碗时你遇到了麻烦。碗和盘子上沾满了油污,单靠自来水和洗洁精根本搞不定,这时你想到了别人教你的方法,常温的水洗不掉的油污用热水就可以轻松洗掉。但是你发现暖水瓶是空的,而放在电磁炉上的水刚烧了5分钟,还不够热,也就是说你必须等待水烧开了才能开始洗碗。

这时你不禁陷入了思考:异步机制真的是万能的吗?对于有前后依赖关系的任务,异步该如何处理呢?这段等待烧水的时间我可以去做其他工作吗?我怎么确定水什么时候才能烧开呢?这时,你眼前一亮:你发现了买水壶时赠送的一个配件,那是一个汽笛,它可以在水烧开的时候发出鸣叫声。听到了汽笛声你就可以知道水烧开了,接着就可以用刚烧开的热水来刷碗,并且烧水的过程中你仍然可以去完成其他工作(比如看技术博客),而不用担心烧水的问题。这个汽笛就可以看成异步中的回调机制。
Kotlin协程Coroutines入门到实战:(一)理解异步回调的本质_第2张图片
同样地我们来类比一下Android开发中的场景:洗碗(渲染某个视图)依赖于烧开水(网络请求)这个耗时操作的结果才能进行,所以你(主线程)在按下电磁炉开关(发起网络请求)的时候,为水壶装上了汽笛(为网络请求配置了回调),以便在水烧开(网络请求完成)的时候,汽笛发出鸣叫(回调函数被调用)你(主线程)就可以继续用烧开的水(网络请求的结果)洗碗(渲染某个视图)了,而等待水烧开(等待网络请求结果)的时候还可以去看技术博客(视图渲染与交互)。这在Android开发过程中几乎是基础得不能再基础的应用场景了,可以说几乎所有的Android应用程序都有这样的一个过程。

所以理解为什么需要回调也很简单:因为不同的任务之间存在前后的依赖关系。

3.回调的缺点

以上的应用场景相对简单,回调处理起来也游刃有余,可以描述为以下代码:

//烧2000mL热水来洗碗
boilWater(2000) { water ->
     washDishes(water)           
}

但函数回调也有缺陷,就是代码结构过分耦合,遇到多重函数回调的嵌套,代码难以维护。

比如客户端顺序进行多次网络异步请求:

//客户端顺序进行三次网络异步请求,并用最终结果更新UI
request1(parameter) { value1 ->
	request2(value1) { value2 ->
		request3(value2) { value3 ->
			updateUI(value3)            
		} 
	}              
}

这种结构的代码无论是阅读起来还是维护起来都是极其糟糕的。对多个回调组成的嵌套耦合,我亲切地称为“回调地狱(Callback Hell)”。

解决回调地狱的方案有很多,其中比较常见的有:链式调用结构。例如:

request1(parameter)
	.map { value1 ->
     	request2(value1)
   	}.map { value2 ->
    	request3(value2)
   	}.subscribe { value3 ->
    	updateUI(value3)
   	}

上面的代码看起来就舒服多了,这就是链式调用结构的魅力。实现链式调用结构的常见方式就是使用RxJava,RxJava是一个强大的工具,它是反应函数式编程在Java中的实现,我们可以通过RxJava中的“流”来构建链式调用结构。虽然RxJava足够强大,但是它也足够复杂,RxJava中“流”的创建、转化与消费都需要使用到它提供的各种类和丰富的操作符,所以要想对RxJava运用自如就需要对这些类和操作符非常熟悉,这也加大了RxJava的学习成本了。

我们可以链式调用结构中获得一些启发,虽然回调嵌套和链式调用在代码结构上完全不一样,但是其表达的东西确完全一致。也就是说回调嵌套和链式调用者两种结构表达的都是同一种逻辑,这不禁让我们想对于回调的本质做一些深入思考,究竟回调的背后是什么东西呢?

4.深入理解异步回调(重点)

在接触多线程编程之前,我们天真地认为代码就是从上到下一行一行地执行的。代码中的基本结构只有三种:顺序、分支、循环,顺序结构中写在上面的代码就是比写在下面的代码先执行,写在下面的代码就是要等到上面的代码执行完了才能得到执行。但接触到了多线程和并发之后,我们之前在脑袋里建立的秩序的世界完全崩塌了,取而代之的是一个混沌的世界。代码的执行顺序好像完全失控了,可能有多处的代码一起执行,写在下面的代码也可能先于上面的执行。

举个简单的例子:

//1.简单秩序的串行世界:
print("Hello ")
print("World!")
//结果为:Hello World!

//2.复杂混沌的并行世界:
Thread { 
    Thread.sleep(2000)
    print("Hello ") 
}.start()
print("World!")
//结果为:World!Hello 

那么我们思考一下在串行的世界里,由回调组织起来的代码结构属于顺序、分支、循环哪种呢?应该不难发现:烧完水再洗碗,网络请求成功再更新UI,这些看似复杂的回调结构其实表达的就是一种代码的顺序执行的方式。

回过头来看看之前提到的回调嵌套的例子,如果放在简单的串行世界里代码其实完全可以写成这样:

val value1 = request1(parameter)
val value2 = request2(value1)
val value3 = request2(value2)
updateUI(value3)

上面代码的执行顺序与下面的回调方式组织代码的执行顺序完全相同:

request1(parameter) { value1 ->
	request2(value1) { value2 ->
		request3(value2) { value3 ->
			updateUI(value3)            
		} 
	}              
}

既然代码执行顺序完全一致为什么我们还要使用回调这么麻烦的方式来顺序执行代码呢?原因就在于我们的世界不是简单的串行世界,实际的程序也不是只有一个线程那么简单的。顺序代码结构是阻塞式的,每一行代码的执行都会使线程阻塞在那里,但是主线程的阻塞会导致很严重的问题,所以也就决定了所有的耗时操作不能在主线程中执行,所以就需要多线程来执行。

对于上面的例子,虽然代码执行顺序是:request1 -> request2 -> request3 -> updateUI
Kotlin协程Coroutines入门到实战:(一)理解异步回调的本质_第3张图片
但是他们是有可能工作在不同的线程上的,比如:request1(work thread) -> request2(work thread) -> request3(work thread) -> updateUI(main thread)。也就是说虽然代码确实是顺序执行的,但其实是在不同的线程上顺序执行的。
Kotlin协程Coroutines入门到实战:(一)理解异步回调的本质_第4张图片
通常线程切换的工作是由异步函数内部完成的,通过回调的方式异步调用外界注入的代码。也就是说:异步回调其实就是代码的多线程顺序执行。

那么能不能既按照顺序的方式编写代码,又可以让代码在不同的线程顺序执行呢?有没有一个东西可以帮助我自动地完成线程的切换工作呢?答案当然是肯定的,接下来就轮到Kotlin协程大显身手的时候了。

你可能感兴趣的:(Kotlin协程Coroutines入门到实战:(一)理解异步回调的本质)