反思与第二部序章

魔鬼藏在细节

在上一篇中的最後我问了一个问题:“为什麽没有使用 Flowable 而是继续用 Observable 来解决 Backpressure?”,不知道大家的心目中有没有答案了呢?这边先给大家答案,其实上次的实作是有问题的!

为了模拟我们在 FirebaseNoteRepository 中的实作,照惯例来用简单的程序码来做实验吧!我想做的实验如下:如果上游持续不断的发送新资料,在有使用 throttleLast 的情况下,接收方还是有可能会塞车吗?

val timer = Observable.interval(5, TimeUnit.MILLISECONDS)

timer
    .throttleLast(30, TimeUnit.MILLISECONDS)
    .doOnNext { println("New item from throttleLast --------- $it") }
    .observeOn(Schedulers.io())
    .subscribe{
        Thread.sleep(1000)
        println("New item for subscribe --------- $it")
    }

Thread.sleep(10000)

这边我使用了 Observable 的 interval 来模拟持续不断发送的事件,每五毫秒就送出一个新事件,接着再对这个 observable 使用 throttleLast ,每 30 毫秒就只留一个事件,并且最後切换执行绪再进行 subscribe 。在 subscribe 这边,为了模拟长时间的任务,毕竟我们是在写入网路资料,在每次执行的时候会停止该执行绪一秒钟。这边几乎完整还原了上一篇最後所使用的方式,接下来看看输出是长怎样吧!

...
New item from throttleLast --------- 191
New item from throttleLast --------- 197
New item from throttleLast --------- 203
New item for subscribe --------- 5
New item from throttleLast --------- 209
New item from throttleLast --------- 215
New item from throttleLast --------- 221
...
...
...
New item from throttleLast --------- 1805
New item from throttleLast --------- 1811
New item for subscribe --------- 54
New item from throttleLast --------- 1817
New item from throttleLast --------- 1823
New item from throttleLast --------- 1829

从执行结果来看,subscribe 中接收到的事件慢了好几拍!而且差距只会越来越远,所以就算使用了 throttleLast ,只要还是 Observable ,就无法解决 Backpressure 的问题!所以只要我们换成 Flowable 就没问题了吗?

val timer = Observable.interval(5, TimeUnit.MILLISECONDS)

timer
    **.toFlowable(BackpressureStrategy.LATEST)** // 切换为 Flowable
    .doOnNext { println("New item from flowable --------- $it") }
    .observeOn(Schedulers.io())
    .subscribe{
        Thread.sleep(1000)
        println("New item for subscribe --------- $it")
    }

Thread.sleep(10000)

执行结果如下:

New item from flowable --------- 0
New item from flowable --------- 1
New item from flowable --------- 2
New item from flowable --------- 3
New item from flowable --------- 4
...
...
New item from flowable --------- 126
New item from flowable --------- 127
New item for subscribe --------- 0
New item for subscribe --------- 1
New item for subscribe --------- 2
New item for subscribe --------- 3
New item for subscribe --------- 4
New item for subscribe --------- 5
New item for subscribe --------- 6
New item for subscribe --------- 7
New item for subscribe --------- 8

Flowable 执行到 127 就停了,接着 subscribe 这里才慢慢的一个一个拿出来,所以这里显示的资料是 Flowable 的 buffer 给我们的(还记得 Flowable 预设的 BUFFEER_SIZE 是 128 吗?)。但是,这并不是我们想要的结果!我们预期要拿到的结果是现在此时此刻的最新结果,而不是过去所累积的资料,现在资料很明显的卡在某个地方!

这时候请看到 observeOn 这边,点进去实作後我们发现了:

Screen Shot 2021-09-11 at 8.55.17 PM.png

Flowable 的 obserbeOn 还有另一个有 buffer size 的实作!看来我们接近答案了,如果将 buffer size 的数值调成 1 的话会发生什麽事呢?

val timer = Observable.interval(5, TimeUnit.MILLISECONDS)
timer
    .toFlowable(BackpressureStrategy.LATEST)
    .doOnNext { println("New item from flowable --------- $it") }
    **.observeOn(Schedulers.io(), false, 1)** // 使用有 buffer size 的 observeOn
    .subscribe{
        Thread.sleep(1000)
        println("New item for subscribe --------- $it")
    }

Thread.sleep(10000)

结果如下:

New item from flowable --------- 0
New item for subscribe --------- 0
New item from flowable --------- 201
New item for subscribe --------- 201
New item from flowable --------- 402
New item for subscribe --------- 402
New item from flowable --------- 603
New item for subscribe --------- 603
New item from flowable --------- 803
New item for subscribe --------- 803
...

看起来很棒! subscribe 拿到的永远是最新的值了!

细节很重要

