RxJava中.zip操作符重复发射相同值问题排查
首先,你的核心问题出在两个地方:Android MotionEvent的复用机制和BehaviorSubject的缓存特性,其中前者是导致你收到两个相同ACTION_UP事件的直接原因。
1. MotionEvent被系统复用,导致对象内容被覆盖
Android系统为了优化性能,会对MotionEvent对象进行池化复用——也就是说,同一个MotionEvent对象可能会被用来承载不同的触摸事件(比如先承载ACTION_DOWN,之后被修改为ACTION_UP)。当你直接把原始的me对象发送到BehaviorSubject时,后续的触摸事件会覆盖这个对象的内容。
举个实际场景:
- 触发ACTION_DOWN时,你把
me发送到touchDownSource,此时这个对象的action是DOWN; - 触发ACTION_UP时,系统复用了同一个
me对象,修改它的action为UP,然后你把它发送到touchUpSource; - 这时候
touchDownSource里缓存的那个MotionEvent对象已经被改成了UP,所以zip操作符拿到的两个事件自然是同一个UP对象。
2. BehaviorSubject的缓存特性可能不符合你的需求
BehaviorSubject会缓存最后一次发送的事件,当有新的事件到来时会自动和缓存的事件配对。如果你的需求是配对每一次ACTION_DOWN和紧随其后的ACTION_UP,这种缓存特性会导致错误配对:比如第一次DOWN-UP配对后,第二次触发DOWN时,touchUpSource缓存的还是上一次的UP事件,zip会立刻把新DOWN和旧UP配对,这显然不是你想要的。
修正后的代码
针对这两个问题,我们可以这样修改:
第一步:复制MotionEvent,避免复用问题
在发送事件到Subject时,使用MotionEvent.obtain(me)创建一个副本,这样原始对象的修改不会影响到Subject中保存的事件:
val touchDownSource = PublishSubject.create<MotionEvent>() // 换成PublishSubject val touchUpSource = PublishSubject.create<MotionEvent>() RxView.touches(binding.containerOverlay) .subscribe({ me -> when (me.action) { MotionEvent.ACTION_DOWN -> touchDownSource.onNext(MotionEvent.obtain(me)) MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> touchUpSource.onNext(MotionEvent.obtain(me)) } })
第二步:换成PublishSubject,避免错误配对
PublishSubject不会缓存事件,只会发送订阅之后产生的新事件,这样每次DOWN事件都会等待后续的第一个UP事件进行配对,完全符合你的需求。
第三步:回收复制的MotionEvent,避免内存泄漏
因为我们创建了MotionEvent的副本,使用完后需要调用recycle()释放资源:
val zipper = BiFunction<MotionEvent, MotionEvent, Pair<MotionEvent, MotionEvent>>({ v1, v2 -> v1 to v2 }) Observable.zip(touchDownSource, touchUpSource, zipper) .filter { Math.abs(it.first.toPoint().length() - it.second.toPoint().length()) < 50 } .map { val point = it.second.toPoint() // 回收两个MotionEvent副本 it.first.recycle() it.second.recycle() point } .subscribe({ Utils.toast(this, "${it.x}--${it.y}") }, ErrLogger::e)
这样修改后,你就不会再收到两个相同的ACTION_UP事件,而且每一次DOWN都会和对应的UP正确配对啦!
内容的提问来源于stack exchange,提问作者P. Ilyin

