Java: Deep understanding of Serializable and arcelable

Posted by socadmin on Thu, 03 Feb 2022 18:22:18 +0100

Preface

This article is the author's record of learning Serializable and arcelable to explore his own questions

I. Problems Caused by the Author

Why does Java implement Serializable objects to realize that they support serialization?
Why is Serializable less efficient than Parcelable?

2. Interpretation of Source Code

Serializable correlation

As we all know, Serializable is an empty interface. Once the interface is implemented by a serialized and deserialized class, it is serialized and deserialized mainly through ObjectInputStream and ObjectOutputStream. If Serializable or other related interfaces are not implemented, errors will occur.
The source code for the Serializable is almost empty, so let's start with the ObjectInputStream and ObjectOutputStream sources.
First look at the source code for ObjectOutputStream

Paste the code used first for easy follow-up reading

	try {
            //Output serialization
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);
            //Input Deserialization
            ByteArrayInputStream bais = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            person = (Person) ois.readObject();
            return person;
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }

Source Entry:

/**
     * Creates an ObjectOutputStream that writes to the specified OutputStream.
     * This constructor writes the serialization stream header to the
     * underlying stream; callers may wish to flush the stream immediately to
     * ensure that constructors for receiving ObjectInputStreams will not block
     * when reading the header.
     *
     * <p>If a security manager is installed, this constructor will check for
     * the "enableSubclassImplementation" SerializablePermission when invoked
     * directly or indirectly by the constructor of a subclass which overrides
     * the ObjectOutputStream.putFields or ObjectOutputStream.writeUnshared
     * methods.
     *
     * @param   out output stream to write to
     * @throws  IOException if an I/O error occurs while writing stream header
     * @throws  SecurityException if untrusted subclass illegally overrides
     *          security-sensitive methods
     * @throws  NullPointerException if {@code out} is {@code null}
     * @since   1.4
     * @see     ObjectOutputStream#ObjectOutputStream()
     * @see     ObjectOutputStream#putFields()
     * @see     ObjectInputStream#ObjectInputStream(InputStream)
     */
    public ObjectOutputStream(OutputStream out) throws IOException {
        verifySubclass();
          // bout represents the underlying byte data container
        bout = new BlockDataOutputStream(out);
        handles = new HandleTable(10, (float) 3.00);
        subs = new ReplaceTable(10, (float) 3.00);
        enableOverride = false;
        writeStreamHeader(); // Write header
        bout.setBlockDataMode(true);// flush data
        if (extendedDebugInfo) {
            debugInfoStack = new DebugTraceInfoStack();
        } else {
            debugInfoStack = null;
        }
    }

Complement: The common ByteArrayOutputStream and FileOutputStream inherit from OutputStream, so the source of the constructor that takes the first two streams as parameters is shown above.

This constructor first binds the out object to the bout

/**
         * Creates new BlockDataOutputStream on top of given underlying stream.
         * Block data mode is turned off by default.
         */
        BlockDataOutputStream(OutputStream out) {
            this.out = out;
            dout = new DataOutputStream(this);
        }

Then call the writeStreamHeader() method

	/**
     * The writeStreamHeader method is provided so subclasses can append or
     * prepend their own header to the stream.  It writes the magic number and
     * version to the stream.
     *
     * @throws  IOException if I/O errors occur while writing to the underlying
     *          stream
     */
    protected void writeStreamHeader() throws IOException {
        bout.writeShort(STREAM_MAGIC);
        bout.writeShort(STREAM_VERSION);
    }

		/**
	 * Magic number that is written to the stream header. 
	 */
	final static short STREAM_MAGIC = (short)0xaced;
	
	/**
	 * Version number that is written to the stream header. 
	 */
	final static short STREAM_VERSION = 5;

Next, call writeObject()

public final void writeObject(Object obj) throws IOException {
        if (enableOverride) {
            writeObjectOverride(obj);
            return;
        }
        try {
            writeObject0(obj, false);
        } catch (IOException ex) {
            if (depth == 0) {
                writeFatalException(ex);
            }
            throw ex;
        }
    }