有时候我们在解决问题时,这边试一试,那边试一试,从网路上复制别人的解法贴到自己的程序码中,在这过程中有可能就试出了没问题的版本,虽然不是很懂里面的运作机制,但是也没太放在心上就直接 release 了。但是在这时候就有可能悄悄的埋下一颗未爆弹,等到爆炸的时候,没人知道为什麽爆炸,可能得要花上一整天的时间来 Debug 才能找出原因。

所以要怎麽办呢?事实上我也犯了很多次这样的错,如果专案时间很赶的话我们也没有时间慢慢研究原理不是吗?暂时可以动就好拉!没错,我们很多时候是要跟时间妥协的,所以在这时候至少养成一个好习惯,就是当你觉得某个程序码片段你不太确定其中运作的原理的时候,就加个 TODO 注解吧!并且确实的放到 backlog 中,至少大家都会意识到它的存在。

说到这,其实上一篇的程序码还有另一个问题,我们在设定新资料时所使用的 firebase api 是同步的还是非同步呢?如果是非同步,不就表示还是有可能造成塞车吗?那我们来回头看看是怎麽设定新资料的:

private fun setNoteDocument(note: Note) {
    val noteData = hashMapOf(
        FIELD_TEXT to note.text,
        FIELD_COLOR to note.color.color,
        FIELD_POSITION_X to note.position.x.toString(),
        FIELD_POSITION_Y to note.position.y.toString()
    )

    firestore.collection(COLLECTION_NOTES)
        .document(note.id)
        .set(noteData)
}

// Firebase - DocumentReference
/**
 * Overwrites the document referred to by this {@code DocumentReference}. If the document does not
 * yet exist, it will be created. If a document already exists, it will be overwritten.
 *
 * @param data The data to write to the document (e.g. a Map or a POJO containing the desired
 *     document contents).
 * @return A Task that will be resolved when the write finishes.
 */
@NonNull
public Task<Void> set(@NonNull Object data) {
  return set(data, SetOptions.OVERWRITE);
}

点进去 set 实作後发现,原来这会回传一个 Task ,那这表示我们使用的是一个非同步的作法,那这时候该怎麽办呢?而且根据官方文件,他们似乎没提供同步的作法(我没有找到,如果读者有找到的话也请跟我讲),同时官方文件也建议了每秒钟不应该有超过一个写入的请求:

Screen Shot 2021-09-11 at 9.48.05 PM.png

看起来我们回到了原点了,花了这麽多时间结果才发现用 Flowable 也无法完美解决我们在 firestore 上遇到的问题,不过这是一个很棒的经验,至少学习到了 Backpressure 更正确的用法,还有更加确定我们的实作是如何运行的,因此在这边我决定遵循文件的建议,将写入的频率调整到每秒钟一次:

updatingNoteSubject
    .throttleLast(1000, TimeUnit.MILLISECONDS) // 每秒只执行写入一次
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { optNote ->
        optNote.ifPresent { setNoteDocument(it) }
    }

第二部

终於完成手势移动便利贴的这个功能了,但是我们的 App 还有很多功能要做呢!接下来我们将完成:

  • 更改颜色
  • 建立新的便利贴
  • 删除既有的便利贴
  • 更改文字

由於这些最终都会操作到 NoteRepository ,所以在本篇的最後,我们就把所有需要用到的 CRUD 功能都开一开吧!

interface NoteRepository {
    fun getAllNotes(): Observable<List<Note>>
    fun getNoteById(id: String): Observable<Note>
    fun putNote(note: Note)
    fun createNote(note: Note)
    fun deleteNote(noteId: String)
}

class FirebaseNoteRepository: NoteRepository {
   ...
   ...
   override fun getNoteById(id: String): Observable<Note> {
        return allNotesSubject.map { notes ->
            Optional.ofNullable(notes.find { note -> note.id == id })
        }.mapOptional { it }
    }

    override fun createNote(note: Note) {
        setNoteDocument(note)
    }

		override fun deleteNote(noteId: String) {
        firestore.collection(COLLECTION_NOTES)
            .document(noteId)
            .delete()
    }
}

由於实作相对容易,就不直逐行解说了,明天将会跟大家一起来完成这些新的功能!


<<:  Day18-pytorch(1)认识tensor

>>:  Day5 「开机」学习 Lua - 变数型别与宣告

Day11 Platform Channel - EventChannel

EventChannel EventChannel:用於接收一系列讯息,这些讯息被包装到 Strea...

第六章 之三

上次提到了在wordpress建置时选择主题的方式,本次就来看一下有关Themes购买,一个是在wo...

qpushbutton 不同的字不同大小和顔色

由於不同的字用不同的样式,所以需要用到html来设定: //add take buttons wit...

DAY27-Firebase Domain设定

前言: 昨天跟大家介绍了如何把你写好的网页透过firebase deploy到网路上,但目前还只能透...

谈谈讯息元件

常用的讯息有以下几类: Toast AlertDialog Toast 是快讯显示的即时计息,几秒内...