Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NotSerializableException when pass kotlin function in bundle on saving instance state

Tags:

android

kotlin

After I pass kotlin function into Bundle in onSaveInstanceState I got NotSerializableException:

java.lang.RuntimeException: Parcelable encountered IOException writing serializable object (name = MyActivity$showFragmentA$1)
   at android.os.Parcel.writeSerializable(Parcel.java:1447)
   at android.os.Parcel.writeValue(Parcel.java:1395)
   at android.os.Parcel.writeArrayMapInternal(Parcel.java:665)
   at android.os.BaseBundle.writeToParcelInner(BaseBundle.java:1330)
   at android.os.Bundle.writeToParcel(Bundle.java:1079)
   at android.os.Parcel.writeBundle(Parcel.java:690)
   at android.app.ActivityManagerProxy.activityStopped(ActivityManagerNative.java:3269)
   at android.app.ActivityThread$StopInfo.run(ActivityThread.java:3632)
   at android.os.Handler.handleCallback(Handler.java:815)
   at android.os.Handler.dispatchMessage(Handler.java:104)
   at android.os.Looper.loop(Looper.java:207)
   at android.app.ActivityThread.main(ActivityThread.java:5728)
   at java.lang.reflect.Method.invoke(Native Method)
   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:789)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:679)
Caused by: java.io.NotSerializableException: MyActivity
   at java.io.ObjectOutputStream.writeNewObject(ObjectOutputStream.java:1344)
   at java.io.ObjectOutputStream.writeObjectInternal(ObjectOutputStream.java:1651)
   at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:1497)
   at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:1461)
   at java.io.ObjectOutputStream.writeFieldValues(ObjectOutputStream.java:959)
   at java.io.ObjectOutputStream.defaultWriteObject(ObjectOutputStream.java:360)
   at java.io.ObjectOutputStream.writeHierarchy(ObjectOutputStream.java:1054)
   at java.io.ObjectOutputStream.writeNewObject(ObjectOutputStream.java:1384)
   at java.io.ObjectOutputStream.writeObjectInternal(ObjectOutputStream.java:1651)
   at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:1497)
   at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:1461)
   at android.os.Parcel.writeSerializable(Parcel.java:1442)
   at android.os.Parcel.writeValue(Parcel.java:1395) 
   at android.os.Parcel.writeArrayMapInternal(Parcel.java:665) 
   at android.os.BaseBundle.writeToParcelInner(BaseBundle.java:1330) 
   at android.os.Bundle.writeToParcel(Bundle.java:1079) 
   at android.os.Parcel.writeBundle(Parcel.java:690) 
   at android.app.ActivityManagerProxy.activityStopped(ActivityManagerNative.java:3269) 
   at android.app.ActivityThread$StopInfo.run(ActivityThread.java:3632) 
   at android.os.Handler.handleCallback(Handler.java:815) 
   at android.os.Handler.dispatchMessage(Handler.java:104) 
   at android.os.Looper.loop(Looper.java:207) 
   at android.app.ActivityThread.main(ActivityThread.java:5728) 
   at java.lang.reflect.Method.invoke(Native Method) 
   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:789) 
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:679)

My class:

class MyActivity : Activity {
    private var lastFragment: (() -> Fragment)? = null

    override fun onSaveInstanceState(outState: Bundle?) {
        super.onSaveInstanceState(outState)
         outState?.putSerializable("lastFragment", lastFragment as Serializable)
    }
    
    fun showFragmentA() {
        lastFragment = { FragmentA() }
        // show fragment lastFragment()
    }
    
    fun showFragmentB() {
        ...
    }
}
like image 678
Alexey Nikitin Avatar asked Dec 10 '22 07:12

Alexey Nikitin


1 Answers

The problem is this. You create a lambda and set it as the value of your function lastFragment. But what is the lambda? It is a custom class created by the compiler within the MyActivity class. And it is an inner class, therefore it has a pointer to an instance of MyActivity which is not serializable. Therefore your instance of the function, the lambda class, has a reference to something that breaks it from being serialized. Look at this:

class MyClass {
    var foo: (()->Unit)? = null

    fun makeProblem() {
        foo = { println("hi") }
    }
}

