Android Handler callback not removed for token type Int or Long (*Kotlin)
Asked Answered
M

1

2

I have this code executed in Kotlin android project and it will log both messages. If I change the token to Char or String it will print only one message which is the wanted behaviour. Same use-case in a java project in android works as it should.

    val handler = Handler()
    //val token1: Long = 1001L
    //val token2: Int = 121
    val token1: Long = 1001L
    val token2: Int = 1002

    handler.postAtTime(
        {
            Log.e("postAtTime 1", " printed 1 ")
            handler.removeCallbacksAndMessages(token2)
        },
        token1,
        SystemClock.uptimeMillis() + 2000
    )

    handler.postAtTime(
        {
            Log.e("postAtTime 2", " printed 2 ")
        },
        token2,
        SystemClock.uptimeMillis() + 4000
    )

My question is why in Kotlin for a token of type Int, Long the handler doesnt remove the callback?

EDIT If I try with commented values it works

Misestimate answered 16/6, 2020 at 9:58 Comment(7)
I try this code and it prints only onePolychrome
maybe its related to my machine, but I tried with other colleagues and they had same issueMisestimate
As I guest, Long and Int is primative type in Kotlin when to JVM code, it mean when you call removeCallbacksAndMessages with Object, it will be auto-boxing and create new object in case Int and Long. In Kotlin I recommend you using String or Long? Int? it's object type.Polychrome
what about char, in java its primitive right but here behaves differently from int?Misestimate
It prints once for me too.Villanelle
ok sorry for the confusion for the values you tested it also worked for me but try now with new values and have a look, i have tried different use-casesMisestimate
did any of you tried the new values if you can reproduce my issue? @VillanelleMisestimate
I
5

The code in MessageQueue (which is handling the message deletion) is doing this:

while (p != null && p.target == h
                    && (object == null || p.obj == object)) {

// clearing code
}

where p is a message in the queue, p.obj is the token associated with it, and object is the optional token you've passed in to clear messages for. So if you have passed in a token, and it matches the token of the current message, the message gets cleared.

The problem is it uses referential equality to compare tokens - if they're not exactly the same object, if you're not passing in the same token instance you posted the message with, it doesn't match and nothing happens.


When you declare token2 as an Int, which is Kotlin's own "kind of a primitive", and then pass it into a method that requires an actual object, it gets boxed into an Integer. And you do that twice - once to post a message with a token, once to clear messages with a token. It creates a different (non-referentially equal) object each time.

You can test this by storing the token objects and comparing them:

val handler = Handler()
//val token1: Long = 1001L
//val token2: Int = 121
val token1: Long = 1001L
val token2: Int = 1002

var postedToken: Any? = null
var cancelledToken: Any? = null

fun postIt(r: ()->Unit, token: Any, time: Long): Any {
    handler.postAtTime(r, token, time)
    return token
}

fun cancelIt(token: Any): Any {
    handler.removeCallbacksAndMessages(token)
    return token
}

postIt(
    {
        Log.e("postAtTime 1", " printed 1 ")
        cancelledToken = cancelIt(token2)
        // referential equality, triple-equals!
        Log.e("Comparing", "Posted === cancelled: ${postedToken === cancelledToken}")
    },
    token1,
    SystemClock.uptimeMillis() + 2000
)

postedToken = postIt(
    {
        Log.e("postAtTime 2", " printed 2 ")
    },
    token2,
    SystemClock.uptimeMillis() + 4000
)
E/Comparing: Posted === cancelled: false

As for why it works with an Int of 121, I'm assuming it's down to Java's integer cache. Under the hood, the Kotlin code (if you do Show Bytecode and then decompile it) is calling Integer.valueOf(token2). Here's what the docs say about it:

Returns an Integer instance representing the specified int value. If a new Integer instance is not required, this method should generally be used in preference to the constructor Integer(int), as this method is likely to yield significantly better space and time performance by caching frequently requested values. This method will always cache values in the range -128 to 127, inclusive, and may cache other values outside of this range.

So calling Integer(number) will always create a new object, valueOf(number) might create one, or it might return an Integer object it created earlier. A value of 121 will always return the same object as before, which is why you're getting referential equality with that one, so the tokens match. For the larger number, you're getting different objects (you can check their IDs in the debugger)


But why does it work in Java and not Kotlin? I didn't test with Java, but it's possible the cache is working differently, maybe the compiler is able to be smarter about reusing the same object for an int variable outside of the "definitely cached" range. Or if you're defining your token in your Java code as an Integer instead of an int then you're creating one object and passing it both times, so that will always match.

Anyhow uh that's a lot of background to try and help you work out why it's breaking! The short version is don't do that, don't let it autobox things, create a token object and keep a reference to it so you can pass the same instance again later ;)

(This goes for Strings too - Java has a String pool where it reuses the same object if you declare a string literal twice, but it might not, so it's safer to assign a String to a variable, and then you know it's always the same object)

Isabelleisac answered 25/8, 2020 at 3:13 Comment(2)
Thanks for the answer, I tried Int because of this answer https://mcmap.net/q/110771/-how-to-remove-a-runnable-from-a-handler-object-added-by-postdelayed, but I was wrongMisestimate
Yeah, it's that whole autoboxing thing which might or might not work depending on whether the JVM likes you ;) The best thing to do when you need a token of some kind is to always create an object yourself (whatever type you like, just so long as it's an Object like an Integer and not a primitive) and then hold on to it so you can pass the exact same object again later. It's the object itself that's the key, not the contents, so you need to keep hold of it!Isabelleisac

© 2022 - 2024 — McMap. All rights reserved.