Android 上的协程(第二部分):入门

Posted Calvin880828

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 上的协程(第二部分):入门相关的知识,希望对你有一定的参考价值。

本系列文章

Android 上的协程(第一部分):了解背景
Android 上的协程(第二部分):入门
Android上的协程 (第三部分): 实际应用

android 上的协程(第二部分):入门

背景回顾

在第一部分中,我们探讨了协程擅长解决的问题。回顾一下,协程是解决两个常见编程问题的好方法:

长时间运行的任务是花费太长时间阻塞主线程的任务。
Main-safety允许您确保可以从主线程调用任何挂起函数。
为了解决这些问题,协程通过添加suspendresume建立在常规函数之上。当一个特定线程上的所有协程都被挂起时,该线程就可以自由地做其他工作。

但是,协程本身并不能帮助您跟踪正在完成的工作。拥有大量协程(数百甚至数千)并同时暂停所有协程是完全没问题的。而且,虽然协程很轻量级,但它们执行的工作通常很重要,例如读取文件或发出网络请求。

使用代码手动跟踪一千个协程是相当困难的。您可以尝试跟踪所有这些并手动确保它们完成或取消,但这样的代码很乏味且容易出错。如果代码不完美,它就会失去协程的踪迹,这就是我所说的工作泄漏。

任务泄漏就像内存泄漏,但更糟。这是一个丢失的协程。除了使用内存之外,任务泄漏还可以自行恢复以使用 CPU、磁盘,甚至发起网络请求。

泄漏的协程会浪费内存、CPU、磁盘,甚至会启动不需要的网络请求。

为了帮助避免协程泄漏,Kotlin 引入了结构化并发。结构化并发是语言特性和最佳实践的结合,遵循这些实践可以帮助您跟踪协程中运行的所有工作。

在 Android 上,我们可以使用结构化并发来做三件事:

  1. 不再需要时取消任务。
  2. 在运行时跟踪任务。
  3. 协程失败时发出异常信号。

让我们深入研究其中的每一个,看看结构化并发如何帮助我们确保我们永远不会失去对协程和泄漏任务的跟踪。

取消作用域内的任务

在 Kotlin 中,协程必须运行在一个称为CoroutineScope的组件中。一个CoroutineScope跟踪您的协程,甚至是挂起的协程。与我们在第一部分中讨论的不同Dispatchers,它实际上并不执行您的协程 — 它只是确保您不会忘记它们。

为确保跟踪所有协程,Kotlin 不允许您在没有CoroutineScope的地方运行协程. 您可以将一个CoroutineScope视为具有超能力的轻量级版本ExecutorService。它使您能够启动新的协程,这些协程具有我们在第一部分中探讨的所有suspendresume优点。

一个CoroutineScope跟踪所有协程,它可以取消其中启动的所有协程。这非常适合 Android 开发,您希望确保在用户离开时清除屏幕启动的所有内容。

CoroutineScope会跟踪所有协程,并且它可以取消其中启动的所有协程。

启动新协程

重要的是要注意你不能从任何地方调用一个suspend函数。挂起和恢复机制要求您必须使用协程。

有两种启动协程的方式launchasync,它们有不同的用途:

  1. launch 将启动一个“即发即弃”的新协程——这意味着它不会将结果返回给调用者。
  2. async 将启动一个新的协程,它允许您返回一个带有名为await的结果。

协程通常由launch发起,常规函数无法调用挂起函数,launch无结果返回。我们稍后会讨论什么时候使用async

在协程左右域上使用launch来启动协程

scope.launch 
    // This block starts a new coroutine 
    // "in" the scope.
    // 
    // It can call suspend functions
   fetchDocs()

您可以将launch视为将您的代码从常规函数带入协程世界的桥梁。在launch主体内部,您可以调用挂起函数并创建主线程安全的代码,就像我们在上一篇文章中介绍的那样。

launch 是从常规函数到协程的桥梁。
警告:launchasync之间有很大的区别是他们如何处理异常。async希望你最终会调用await获取结果(或异常),因此默认情况下不会抛出异常。这意味着如果你使用async启动一个新的协程,它会默默地丢弃异常。

由于launchasync仅在 CoroutineScope上可用,您知道您创建的任何协程将始终由作用域跟踪。Kotlin 只是不允许您创建未跟踪的协程,这对避免任务泄漏大有帮助。

从 ViewModel 开始

因此,如果 一个CoroutineScope跟踪其中启动的所有协程,并launch创建一个新的协程,那么您究竟应该在哪里调用launch和放置您的作用域呢?而且,什么时候取消在一个作用域内启动的所有协程才有意义?

