Skip to content

Commit 624aaab

Browse files
committed
Fix WeakListenerManager and TimberJvmRule bugs
1 parent 02e2ff9 commit 624aaab

File tree

4 files changed

+131
-13
lines changed

4 files changed

+131
-13
lines changed

baseLib/src/main/java/me/ycdev/android/lib/common/utils/WeakListenerManager.kt

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package me.ycdev.android.lib.common.utils
22

3+
import androidx.annotation.VisibleForTesting
4+
import timber.log.Timber
35
import java.lang.ref.WeakReference
46
import java.util.ArrayList
57

@@ -11,9 +13,9 @@ open class WeakListenerManager<IListener : Any> {
1113
fun notify(listener: IListener)
1214
}
1315

14-
private class ListenerInfo<IListener : Any> internal constructor(listener: IListener) {
15-
internal var className: String = listener::class.java.name
16-
internal var holder: WeakReference<IListener> = WeakReference(listener)
16+
private class ListenerInfo<IListener : Any>(listener: IListener) {
17+
var className: String = listener::class.java.name
18+
var holder: WeakReference<IListener> = WeakReference(listener)
1719
}
1820

1921
/**
@@ -76,23 +78,27 @@ open class WeakListenerManager<IListener : Any> {
7678

7779
fun notifyListeners(action: (IListener) -> Unit) {
7880
synchronized(listeners) {
79-
var i = 0
80-
while (i < listeners.size) {
81-
val listenerInfo = listeners[i]
81+
// The listener may unregister itself!
82+
val listenersCopied: List<ListenerInfo<IListener>> = ArrayList(listeners)
83+
for ((i, listenerInfo) in listenersCopied.withIndex()) {
8284
val l = listenerInfo.holder.get()
8385
if (l == null) {
84-
LibLogger.e(TAG, "listener leak found: " + listenerInfo.className)
85-
listeners.removeAt(i)
86+
Timber.tag(TAG).w("listener leak found: %s", listenerInfo.className)
87+
listeners.remove(listenerInfo)
8688
} else {
87-
LibLogger.d(TAG, "notify: " + listenerInfo.className)
89+
Timber.tag(TAG).d("notify #%d: %s", i, listenerInfo.className)
8890
action(l)
89-
i++
9091
}
9192
}
92-
LibLogger.d(TAG, "notify done, cur size: " + listeners.size)
93+
Timber.tag(TAG).d("notify done, cur size: %d in %s", listeners.size, this)
9394
}
9495
}
9596

97+
@VisibleForTesting
98+
internal fun listenersCount(): Int {
99+
return listeners.size
100+
}
101+
96102
companion object {
97103
private const val TAG = "WeakListenerManager"
98104
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package me.ycdev.android.lib.common.utils
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import me.ycdev.android.lib.test.rules.TimberJvmRule
5+
import org.junit.Rule
6+
import org.junit.Test
7+
8+
class WeakListenerManagerTest {
9+
@get:Rule
10+
val timberRule = TimberJvmRule()
11+
12+
@Test
13+
fun basic() {
14+
val manager = DemoListenerManager()
15+
val listener1 = DemoListener(manager)
16+
val listener2 = DemoListener(manager)
17+
18+
manager.addListener(listener1)
19+
manager.addListener(listener2)
20+
21+
assertThat(manager.listenersCount()).isEqualTo(2)
22+
23+
manager.notifyListeners { l -> l.call(1) }
24+
assertThat(listener1.value).isEqualTo(1)
25+
assertThat(listener2.value).isEqualTo(1)
26+
27+
manager.notifyListeners { l -> l.call(2) }
28+
assertThat(listener1.value).isEqualTo(2)
29+
assertThat(listener2.value).isEqualTo(2)
30+
31+
assertThat(manager.listenersCount()).isEqualTo(2)
32+
}
33+
34+
@Test
35+
fun listenerLeak() {
36+
val manager = DemoListenerManager()
37+
val listener1 = DemoListener(manager)
38+
39+
manager.addListener(listener1)
40+
addLeakedListener(manager)
41+
42+
// force GC
43+
GcHelper.forceGc()
44+
45+
// before notify
46+
assertThat(manager.listenersCount()).isEqualTo(2)
47+
48+
manager.notifyListeners { l -> l.call(1) }
49+
assertThat(listener1.value).isEqualTo(1)
50+
51+
// after notify
52+
assertThat(manager.listenersCount()).isEqualTo(1)
53+
54+
manager.notifyListeners { l -> l.call(2) }
55+
assertThat(listener1.value).isEqualTo(2)
56+
}
57+
58+
@Test
59+
fun listenerRemovedWhenNotify() {
60+
val manager = DemoListenerManager()
61+
val listener1 = DemoListener(manager, true)
62+
val listener2 = DemoListener(manager)
63+
64+
manager.addListener(listener1)
65+
manager.addListener(listener2)
66+
67+
assertThat(manager.listenersCount()).isEqualTo(2)
68+
69+
manager.notifyListeners { l -> l.call(1) }
70+
assertThat(listener1.value).isEqualTo(1)
71+
assertThat(listener2.value).isEqualTo(1)
72+
73+
assertThat(manager.listenersCount()).isEqualTo(1)
74+
75+
manager.notifyListeners { l -> l.call(2) }
76+
assertThat(listener1.value).isEqualTo(1)
77+
assertThat(listener2.value).isEqualTo(2)
78+
79+
assertThat(manager.listenersCount()).isEqualTo(1)
80+
}
81+
82+
private fun addLeakedListener(manager: DemoListenerManager) {
83+
manager.addListener(DemoListener(manager))
84+
}
85+
86+
class DemoListener(
87+
private val manager: DemoListenerManager,
88+
private val notifyOnce: Boolean = false
89+
) {
90+
var value: Int = 0
91+
92+
fun call(value: Int) {
93+
this.value = value
94+
if (notifyOnce) {
95+
manager.removeListener(this)
96+
}
97+
}
98+
}
99+
100+
class DemoListenerManager : WeakListenerManager<DemoListener>()
101+
}

testLib/src/main/java/me/ycdev/android/lib/test/log/TimberJvmTree.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,16 @@ class TimberJvmTree : Timber.Tree() {
2525
println(log)
2626
t?.printStackTrace(System.out)
2727
}
28+
29+
companion object {
30+
fun plantIfNeeded() {
31+
// only plant TimberJvmTree once
32+
Timber.forest().forEach {
33+
if (it is TimberJvmTree) {
34+
return
35+
}
36+
}
37+
Timber.plant(TimberJvmTree())
38+
}
39+
}
2840
}

testLib/src/main/java/me/ycdev/android/lib/test/rules/TimberJvmRule.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ package me.ycdev.android.lib.test.rules
22

33
import me.ycdev.android.lib.test.log.TimberJvmTree
44
import org.junit.rules.ExternalResource
5-
import timber.log.Timber
65

76
class TimberJvmRule : ExternalResource() {
87
override fun before() {
9-
Timber.plant(TimberJvmTree())
8+
TimberJvmTree.plantIfNeeded()
109
}
1110
}

0 commit comments

Comments
 (0)