Skip to content

Commit

Permalink
Switched ArrayList with costum Linked List and IdentityHashMap
Browse files Browse the repository at this point in the history
  • Loading branch information
IlyaGazman committed Mar 22, 2021
1 parent cff2279 commit e47fbce
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 72 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.2'
classpath 'com.android.tools.build:gradle:4.1.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
Expand Down
8 changes: 4 additions & 4 deletions library/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.gazman.signals"
xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gazman.signals">

<application>
<provider
android:name=".context.ContextProvider"
android:authorities="com.gazman.signals.context.ContextProvider"
android:exported="false"
android:name=".context.ContextProvider" />
android:exported="false" />
</application>
</manifest>
67 changes: 67 additions & 0 deletions library/src/main/java/com/gazman/signals/ListenersList.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.gazman.signals

import java.util.*

internal class ListenersList<T> : Iterable<T?> {
private val none = Node<T>(null)
private var head: Node<T>? = null
private var tail: Node<T>? = null
private var map = IdentityHashMap<T, Node<T>>()

fun isNotEmpty(): Boolean {
return map.isNotEmpty()
}

fun add(listener: T) {
synchronized(this) {
if (map.containsKey(listener)) {
return
}
val node = Node(listener)
map[listener] = node

if (tail == null) {
head = node
tail = node
} else {
node.previous = tail
tail?.next = node
tail = node
}
}
}

fun remove(listener: T) {
synchronized(this) {
val node = map[listener]
node?.previous?.next = node?.next
node?.next?.previous = node?.previous
}
}

override fun iterator(): Iterator<T?> {
return object : Iterator<T?> {
var node: Node<T>? = none

override fun hasNext() = node != null && node != tail

override fun next(): T? {
node = if (node == none) {
this@ListenersList.head
} else {
node?.next
}
return node?.value
}

}
}

fun clear() {
synchronized(this) {
head = null
tail = null
map.clear()
}
}
}
6 changes: 6 additions & 0 deletions library/src/main/java/com/gazman/signals/Node.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.gazman.signals