CoroutineScope在 Android 上,与用户交互相关联通常很有意义。这使您可以避免泄漏协程或为用户做额外的工作Activities或Fragments不再与用户相关。当用户离开Screen时,CoroutineScope与Screen相关联的工作都将取消。

结构化并发保证当一个范围 取消时,它的所有协程都 取消。

将协程与 Android 架构组件集成时,您通常希望launch在ViewModel上,因为这是工作开始的地方——你不必担心屏幕旋转会取消你所有的协程。我们可以使用lifecycle-viewmodel-ktx来在viewModel上启动协程。

看下面这个例子:

class MyViewModel(): ViewModel() 
    fun userNeedsDocs() 
        // Start a new coroutine in a ViewModel
        viewModelScope.launch 
            fetchDocs()
        
    

viewModelScope将在viewModel销毁时,取消其内的所有协程,在onCleared()回调内完成。CoroutineScope会自动传播,因此,如果您启动的一个协程后又继续启动了另一个协程,那么它们最终都会在同一作用域viewModelScope内。

警告:当协程挂起时,通过抛出 aCancellationException协程取消的异常。捕获顶级异常的异常处理程序Throwable将捕获此异常。如果您在异常处理程序中使用异常,或者从不挂起,协程将停留在半取消状态。

所以,当你需要在ViewModel中运行一个协程时,只需使用viewModelScope从常规函数切换到协程。然后,因为viewModelScope会自动为你取消协程,所以在这里写一个无限循环而不会造成泄漏是完全没问题的。

fun runForever() 
    // start a new coroutine in the ViewModel
    viewModelScope.launch 
        // cancelled when the ViewModel is cleared
        while(true) 
            delay(1_000)
            // do something every second
        
    

通过使用viewModelScope您可以确保在不再需要时取消所有任务,甚至是这个无限循环。

跟踪任务

启动一个协程是件好事——对于很多代码来说,这就是您真正需要做的所有事情。启动协程,发出网络请求,并将结果写入数据库。

但是,有时您需要更复杂一些。假设您想在协程中同时执行两个网络请求——为此您需要启动更多协程!

要创建更多协程,任何挂起函数都可以通过使用另一个名为coroutineScope或其supervisorScope的构建器来启动更多协程。老实说,这个 API 有点令人困惑。coroutineScopeCoroutineScope是2个不同的东西,尽管他们的名字只有一个字符不同,coroutineScope是一个协程构建器,而CoroutineScope是协程作用域类。

在任何地方启动新协程是造成潜在任务泄漏的一种方式。调用者可能不知道新协程,如果不知道,它如何跟踪工作?

为了解决这个问题,结构化并发可以帮助我们解决这个问题。也就是说,它提供了一个保证,即当suspend函数返回时,它的所有工作都已完成。

结构化并发保证当挂起函数返回时,它的所有工作都已完成。

coroutineScope下面是一个用于获取两个文档的示例:

suspend fun fetchTwoDocs() 
    coroutineScope 
        launch  fetchDoc(1) 
        async  fetchDoc(2) 
    

在此示例中,同时从网络中获取了两个文档。第一个是在一个以launch“即发即弃”开始的协程中获取的——这意味着它不会将结果返回给调用者。

第二个文档是用 获取的async,所以文档可以返回给调用者。这个例子有点奇怪,因为通常你会同时使用async同时获取这两个文档——但我想表明你可以根据需要混合launch搭配。

coroutineScopesupervisorScope 让您可以从挂起函数中安全地启动协程。

需要注意的是,这段代码从未显式等待任何新协程!看起来像是fetchTwoDocs将在协程运行时返回!

为了实现结构化并发并避免任务泄漏,我们要确保当fetchTwoDocs返回时,它的所有工作都已完成。这意味着它启动的两个协程都必须在fetchTwoDocs返回之前完成。

使用coroutineScope构建器能确保任务不会从fetchTwoDocs泄漏出来。coroutineScope构建器将暂停自身,直到其内部启动的所有协程都完成。因此,在coroutineScope构建器中启动的所有协程完成之前,无法从fetchTwoDocs返回。

更多的工作

现在我们已经探索了跟踪一个和两个协程,是时候全力以赴并尝试跟踪一千个协程了!

看看下面的动画:

这个例子展示了同时进行一千个网络请求。这在实际的Android代码中是不推荐的——您的应用将会使用大量资源。

在这段代码中,我们使用coroutineScope构建器在其中启动了一千个协程。您可以看到这些东西是如何连接起来的。由于我们在一个挂起函数中,某个地方的代码必须使用CoroutineScope来创建协程。我们不知道这个CoroutineScope的任何信息,它可能是一个viewModelScope或定义在其他地方的其他CoroutineScope。不管是哪个调用范围,coroutineScope构建器都会将其用作新范围的父范围。