It's obvious that enableOverride was set to false when initialized in our previous code, so the writeObject0 method is called here, so let's move on

	/**
     * Underlying writeObject/writeUnshared implementation.
     */
    private void writeObject0(Object obj, boolean unshared)
        throws IOException
    {
        boolean oldMode = bout.setBlockDataMode(false);
        depth++;
        try {
            // handle previously written and non-replaceable objects
            int h;
            if ((obj = subs.lookup(obj)) == null) {
                writeNull();
                return;
            } else if (!unshared && (h = handles.lookup(obj)) != -1) {
                writeHandle(h);
                return;
            } else if (obj instanceof Class) {
                writeClass((Class) obj, unshared);
                return;
            } else if (obj instanceof ObjectStreamClass) {
                writeClassDesc((ObjectStreamClass) obj, unshared);
                return;
            }

            // check for replacement object
            Object orig = obj;
            //Get the object to serialize
            Class<?> cl = obj.getClass();
            ObjectStreamClass desc;
            for (;;) {
                // REMIND: skip this check for strings/arrays?
                //Verify if there is a rewrite method
                Class<?> repCl;
                desc = ObjectStreamClass.lookup(cl, true);
                if (!desc.hasWriteReplaceMethod() ||
                    (obj = desc.invokeWriteReplace(obj)) == null ||
                    (repCl = obj.getClass()) == cl)
                {
                    break;
                }
                cl = repCl;
            }
            if (enableReplace) {
                Object rep = replaceObject(obj);
                if (rep != obj && rep != null) {
                    cl = rep.getClass();
                    desc = ObjectStreamClass.lookup(cl, true);
                }
                obj = rep;
            }

            // if object replaced, run through original checks a second time
            if (obj != orig) {
                subs.assign(orig, obj);
                if (obj == null) {
                    writeNull();
                    return;
                } else if (!unshared && (h = handles.lookup(obj)) != -1) {
                    writeHandle(h);
                    return;
                } else if (obj instanceof Class) {
                    writeClass((Class) obj, unshared);
                    return;
                } else if (obj instanceof ObjectStreamClass) {
                    writeClassDesc((ObjectStreamClass) obj, unshared);
                    return;
                }
            }

            // remaining cases
            if (obj instanceof String) {
                writeString((String) obj, unshared);
            } else if (cl.isArray()) {
                writeArray(obj, desc, unshared);
            } else if (obj instanceof Enum) {
                writeEnum((Enum<?>) obj, desc, unshared);
            } else if (obj instanceof Serializable) {
            	//Classes that implement the Serializable interface do the following
                writeOrdinaryObject(obj, desc, unshared);
            } else {
                if (extendedDebugInfo) {
                    throw new NotSerializableException(
                        cl.getName() + "\n" + debugInfoStack.toString());
                } else {
                    throw new NotSerializableException(cl.getName());
                }
            }
        } finally {
            depth--;
            bout.setBlockDataMode(oldMode);
        }
    }

Whoops, it's all about seeing the hero of our analysis, and here we can see that if you don't implement the Serializable method, you won't call the writeOrdinaryObject method, you'll throw an exception down
Next, take a look at the writeOrdinaryObject() method

/**
     * Writes representation of an "ordinary" (i.e., not a String, Class,
     * ObjectStreamClass, array, or enum constant) serializable object to the
     * stream.
     */
    private void writeOrdinaryObject(Object obj,
                                     ObjectStreamClass desc,
                                     boolean unshared)
        throws IOException
    {
        if (extendedDebugInfo) {
            debugInfoStack.push(
                (depth == 1 ? "root " : "") + "object (class \"" +
                obj.getClass().getName() + "\", " + obj.toString() + ")");
        }
        try {
        	// Write Object Flag Bits
            desc.checkSerialize();

            bout.writeByte(TC_OBJECT);
            // Write Class Metadata
            writeClassDesc(desc, false);
            handles.assign(unshared ? null : obj);

            if (desc.isRecord()) {
                writeRecordData(obj, desc);
            } else if (desc.isExternalizable() && !desc.isProxy()) {
                writeExternalData((Externalizable) obj);// Write instance data of the serialized object
            } else {
                writeSerialData(obj, desc);
            }
        } finally {
            if (extendedDebugInfo) {
                debugInfoStack.pop();
            }
        }
    }

	/**
	 * new Object.
	 */
	final static byte TC_OBJECT =       (byte)0x73;

