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