然后,在coroutineScope块内,launch将在新范围内启动协程。当由launch启动的协程完成时,新范围将跟踪它们。最后,一旦在coroutineScope内部启动的所有协程都完成,loadLots就可以自由返回了。

注意:作用域和协程之间的父子关系是使用Job对象创建的。但是,您通常可以在不深入到那个级别的情况下考虑协程和作用域之间的关系。
coroutineScopesupervisorScope 将等待子协程完成。

在这里,底层有很多事情要处理,但重要的是,使用coroutineScopesupervisorScope,您可以从任何挂起函数安全地启动协程。即使它启动一个新协程,您也不会意外地泄漏工作,因为您总是会挂起调用者,直到新协程完成。

非常酷的是,coroutineScope会创建一个子作用域。因此,如果父作用域被取消,它将向所有新协程传递取消。如果调用者是viewModelScope,当用户从屏幕导航离开时,所有一千个协程将自动取消。非常棒!

在我们继续讨论错误之前,值得花一点时间来谈谈supervisorScopecoroutineScope之间的区别。主要区别在于,coroutineScope将在其任何子协程失败时取消。因此,如果一个网络请求失败了,所有其他请求将立即被取消。如果您希望即使有一个请求失败,也要继续处理其他请求,则可以使用supervisorScopesupervisorScope不会在一个子协程失败时取消其他子协程。

协程失败时抛出异常

在协程中,错误通过抛出异常来处理,就像常规函数一样。函数的异常suspend将由 resume 重新抛出给调用者。就像常规函数一样,您不限于try/catch来处理错误,如果您愿意,您可以构建抽象以使用其他样式执行错误处理。

但是,在某些情况下,协程中可能会丢失异常。

val unrelatedScope = MainScope()
// example of a lost error
suspend fun lostError() 
    // async without structured concurrency
    unrelatedScope.async 
        throw InAsyncNoOneCanHearYou("except")
    

请注意,此代码声明了一个无关的协程作用域,将启动一个没有结构化并发的新协程。请记住,我在开始时说过,结构化并发是类型和编程实践的组合,而在挂起函数中引入无关的协程范围不遵循结构化并发的编程实践。

在这个代码中,错误被忽略了,因为async假定您最终会调用await,它会重新抛出异常。但是,如果您从未调用await,异常将永远存储下来,耐心地等待被引发。

结构化并发保证当协程出错时,它的调用者或作用域会得到通知。

如果您确实对上述代码使用结构化并发,错误将正确地抛给调用者。

suspend fun foundError() 
    coroutineScope 
        async  
            throw StructuredConcurrencyWill("throw")
        
    


由于coroutineScope将等待所有子协程完成,因此它也可以在它们失败时得到通知。如果coroutineScope启动的协程抛出异常,coroutineScope可以将其抛给调用者。由于我们使用的是coroutineScope而不是supervisorScope,在抛出异常时它还会立即取消所有其他子协程。

使用结构化并发

在本文中,我介绍了结构化并发,展示了它如何使我们的代码与Android ViewModel完美匹配,避免工作泄漏。

我还谈到了如何使挂起函数更容易理解。通过确保它们在返回之前完成工作,以及确保它们通过抛出异常来表明错误。

如果我们使用非结构化并发,协程很容易意外泄漏调用者不知道的工作。这些工作是不可取消的,也不能保证异常会被重新抛出。这会使我们的代码更加出人意料,并可能创建难以理解的错误。

你可以通过引入一个新的无关的CoroutineScope(注意大写的C),或者使用一个名为GlobalScope的全局作用域来创建非结构化并发,但是只有在需要协程比调用作用域更长寿的罕见情况下才应该考虑非结构化并发。在这种情况下,最好添加结构来确保跟踪非结构化协程,处理错误并具有良好的取消策略。

如果您有使用非结构化并发的经验,那么结构化并发确实需要一些时间来适应。这种结构和保证使与挂起函数交互更安全、更容易。因此,尽可能使用结构化并发是一个好主意,因为它有助于使代码更易于阅读,更少出现令人惊讶的情况。

在本文开头,我列出了三个结构化并发解决的问题。

结构化并发能够给我们带来以下保证,以实现

  • 取消不再需要的工作
  • 在运行时跟踪工作
  • 在协程失败时发出错误信号

以下是结构化并发的保证:

  • 当一个作用域取消时,它的所有协程都会取消。
  • 当一个挂起函数返回时,它的所有工作都已完成。
  • 当一个协程出错时,它的调用方或作用域会被通知。
    总的来说,结构化并发的保证使我们的代码更安全、更容易理解,让我们能够避免泄漏工作!

下一步是什么?