Next, the writeClassDesc() method is called to write the class metadata of the class being serialized, and the writeClassDesc() method is implemented as follows:

private void writeClassDesc(ObjectStreamClass desc, boolean unshared)
    throws IOException
{
    int handle;
    if (desc == null) {
        // If desc is null
        writeNull();
    } else if (!unshared && (handle = handles.lookup(desc)) != -1) {
        writeHandle(handle);
    } else if (desc.isProxy()) {
        writeProxyDesc(desc, unshared);
    } else {
        writeNonProxyDesc(desc, unshared);
    }
}

In general, the writeNonProxyDesc() method is then called, which implements the following:

private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared)
    throws IOException
{
    // TC_CLASSDESC =    (byte)0x72;
    // Represents a new Class descriptor
    bout.writeByte(TC_CLASSDESC);
    handles.assign(unshared ? null : desc);
 
    if (protocol == PROTOCOL_VERSION_1) {
        // do not invoke class descriptor write hook with old protocol
        desc.writeNonProxy(this);
    } else {
        writeClassDescriptor(desc);
    }
 
    Class cl = desc.forClass();
    bout.setBlockDataMode(true);
    if (cl != null && isCustomSubclass()) {
        ReflectUtil.checkPackageAccess(cl);
    }
    annotateClass(cl);
    bout.setBlockDataMode(false);
    bout.writeByte(TC_ENDBLOCKDATA);
 
    writeClassDesc(desc.getSuperDesc(), false);
}

WteNonProxyDesc calls writeNonProxy() and finally writes the data

void writeNonProxy(ObjectOutputStream out) throws IOException {
    out.writeUTF(name); // Write the name of the class
    out.writeLong(getSerialVersionUID()); // Write Serial Number of Class
 
    byte flags = 0;
    // Get the identity of the class
    if (externalizable) {
        flags |= ObjectStreamConstants.SC_EXTERNALIZABLE;
        int protocol = out.getProtocolVersion();
        if (protocol != ObjectStreamConstants.PROTOCOL_VERSION_1) {
            flags |= ObjectStreamConstants.SC_BLOCK_DATA;
        }
    } else if (serializable) {
        flags |= ObjectStreamConstants.SC_SERIALIZABLE;
    }
    if (hasWriteObjectData) {
        flags |= ObjectStreamConstants.SC_WRITE_METHOD;
    }
    if (isEnum) {
        flags |= ObjectStreamConstants.SC_ENUM;
    }
    out.writeByte(flags); // Write flag to class
 
    out.writeShort(fields.length); // Number of fields written to the object
    for (int i = 0; i < fields.length; i++) {
        ObjectStreamField f = fields[i];
        out.writeByte(f.getTypeCode());
        out.writeUTF(f.getName());
        if (!f.isPrimitive()) {
            // If it's not the original type, it's an object or an Interface
            // Writes a type string that represents an object or class
            out.writeTypeString(f.getTypeString());
        }
    }
}

From here we can also see that the work of Serilizable is almost done in Java layer, the input and output are also in the form of streams, resulting in a large number of I/O operations, a large number of object generation and elimination in the process, which will inevitably cause the GC mechanism to reduce efficiency, and the use of reflection mechanism, which also reduces its efficiency to a certain extent.

Parcelable correlation

Parcelable is related to Serializable, but it operates on continuous memory
Parcelable is strictly an API in the Android SDK, and its serialization is based on the Native layer, which may be implemented differently in different versions of the API
Let's start with Parcelable's source code

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package android.os;

public interface Parcelable {
    int CONTENTS_FILE_DESCRIPTOR = 1;
    int PARCELABLE_WRITE_RETURN_VALUE = 1;

    int describeContents();

    void writeToParcel(Parcel var1, int var2);