internal class Node<T>(val value: T?) {
var previous: Node<T>? = null
var next: Node<T>? = null
}
67 changes: 17 additions & 50 deletions library/src/main/java/com/gazman/signals/Signal.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@ class Signal<T> internal constructor(@JvmField val originalType: Class<T>) {

@JvmField
val dispatcher: T
private val synObject = Any()
private val listeners = ArrayList<T>()
private val oneTimeListeners = LinkedList<T>()
private var hasListeners = false
private val listeners = ListenersList<T>()
private val oneTimeListeners = ListenersList<T>()
private var invoker = DEFAULT_INVOKER


Expand Down Expand Up @@ -65,7 +63,9 @@ class Signal<T> internal constructor(@JvmField val originalType: Class<T>) {
* @param listener listener to register
*/
fun addListener(listener: T) {
applyListener(listeners, listener)
synchronized(this) {
listeners.add(listener)
}
}

/**
Expand All @@ -77,22 +77,8 @@ class Signal<T> internal constructor(@JvmField val originalType: Class<T>) {
* @param listener listener to register
*/
fun addListenerOnce(listener: T) {
applyListener(oneTimeListeners, listener)
}

private fun <TYPE> applyListener(list: MutableList<TYPE>, listener: TYPE) {
validateListener(listener)
synchronized(synObject) {
if (!list.contains(listener)) {
hasListeners = true
list.add(listener)
}
}
}

private fun validateListener(listener: Any?) {
if (listener == null) {
throw NullPointerException("Listener can't be null")
synchronized(this) {
oneTimeListeners.add(listener)
}
}

Expand All @@ -102,47 +88,28 @@ class Signal<T> internal constructor(@JvmField val originalType: Class<T>) {
* @param listener listener to unregister
*/
fun removeListener(listener: T) {
validateListener(listener)
synchronized(synObject) {
synchronized(this) {
listeners.remove(listener)
oneTimeListeners.remove(listener)
updateHasListeners()
}
}

private fun updateHasListeners() {
synchronized(synObject) { hasListeners = listeners.size + oneTimeListeners.size > 0 }
}

private operator fun invoke(method: Method?, args: Array<Any?>?) {
var listener: T?
if (listeners.size > 0) {
var i = 0
while (true) {
synchronized(synObject) {
listener = if (i < listeners.size) {
listeners[i]
} else {
null
}
}
if (listener == null) {
break
}
invoker.invoke(method, args, listener!!)
i++
for (listener in listeners) {
if (listener != null) {
invoker.invoke(method, args, listener)
}
}
while (oneTimeListeners.size > 0) {
synchronized(synObject) {
listener = oneTimeListeners.removeFirst()
for (listener in oneTimeListeners) {
if (listener != null) {
invoker.invoke(method, args, listener)
}
invoker.invoke(method, args, listener!!)
}
updateHasListeners()

oneTimeListeners.clear()
}

fun hasListeners(): Boolean {
return hasListeners
return listeners.isNotEmpty() || oneTimeListeners.isNotEmpty()
}
}
95 changes: 78 additions & 17 deletions library/src/test/java/com/gazman/signals/SignalsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,101 @@ import org.junit.Test
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import kotlin.random.Random

class SignalsTest {

private fun interface EatSignal {
fun onEat()
}

@Test
fun testDispatch() {
val foods = AtomicInteger()
fun addListenerAndDispatch() {
val counter = AtomicInteger()
val eatSignal: Signal<EatSignal> = signal(EatSignal::class)
eatSignal.addListener { foods.incrementAndGet() }
eatSignal.addListener { foods.incrementAndGet() }
eatSignal.addListener { counter.incrementAndGet() }
eatSignal.addListener { counter.incrementAndGet() }

eatSignal.dispatcher.onEat()
Assert.assertEquals(2, counter.get())
}

@Test
fun removeListener() {
val counter = AtomicInteger()
val eatSignal: Signal<EatSignal> = signal(EatSignal::class)
eatSignal.addListener { counter.incrementAndGet() }
eatSignal.addListener { counter.incrementAndGet() }

eatSignal.dispatcher.onEat()
Assert.assertEquals(2, counter.get())
}

@Test
fun addListenerWhileDispatching() {
val counter = AtomicInteger()
val eatSignal: Signal<EatSignal> = signal(EatSignal::class)
eatSignal.addListener {
counter.incrementAndGet()
eatSignal.addListener { counter.incrementAndGet() }
}

eatSignal.dispatcher.onEat()
Assert.assertEquals(2, foods.get().toLong())
Assert.assertEquals(2, counter.get())
}

@Test
fun testDispatchMultithreaded() {
val executorService = Executors.newSingleThreadExecutor()
val foods = AtomicInteger()
fun removeListenerWhileDispatching() {
for (i in 0..9) {
removeListenerWhileDispatching(i)
}
}

private fun removeListenerWhileDispatching(index: Int) {
val counter = AtomicInteger()
val eatSignal: Signal<EatSignal> = signal(EatSignal::class)

val listeners = ArrayList<EatSignal>()
for (i in 0..9) {
eatSignal.addListener { foods.incrementAndGet() }
if (i == 4) {
listeners.add {
counter.incrementAndGet()
eatSignal.removeListener(listeners[index])
}
} else {
listeners.add { counter.incrementAndGet() }
}
}

for (listener in listeners) {
eatSignal.addListener(listener)
}

eatSignal.dispatcher.onEat()
Assert.assertEquals(if (index <= 4) 10 else 9, counter.get())
}

@Test
fun dispatchAsync() {
val executorService = Executors.newFixedThreadPool(10)
val counter = AtomicInteger()
val eatSignal: Signal<EatSignal> = signal(EatSignal::class)
val random = Random(123)

for (i in 0..9) {
eatSignal.dispatcher.onEat()
// Add 10 anonymous class listeners
eatSignal.addListener {
counter.incrementAndGet()
Thread.sleep(random.nextLong(3))
}
}
Assert.assertEquals(100, foods.get().toLong())

for (i in 0..9) {
executorService.execute { eatSignal.dispatcher.onEat() }
}
executorService.shutdown()
executorService.awaitTermination(1, TimeUnit.HOURS)
executorService.awaitTermination(1, TimeUnit.MINUTES)

Assert.assertEquals(200, foods.get().toLong())
}

private fun interface EatSignal {
fun onEat()
Assert.assertEquals(100, counter.get())
}
}

0 comments on commit e47fbce

Please sign in to comment.