在这篇文章中,我们探讨了如何在 ViewModel 中启动 Android 上的协程,以及如何使用结构化并发来使我们的代码容易让你接受。

在下一篇文章中,我们将更多地讨论如何在实际情况中使用协程!

参考链接

https://medium.com/androiddevelopers/coroutines-on-android-part-ii-getting-started-3bff117176dd

iOS开发 ReactiveCocoa入门教程 第二部分

ReactiveCocoa 是一个框架,它允许你在你的iOS程序中使用函数响应式(FRP)技术。加上第一部分的讲解,你将会学会如何使用信号量(对事件发出数据流)如何替代标准的动作和事件处理逻辑。你也会学到如何转换、分离和组合这些信号量。

在这里,也就是第二部分里,你将会学到更多先进的ReactiveCocoa特性,包括:

1、另外两个事件类型:error和completed

2、Throttling(节流)

3、Threading

4、Continuations

5、更多。。。

是时候开始了。

Twitter Instant

这里我们要使用的贯穿整个教程的程序是叫做Twitter Instant的程序,该程序可以在你输入的时候实时更新搜索到的结果。

该应用包括一些基本的用户交互界面和一些平凡的代码,了解之后就可以开始了。在第一部分里面,你使用Cocoapods来把CocoaPods加载到你的工程里面,这里的工程里面就已经包含了Podfile文件,你只需要pod install一下即可。

然后重新打开工程即可。(这个时候打开TwitterInstant.xcworkspace):

1、TwitterInstant:这是你的程序逻辑

2、Pods:里面是包括的三方类库

运行一下程序,你会看到如下的页面:

技术分享

花费一会时间让你自己熟悉一下整个工程。它就是一个简单的split viewController app.左边的是RWSearchFormViewController,右边的是:RWSearchResultsViewController。

自己说:原文简单介绍了一下该工程,就不在介绍看一下就可以了。

验证搜索文本

你第一件要做的事情就是去验证一下搜索文本,让它确保大于两个字符串。如果你看了第一篇文章,这个将会很简单。

在RWSearchFormViewController.m中添加方法如下:

1
2
3
- (BOOL)isValidSearchText:(NSString *)text {
  return text.length > 2;
}

这就简单的保证了搜索的字符串大于两个字符。写这个很简单的逻辑你可能会问:为什么要分开该方法到工程文件里面呢?

当前的逻辑很简单,但是如果后面这个会更复杂呢?在上面的例子中,你只需要修改一个地方。此外,上面的写法让你的代码更有表现力,它告诉你为什么要检查string的长度。我们应该遵守好的编码习惯,不是么?

然后,我们导入头文件:

1
#import

然后在导入该头文件的文件里面的viewDidLoad后面写上如下代码:

1
2
3
4
5
[[self.searchText.rac_textSignal
  map:^id(NSString *text) {
    return [self isValidSearchText:text] ?      [UIColor whiteColor] : [UIColor yellowColor];  }]
  subscribeNext:^(UIColor *color) {
    self.searchText.backgroundColor = color;  }];

想想这是做什么呢?上面的代码:

1、取走搜索文本框的信号量

2、把它转换一下:用背景色来预示内容是否可用。

3、然后设置backgroundColor属性在subscribeNext:的block里面。

Build然后运行我们就会发现当搜索有效的时候就会是白色,搜索字符串无效的时候就是黄色。

下面是图解,这个简单的反应传输看起来如下:

技术分享

ran_textSignal发出包含当前文本框每次改变内容的next事件。map那个步骤转换text value,将其转换成了color,subscribeNext那一步将这个value提供给了textField的background。

当然了,你从第一个教程一定记得这些,对吧?如果你不记得的话,你也许想在这里停止阅读,至少读了整个测试工程。

在添加Twitter 搜索逻辑之前 ,这里有一些更有趣的话题。

Formatting of Pipelines

当你正在钻研格式化的ReactiveCocoa代码的时候,普遍接受的惯例就是:每一个操作在一个新行,和所有步骤垂直对齐的。

在下面的图片,你会看到更复杂的对齐方式,从上一个教程拿来的图片:

技术分享

这样你会更容易看到该组成管道的操作。另外,在每个block中用最少的代码任何超过几行的都应该拆分出一个私有的方法。

不幸的是,Xcode真的不喜欢这种格式类型的代码,因此你可能需要找到自己调整。

Memory Management

思考一下你刚才加入到TwitterInstant的代码。你是否想过你刚才创建的管道式如何保留的呢?无疑地,是否是它没有赋值为一个变量或者属性他就不会有自己的引用计数,注定会消亡呢?