This created MyClass$makeProblem$1 inner class of MyClass to hold the body of my lambda { println("hi") } ... and all inner classes have pointers to their containing class, so MyClass$makeProblem$1 has a variable of type MyClass that you cannot see but is obviously there because that allows the code in the lambda to access members of the containing class. Then boom, this breaks serialization.

Libraries that plan to serialize lambdas know about this and special case cutting of this link assuming there is no use of this inner class reference. Apache Spark does this, by basically using introspection to find a specific hidden field and setting it to null. I have a Kotlin example of this somewhere, but it is fragile in case the internals ever changed.

You can also declare your lambda outside of any class to avoid it from being an inner class. Or make sure the containing class is also serializable. Or wrap it with a static class that is serializable. One of these might work, or not, depending on what you want to happen when the lambda (and class) is deserialized later.

If you look at the bytecode generated, you can see this lambda is clearly an inner class:

// ================uy/sotest/MyClass.class =================
// class version 50.0 (50)
// access flags 0x31
public final class uy/sotest/MyClass {


  // access flags 0x2
  // signature Lkotlin/jvm/functions/Function0<Lkotlin/Unit;>;
  // declaration: kotlin.jvm.functions.Function0<kotlin.Unit>
  private Lkotlin/jvm/functions/Function0; foo
  @Lorg/jetbrains/annotations/Nullable;() // invisible

  // access flags 0x11
  // signature ()Lkotlin/jvm/functions/Function0<Lkotlin/Unit;>;
  // declaration: kotlin.jvm.functions.Function0<kotlin.Unit> getFoo()
  public final getFoo()Lkotlin/jvm/functions/Function0;
  @Lorg/jetbrains/annotations/Nullable;() // invisible
   ...

  // access flags 0x11
  // signature (Lkotlin/jvm/functions/Function0<Lkotlin/Unit;>;)V
  // declaration: void setFoo(kotlin.jvm.functions.Function0<kotlin.Unit>)
  public final setFoo(Lkotlin/jvm/functions/Function0;)V
    @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0
   ...

  // access flags 0x11
  public final makeProblem()V
   L0
    LINENUMBER 7 L0
    ALOAD 0
    GETSTATIC uy/sotest/MyClass$makeProblem$1.INSTANCE : Luy/sotest/MyClass$makeProblem$1;
    CHECKCAST kotlin/jvm/functions/Function0
    PUTFIELD uy/sotest/MyClass.foo : Lkotlin/jvm/functions/Function0;
   L1
    LINENUMBER 8 L1
    RETURN
   L2
    LOCALVARIABLE this Luy/sotest/MyClass; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x1
  public <init>()V
   ...

  @Lkotlin/Metadata;( ... )
  // access flags 0x18
  final static INNERCLASS uy/sotest/MyClass$makeProblem$1 null null
  // compiled from: ShowThing.kt
}


// ================uy/sotest/MyClass$makeProblem$1.class =================
// class version 50.0 (50)
// access flags 0x30
// signature Lkotlin/jvm/internal/Lambda;Lkotlin/jvm/functions/Function0<Lkotlin/Unit;>;
// declaration: uy/sotest/MyClass$makeProblem$1 extends kotlin.jvm.internal.Lambda implements kotlin.jvm.functions.Function0<kotlin.Unit>
final class uy/sotest/MyClass$makeProblem$1 extends kotlin/jvm/internal/Lambda  implements kotlin/jvm/functions/Function0  {


  // access flags 0x1041
  public synthetic bridge invoke()Ljava/lang/Object;
    ...

  // access flags 0x11
  public final invoke()V
    ...

  // access flags 0x0
  <init>()V
    ...

  // access flags 0x19
  public final static Luy/sotest/MyClass$makeProblem$1; INSTANCE

  // access flags 0x8
  static <clinit>()V
    ...

  @Lkotlin/Metadata;( ... )
  OUTERCLASS uy/sotest/MyClass makeProblem ()V
  // access flags 0x18
  final static INNERCLASS uy/sotest/MyClass$makeProblem$1 null null
     ...

}
like image 51
3 revs Avatar answered Mar 02 '23 01:03

3 revs