    public interface Creator<T> {
        T createFromParcel(Parcel var1);

        T[] newArray(int var1);
    }

    public interface ClassLoaderCreator<T> extends Parcelable.Creator<T> {
        T createFromParcel(Parcel var1, ClassLoader var2);
    }
}

Then we found that two of the key functions actually have Parcel. It seems that a lot of work is done by Parcel. Parcelable is just a shell, so we continue to look at Parcel. Here we look at a writeInt method

/**
     * Write an integer value into the parcel at the current dataPosition(),
     * growing dataCapacity() if needed.
     */
    public final void writeInt(int val) {
        int err = nativeWriteInt(mNativePtr, val);
        if (err != OK) {
            nativeSignalExceptionForError(err);
        }
    }
	
	@CriticalNative
    private static native int nativeWriteInt(long nativePtr, int val);

Discovery is a Native method, okk, look at the Native layer
The author located android_os_Parcel.cpp, excuse me for not finding a specific C implementation, here's a big blog for ideas

static void android_os_Parcel_writeInt(JNIEnv* env, jclass clazz, jlong nativePtr, jint val) {
    //Reinterpret Strong Parcel Objects by Pointer
    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
    if (parcel != NULL) {
        //Finally, the writeInt32 function in arcel is called
        const status_t err = parcel->writeInt32(val);
        if (err != NO_ERROR) {
            signalExceptionForError(env, clazz, err);
        }
    }
}

...

//The actual call is a generic template method
status_t Parcel::writeInt32(int32_t val)
{
    return writeAligned(val);
}

...

//template method
//among
//mData represents the first address to Parcel memory
//mDataPos represents the first address to Parcel's free memory
//mDataCapacity represents the size of memory allocated by Parcel
template<class T>
status_t Parcel::writeAligned(T val) {
    COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE(sizeof(T)) == sizeof(T));
    //Determine if adding val ue will exceed available size
    if ((mDataPos+sizeof(val)) <= mDataCapacity) {
restart_write:
        //reinterpret_cast is a c++ reinterpretation cast operation
        //First calculate the mData + mDataPos to get the physical address, and convert to a pointer to the T type (the T type is the actual type of input)
        //Then assign val ue to what the pointer points to
        *reinterpret_cast<T*>(mData+mDataPos) = val;
        //The main logic is to modify the offset address of mDataPos
        //Add the offset address to the number of bytes of the newly added data
        return finishWrite(sizeof(val));
    }
    //Execute the growth function if it exceeds the available size
    //Then go to restart_above Write tag performs write logic
    status_t err = growData(sizeof(val));
    if (err == NO_ERROR) goto restart_write;
    return err;
}

With the above code analysis, the writeInt32 function calls a template method and writes the data to a shared memory
Reading process

template<class T>
status_t Parcel::readAligned(T *pArg) const {
    COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE(sizeof(T)) == sizeof(T));
    if ((mDataPos+sizeof(T)) <= mDataSize) {
        //Get the address to read the data
        const void* data = mData+mDataPos;
        //mDataPos points to the next data
        mDataPos += sizeof(T);
        //Remove data based on data pointer type
        *pArg =  *reinterpret_cast<const T*>(data);
        return NO_ERROR;
    } else {
        return NOT_ENOUGH_DATA;
    }
}

Read-write start addresses must be consistent, which is why we want to keep member variables of read-write classes consistent
Because the method of writing dynamic memory is used, if you want to ensure data persistence and avoid unpredictable errors due to unexpected outages (like power outages), try to use Serializable instead.
However, because Parcelable is read and write to memory directly, and uses pointer operations directly, it is much faster in efficiency and can also be used when focusing on efficiency, such as communication of small data between processes.

summary

The questions I asked before have been explained in the source answer section, so let's not repeat them here. Feel like the blog can be appreciated by your friends.

Reference Article
Android serialization (Serializable and arcelable)
Principle Analysis of Android Parcelable
Differences between Serializable and arcelable (Android Daily Interview Questions)
Java Object Serialization Bottom Principle Source Parsing WhatHowWhyOther

Topics: Java Back-end source code