其中一个设计目标就是ReactiveCocoa允许这种类型的编程,这里管道可以匿名形式。所有你写过的响应式代码都应该看起来比较直观。

为了支持这种模型,ReactiveCocoa维持和保留自己全局的信号。如果它有一个或者多个subscribers(订阅者),信号就会活跃。如果所有的订阅者都移除掉了,信号就会被释放。想了解更多关于ReactiveCocoa管理进程,可以参看Memory Management 文档。

这就剩下了最后的问题:你如何从一个信号取消订阅?当一个completed或者error事件之后,订阅会自动的移除(一会就会学到)。手工的移除将会通过RACDisposable.

所有RACSignal的订阅方法都会返回一个RACDisposable实例,它允许你通过处置方法手动的移除订阅。下面是一个使用当前管道的快速的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
RACSignal *backgroundColorSignal =
  [self.searchText.rac_textSignal
    map:^id(NSString *text) {
      return [self isValidSearchText:text] ?
        [UIColor whiteColor] : [UIColor yellowColor];
    }];
  
RACDisposable *subscription =
  [backgroundColorSignal
    subscribeNext:^(UIColor *color) {
      self.searchText.backgroundColor = color;
    }];
  
// at some point in the future ...
[subscription dispose];

你不会经常做这些,但是你必须知道可能性的存在。

Note:作为这些的一个推论,如果你创建了一个管道,但是你不给他订阅,这个管道将不会执行,这些包括任何侧面的影响,例如doNext:blocks。

Avoiding Retain Cycles

当ReactiveCocoa在场景背后做了好多聪明的事情—这就意味着你不必要担心太多关于信号量的内存管理——这里有一个很重要的内存喜爱那个管的问你你需要考虑。

如果你看到下面的响应式代码你仅仅加入:

1
2
3
4
5
6
7
8
[[self.searchText.rac_textSignal
  map:^id(NSString *text) {
    return [self isValidSearchText:text] ?
      [UIColor whiteColor] : [UIColor yellowColor];
  }]
  subscribeNext:^(UIColor *color) {
    self.searchText.backgroundColor = color;
  }];

subscribeNext:block使用self来获得一个textField的引用,Blocks在封闭返回内捕获并且持有了值。因此在self和这个信号量之间造成了强引用,造成了循环引用。这取决于对象的生命周期,如果他的生命周期是应用程序的生命周期,那这样是没关系的,但是在更复杂的应用中就不行了。

为了避免这种潜在的循环引用,苹果官方文档:Working With Blocks 建议捕捉一个弱引用self,当前的代码可以这样写:

1
2
3
4
5
6
7
8
9
10
__weak RWSearchFormViewController *bself = self; // Capture the weak reference
  
[[self.searchText.rac_textSignal
  map:^id(NSString *text) {
    return [self isValidSearchText:text] ?
      [UIColor whiteColor] : [UIColor yellowColor];
  }]
  subscribeNext:^(UIColor *color) {
    bself.searchText.backgroundColor = color;
  }];

在上面的代码中,bself就是self标记为__weak(使用它可以make一个弱引用)的引用,现在可以看到使用textField的时候使用bself代用的。这看起来并不是那么高雅。

ReactiveCocoa框架包含了一个小诀窍,你可以使用它代替上百年的代码。添加下面的引用:

1
#import "RACEXTScope.h"

然后代码修改后如下:

1
2
3
4
5
6
7
8
9
10
@weakify(self)
[[self.searchText.rac_textSignal
  map:^id(NSString *text) {
    return [self isValidSearchText:text] ?
      [UIColor whiteColor] : [UIColor yellowColor];
  }]
  subscribeNext:^(UIColor *color) {
    @strongify(self)
    self.searchText.backgroundColor = color;
  }];

@weakify和@strongify语句是在Extended Objective-C库的宏定义,他们也包含在ReactiveCocoa中。@weakify 宏定义允许你创建一个若饮用的影子变量,@strongify宏定义允许你创建一个前面使用@weakify传递的强引用变量。

Note:如果你对@weakify和@strongify感兴趣,可以进入RACEXTSCope.h中查看其实现。

最后一个提醒,当在Blocks使用实例变量的时候要小心,这样也会导致block捕获一个self的强引用。你可以打开编译警告去告诉你你的代码有这个问题。

技术分享

好了,你从理论中幸存出来了,恭喜。现在你变得更加明智,准备移步到有趣的环节:添加一些真实的函数到你的工程里面。

Requesting Access to Twitter

为了在TwitterInstant 应用中去搜索Tweets,你将会用到社交框架(Social Framework)。为了访问Twitter你需要使用Accounts Framework。

在你添加代码之前,你需要到模拟器中输入你的账号:

技术分享

设置好账号之后,然后你只需要在RWSearchFormViewController.m中导入以下文件即可:

