Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Parcelable with circular references

It appears that Parcelable doesn't gracefully handle circular references like Serializable does. In the following example, the Serialization of Bar works just fine, but writing it to a Parcel causes a stackoverflow:

I/TestRunner( 1571): java.lang.StackOverflowError
I/TestRunner( 1571):    at android.os.Parcel.writeParcelable(Parcel.java:1106)
I/TestRunner( 1571):    at android.os.Parcel.writeValue(Parcel.java:1029)
I/TestRunner( 1571):    at com.XXX.util.ParcelableTest$Bar.writeToParcel(ParcelableTest.java:209)
I/TestRunner( 1571):    at android.os.Parcel.writeParcelable(Parcel.java:1106)
I/TestRunner( 1571):    at android.os.Parcel.writeValue(Parcel.java:1029)
I/TestRunner( 1571):    at com.XXX.util.ParcelableTest$Baz.writeToParcel(ParcelableTest.java:246)
I/TestRunner( 1571):    at android.os.Parcel.writeParcelable(Parcel.java:1106)
I/TestRunner( 1571):    at android.os.Parcel.writeValue(Parcel.java:1029)
I/TestRunner( 1571):    at com.XXX.util.ParcelableTest$Bar.writeToParcel(ParcelableTest.java:209)
I/TestRunner( 1571):    at android.os.Parcel.writeParcelable(Parcel.java:1106)
I/TestRunner( 1571):    at android.os.Parcel.writeValue(Parcel.java:1029)


public void testCircular() throws Exception {

    final Bar bar = new Bar();
    final Baz baz = new Baz(bar);
    bar.baz = baz;

    // First, serialize
    final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
    new ObjectOutputStream(bytes).writeObject(bar);
    final ByteArrayInputStream bytesIn = new ByteArrayInputStream(bytes.toByteArray());
    final Bar bar2 = (Bar) new ObjectInputStream(bytesIn).readObject();

    assertNotNull(bar2);
    assertNotNull(bar2.baz);
    assertEquals( bar2, bar2.baz.bar );


    // Now try same thing using parcelable
    final Parcel p = Parcel.obtain();
    p.writeValue(bar); // FAIL!  StackOverflowError
    p.setDataPosition(0);
    final Bar bar3 = (Bar) p.readValue(Bar.class.getClassLoader());

    assertNotNull(bar3);
    assertNotNull(bar3.baz);
    assertEquals( bar3, bar3.baz.bar );

}


protected static class Bar implements Parcelable, Serializable {
    private static final long serialVersionUID = 1L;
    public static final Parcelable.Creator<Bar> CREATOR = new Parcelable.Creator<Bar>() {
        public Bar createFromParcel(Parcel source) {
            final Bar f = new Bar();
            f.baz = (Baz) source.readValue(Bar.class.getClassLoader());
            return f;
        }

        public Bar[] newArray(int size) {
            throw new UnsupportedOperationException();
        }

    };


    public Baz baz;

    public Bar() {
    }

    public Bar( Baz baz ) {
        this.baz = baz;
    }

    public int describeContents() {
        return 0;
    }

    public void writeToParcel(Parcel dest, int ignored) {
        dest.writeValue(baz);
    }


}


protected static class Baz implements Parcelable, Serializable {
    private static final long serialVersionUID = 1L;
    public static final Parcelable.Creator<Baz> CREATOR = new Parcelable.Creator<Baz>() {
        public Baz createFromParcel(Parcel source) {
            final Baz f = new Baz();
            f.bar = (Bar) source.readValue(Baz.class.getClassLoader());
            return f;
        }

        public Baz[] newArray(int size) {
            throw new UnsupportedOperationException();
        }

    };


    public Bar bar;

    public Baz() {
    }

    public Baz( Bar bar ) {
        this.bar = bar;
    }

    public int describeContents() {
        return 0;
    }

    public void writeToParcel(Parcel dest, int ignored) {
        dest.writeValue(bar);
    }


}

I'm trying to port some code over from using Serializable to Parcelable that uses circular references. Is there a good strategy for handling this with Parcelable?

like image 379
emmby Avatar asked Nov 11 '09 02:11

emmby


1 Answers

Perhaps the answer lies in a more intelligent set of writeToParcel and createFromParcel methods?

Off the top of my head, you could keep a list of objects you had already fully written to a given Parcel and identify them only by a tag (their local identityHashCode(), perhaps). (Note that this is not a global list, it is explicitly per-Parcel; perhaps itself stored via a semi-global Map<Parcel,Set<Integer> > ? You'd need to be sure the set was forgotten once the parcel was fully written.)

The relevant bit of writeToParcel() would look something like this:

HashSet<Integer> set = getWrittenSetFor(dest);
final int tag = identityHashCode();
if (set.contains(tag)) {
    // Already sent
    dest.writeInt(tag);
} else {
    set.put(tag);
    dest.writeInt(tag);
    dest.writeValue(this);
}

The corresponding createFromParcel() would be slightly more complex.

I expect there are lurking problems with this method, but it's where I'd start. As I've put it here, it depends on identityHashCode() being guaranteed to be different for different objects - it usually is on 32-bit JVMs (being the value of the underlying C++ pointer). Plain hashCode() might be worthwhile (perhaps with the addition of typing information?), or perhaps some sort of serial number.

Another option might be to just plain serialize your objects to a byte[] and write that into the Parcel, but it strikes me as a bit inefficient...

like image 180
crazyscot Avatar answered Sep 29 '22 19:09

crazyscot