1
#import #import

然后在引入的头文件下面写如下的代码:

1
2
3
4
5
typedef NS_ENUM(NSInteger, RWTwitterInstantError) {
    RWTwitterInstantErrorAccessDenied,
    RWTwitterInstantErrorNoTwitterAccounts,
    RWTwitterInstantErrorInvalidResponse}; 
static NSString * const RWTwitterInstantDomain = @"TwitterInstant";

你将会使用这些简单地鉴定错误。然后在interface和end之间声明两个属性:

1
2
@property (strong, nonatomic) ACAccountStore *accountStore;
@property (strong, nonatomic) ACAccountType *twitterAccountType;

 

ACAccountsStore类提供访问你当前设备有的social账号,ACAccountType类代表指定类型的账户。

然后在viewDidLoad里面加入以下代码:

1
2
self.accountStore = [[ACAccountStore alloc] init];
self.twitterAccountType = [self.accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];

这些代码创建了账户存储和Twitter账号标示。在.m中添加如下方法:

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
- (RACSignal *)requestAccessToTwitterSignal {
 
  // 1 - define an error
  NSError *accessError = [NSError errorWithDomain:RWTwitterInstantDomain
                                             code:RWTwitterInstantErrorAccessDenied
                                         userInfo:nil];
  // 2 - create the signal
  @weakify(self)
  return [RACSignal createSignal:^RACDisposable *(id subscriber) {
    // 3 - request access to twitter
    @strongify(self)
    [self.accountStore
       requestAccessToAccountsWithType:self.twitterAccountType
         options:nil
      completion:^(BOOL granted, NSError *error) {
          // 4 - handle the response
          if (!granted) {
            [subscriber sendError:accessError];
          else {
            [subscriber sendNext:nil];
            [subscriber sendCompleted];
          }
        }];
    return nil;
  }];
}

这个方法的作用是:

1、定义了如果用户拒绝访问的错误

2、根据第一个入门教程,类方法createSignal返回了一个RACSignal的实例。

3、通过账户存储请求访问Twitter。在这一点上,用户将看到一个提示,要求他们给予这个程序访问Twitter账户的弹框。

4、当用户同意或者拒绝访问,信号事件就会触发。如果用户同意访问,next事件将会紧随而来,然后是completed发送,如果用户拒绝访问,error事件会触发。

如果你回想其第一个入门教程,一个信号可以以三种不同的事件发出:

1、next

2、completed

3、error

超过了signal的生命周期,它将不会发出任何信号事件。

最后,为了充分利用信号,在viewDidLoad后面添加如下代码;

1
2
3
4
[[self requestAccessToTwitterSignal]
  subscribeNext:^(id x) {
    NSLog(@"Access granted");  } error:^(NSError *error) {
    NSLog(@"An error occurred: %@", error);  }];

如果你运行程序,将会看到一个弹出框:

技术分享

提示是否允许访问权限,如果ok,则打印出来Access granted ,否则将会走error。

Accounts Framework会记住你的决定,因此如果想再次测试,你需要针对模拟机进行:Reset Contents and Settings。

Chaining Signals

一旦用户允许访问Twitter账户,为了执行twitter,程序将会不断监听搜索内容textField的变化.

程序需要等待信号,它请求访问Twitter去发出completed事件,然后订阅textField的信号。不同信号连续的链是一个共有的问题,但是ReactiveCocoa处理起来非常优雅。

用下面的代码替换当前在viewDidLoad后面的管道:

1
2
3
4
5
6
7
8
9
10
[[[self requestAccessToTwitterSignal]
  then:^RACSignal *{
    @strongify(self)
    return self.searchText.rac_textSignal;
  }]
  subscribeNext:^(id x) {
    NSLog(@"%@", x);
  } error:^(NSError *error) {
    NSLog(@"An error occurred: %@", error);
  }];

then方法会一直等待,知道completed事件发出,然后订阅者通过自己的block参数返回,这有效地将控制从一个信号传递给下一个。

Note:上面已经写过了@weakly(self);所以这里就不用再写了。

then方法传递error事件。因此最后的subscribeNext:error: block还接收初始的访问请求错误。

当你运行的时候,然后允许访问,你应该可以在控制台看到打印出来的你输入的东西。

然后,添加filter操作到管道去移除无效的搜索字符串。在这个实例中,他们是不到三个字符的string:

1
2
3
4
5
6
7
8
9
10
[[[[self requestAccessToTwitterSignal]
  then:^RACSignal *{
    @strongify(self)
    return self.searchText.rac_textSignal;  }]
  filter:^BOOL(NSString *text) {
    @strongify(self)
    return [self isValidSearchText:text];  }]
  subscribeNext:^(id x) {
    NSLog(@"%@", x);  } error:^(NSError *error) {
    NSLog(@"An error occurred: %@", error);  }];

运行就可以在控制台看到只有三个以上的才能输出。

图解一下上边的管道:

技术分享

程序管道从requestAccessToTwitterSignal信号开始,然后转换到tac_textSignal。同事next事件通过filter,最后到达订阅block.你也可以看到任何通过第一步的error事件。

现在你有一个发出搜索text的信号,它可以用来搜索Twitter了。很有趣吧。

Searching Twitter

Social Framework是一个访问Twitter 搜索API的选项。然而,它并无法响应搜索,下一步就是给信号包括API请求方法。在当前的控制器中,添加如下方法:

1
2
3
4
5
- (SLRequest *)requestforTwitterSearchWithText:(NSString *)text { 
     NSURL *url = [NSURL URLWithString:@"https://api.twitter.com/1.1/search/tweets.json"];      NSDictionary *params = @{@"q" : text};   
     SLRequest *request =  [SLRequest requestForServiceType:SLServiceTypeTwitter                                           requestMethod:SLRequestMethodGET  URL:url parameters:params        ];  
     return request;
 }

这创建了一个请求:搜索Twitter(V.1.1REST API)。这个是调用Twitter的api。

下一步就是创建一个基于request的信号量。添加如下方法:

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
- (RACSignal *)signalForSearchWithText:(NSString *)text {
  
  // 1 - define the errors
  NSError *noAccountsError = [NSError errorWithDomain:RWTwitterInstantDomain
                                                 code:RWTwitterInstantErrorNoTwitterAccounts
                                             userInfo:nil]; 
  NSError *invalidResponseError = [NSError errorWithDomain:RWTwitterInstantDomain
                                                      code:RWTwitterInstantErrorInvalidResponse
                                                  userInfo:nil]; 
  // 2 - create the signal block
  @weakify(self)
  return [RACSignal createSignal:^RACDisposable *(id subscriber) {
    @strongify(self); 
    // 3 - create the request
    SLRequest *request = [self requestforTwitterSearchWithText:text]; 
    // 4 - supply a twitter account
    NSArray *twitterAccounts = [self.accountStore
      accountsWithAccountType:self.twitterAccountType];    if (twitterAccounts.count == 0) {
      [subscriber sendError:noAccountsError];    } else {
      [request setAccount:[twitterAccounts lastObject]]; 
      // 5 - perform the request
      [request performRequestWithHandler: ^(NSData *responseData,                                          NSHTTPURLResponse *urlResponse, NSError *error) {
        if (urlResponse.statusCode == 200) {
  
          // 6 - on success, parse the response
          NSDictionary *timelineData =
             [NSJSONSerialization JSONObjectWithData:responseData
                                             options:NSJSONReadingAllowFragments
                                               error:nil];          [subscriber sendNext:timelineData];          [subscriber sendCompleted];        }
        else {
          // 7 - send an error on failure
          [subscriber sendError:invalidResponseError];        }
      }];    }
  
    return nil;  }];}

然后在viewDidLoad方法中进一步添加信号量:

1
2
3
4
5
6
7
8
9
10
11
12
13
[[[[[self requestAccessToTwitterSignal]
  then:^RACSignal *{
    @strongify(self)
    return self.searchText.rac_textSignal;  }]
  filter:^BOOL(NSString *text) {
    @strongify(self)
    return [self isValidSearchText:text];  }]
  flattenMap:^RACStream *(NSString *text) {
    @strongify(self)
    return [self signalForSearchWithText:text];  }]
  subscribeNext:^(id x) {
    NSLog(@"%@", x);  } error:^(NSError *error) {
    NSLog(@"An error occurred: %@", error);  }];

运行:

即可在控制台里面打印出来筛选的数据。

Threading

我很确信你这会亟待把JSON数据放到UI里面,但是在放到UI里面之前你需要做最后一件事:找到他是什么,你需要做一些探索!

添加一个端点到subscribeNext:error:那个步,然后我们会看到Xcode左侧的Thread,我们发现如果想加载图片的话必须在主线程里面,但是他不在主线程中,所以我们就可以做如下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[[[[[[self requestAccessToTwitterSignal]
  then:^RACSignal *{
    @strongify(self)
    return self.searchText.rac_textSignal;
  }]
  filter:^BOOL(NSString *text) {
    @strongify(self)
    return [self isValidSearchText:text];
  }]
  flattenMap:^RACStream *(NSString *text) {
    @strongify(self)
    return [self signalForSearchWithText:text];
  }]
  deliverOn:[RACScheduler mainThreadScheduler]]
  subscribeNext:^(id x) {
    NSLog(@"%@", x);
  } error:^(NSError *error) {
    NSLog(@"An error occurred: %@", error);
  }];

这样就会在主线程中运行。也就是更新了管道:添加了deliverOn:操作。

然后再次运行我们就会发现他是在主线程上执行了。这样你就可以更新UI了。

Updating the UI

这里用到了另一个库:LinqToObjectiveC。安装方式就不说了和ReactiveCocoa一样

我们在RWSearchFormViewController中导入:

1
2
#import "RWTweet.h"
#import "NSArray+LinqExtensions.h"

然后在输出json数据的地方修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[[[[[[self requestAccessToTwitterSignal]
  then:^RACSignal *{
    @strongify(self)
    return self.searchText.rac_textSignal;  }]
  filter:^BOOL(NSString *text) {
    @strongify(self)
    return [self isValidSearchText:text];  }]
  flattenMap:^RACStream *(NSString *text) {
    @strongify(self)
    return [self signalForSearchWithText:text];  }]
  deliverOn:[RACScheduler mainThreadScheduler]]
  subscribeNext:^(NSDictionary *jsonSearchResult) {
    NSArray *statuses = jsonSearchResult[@"statuses"];    NSArray *tweets = [statuses linq_select:^id(id tweet) {
      return [RWTweet tweetWithStatus:tweet];    }];    [self.resultsViewController displayTweets:tweets];  } error:^(NSError *error) {
    NSLog(@"An error occurred: %@", error);  }];

运行:

就可以看到右侧的详情页面加载到数据了。刚引入的类库其实就是将json数据转换成了model.加载数据的效果如下:

技术分享

Asynchronous Loading of Images

现在内容都加载出来了,就差图片了。在RWSearchResultsViewController.m中添加如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-(RACSignal *)signalForLoadingImage:(NSString *)imageUrl {
  
  RACScheduler *scheduler = [RACScheduler
                         schedulerWithPriority:RACSchedulerPriorityBackground];
  
  return [[RACSignal createSignal:^RACDisposable *(id subscriber) {
    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]];
    UIImage *image = [UIImage imageWithData:data];
    [subscriber sendNext:image];
    [subscriber sendCompleted];
    return nil;
  }] subscribeOn:scheduler];
  
}

这会你一ing该就会很熟悉这种模式了。然后在tableview:cellForRowAtIndex:方法里面添加:

1
2
3
4
5
6
7
cell.twitterAvatarView.image = nil;
  
[[[self signalForLoadingImage:tweet.profileImageUrl]
  deliverOn:[RACScheduler mainThreadScheduler]]
  subscribeNext:^(UIImage *image) {
   cell.twitterAvatarView.image = image;
  }];

再次运行就可以出来效果了:

技术分享

Throttling(限流)

你可能注意到这个问题:每次输入一个字符串都会立即执行然后导致刷新太快 ,导致每秒会显示几次搜索结果。这不是理想的状态。

一个好的解决方式就是如果搜索内容不变之后的时间间隔后在搜索比如500毫秒。

而ReactiveCocoa是这个工作变的如此简单。

打开RWSearchFormViewController.m然后更新管道,调整如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[[[[[[[self requestAccessToTwitterSignal]
  then:^RACSignal *{
    @strongify(self)
    return self.searchText.rac_textSignal;
  }]
  filter:^BOOL(NSString *text) {
    @strongify(self)
    return [self isValidSearchText:text];
  }]
  throttle:0.5]
  flattenMap:^RACStream *(NSString *text) {
    @strongify(self)
    return [self signalForSearchWithText:text];
  }]
  deliverOn:[RACScheduler mainThreadScheduler]]
  subscribeNext:^(NSDictionary *jsonSearchResult) {
    NSArray *statuses = jsonSearchResult[@"statuses"];
    NSArray *tweets = [statuses linq_select:^id(id tweet) {
      return [RWTweet tweetWithStatus:tweet];
    }];
    [self.resultsViewController displayTweets:tweets];
  } error:^(NSError *error) {
    NSLog(@"An error occurred: %@", error);
  }];

你会发现这样就可以了。throttle操作只是发送一个操作,这个操作在时间到之后继续进行。

以上是关于Android 上的协程(第二部分):入门的主要内容,如果未能解决你的问题,请参考以下文章

什么时候释放android服务中的协程资源?

“无法访问主线程上的数据库,因为它可能会长时间锁定 UI。”我的协程错误

python基础===基于requests模块上的协程trip

我的协程在 Unity android 应用程序上运行速度较慢

iOS开发 ReactiveCocoa入门教程 第二部分

我如何才能等到我的 Android 应用程序中的协程作用域执行完成?