Parcelable最強解析

這兩天有個同事在使用泛型的過程當中,T extends BaseBean,對BaseBean類實現了parceable接口,當一個Activity中跳轉到另外一個Activity的時候,intent.putExtra("key",childBean),用到ChildBean對象,該類直接繼承了BaseBean,他以爲在另一個Acitivty拿不到ChildBean中的數據信息,甚至當他在用ChildBean=getIntent().getParcelableExtra()的時候出現了類型轉換錯誤,用BaseBean=getIntent().getParcelableExtra()確沒有問題,一時間對父類實現parcelable接口,子類是否有必要實現parcelable接口,而後傳值產生了爭議,相信也有很多同窗也有這樣的困惑,因此有了這篇文章 (這裏的Basebean和ChildBean是指父類和子類,正文的也是這個意思)java

1.Java serialization algorithm

答:當咱們對一個對象實現Serializable 接口的時候,它會告訴序列化機制這個類是能夠序列化的,java會經過文件流的形式,將object寫在一個文件file當中,android

public static void main(String args[]) throws IOException {
    FileOutputStream fos = new FileOutputStream("temp.out");
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    TestSerial ts = new TestSerial();
    oos.writeObject(ts);
    oos.flush();
    oos.close();
}

這裏咱們要注意ObjectOutputStream的構造對象,會寫如流的header,在這裏注意下code後面的註釋,由於在例子上面都要給對上的。c++

public ObjectOutputStream(OutputStream out) throws IOException {
        verifySubclass();
        ......
        writeStreamHeader();
        .....
    }
protected void writeStreamHeader() throws IOException {
        bout.writeShort(STREAM_MAGIC);//這裏寫入序列化協議
        bout.writeShort(STREAM_VERSION);//這裏寫入序列化的版本
    }
protected void writeStreamHeader() throws IOException {
        bout.writeShort(STREAM_MAGIC);
        bout.writeShort(STREAM_VERSION);
    }

具體的實現是在:緩存

private void writeObject0(Object obj, boolean unshared) throws IOException
    {
            ······
            else if (obj instanceof Serializable) {
                writeOrdinaryObject(obj, desc, unshared);
            } 
            ······
       ·····
    }

如上面代碼所示,剛開始的時候,對象是一個Serializable,因此會走writeOrdinaryObject(obj, desc, unshared);方法:ide

private void writeOrdinaryObject(Object obj,ObjectStreamClass desc,boolean unshared) throws IOException
    {
        ······
        try {
            desc.checkSerialize();

            bout.writeByte(TC_OBJECT);//這裏寫入TC_OBJECT
            writeClassDesc(desc, false);//接着寫classDesc
            ······
        } finally {
            if (extendedDebugInfo) {
                debugInfoStack.pop();
            }
        }
    }

上面再寫入TC_OBJECT以後,就調用writeClassDesc方法,在這裏我就不繼續分析了,由於文章的重點不該該在Serializable的分析上,接下來都是些java代碼的調用,也有源碼,若是你本身感興趣,相信大家也能夠隨便看看源代碼就能分析出來,在這裏我就不浪費你們的時間了,不過要提一下,寫的時候,是先寫自身類的描述,而後若是有父類就寫父類的描述,若是自身類包含的字段是一個對象,再寫該對象的描述,都寫完了,最後寫字段的數據。在這裏對一個類獲取裏面的字段,方法等是用到了反射機制

如下是一個對象寫入的例子,假設一個類是:this

class TestSerial implements Serializable {
    public byte version = 100;
}

如上一個對象所示,在寫入磁盤的時候,保存的數據以下:debug

AC ED (序列化協議)
00 05 (序列化版本)
73     (TC_OBJECT. 新的對象)
72     (TC_CLASSDESC. 這是一個新類描述) 
00 0A  (類名的長度)
53 65 72 69 61 6C 54 65 73 74 (類的名稱) 
05 52 81 5A AC 66 02 F6 (SerialVersionUID)
02     (Various flags,0x02表明這個對象支持序列化) 
00 01  (類有幾個字段) 
49     (表明是int類型)
00 07  (字段名稱的長度)
76 65 72 73 69 6F 6E (version, 字段的名稱)
78     (TC_ENDBLOCKDATA, 描述的結束符)
70     (TC_NULL)
00 00 00 64 (version的值)

從上面能夠看到serialiable的序列化和反序列化會創造大量的對象和寫入數據的時候,會寫入除去真實數據之外的其它數據,好比序列化協議,版本等等。指針

2.Parcable 機制的原理?

首先咱們在一個實體對象在實現parcelable的時候,這個時候,咱們會重寫writeToParcel方法,其中執行dest.writeInt(this.offLineBtn);writeLong等等類型的數據,實際是執行native方法,在這裏咱們就不分析各類數據類型的存取了,咱們如今拿一個表明int來分析下,看下jni方法:rest

static void android_os_Parcel_writeInt(JNIEnv* env, jclass clazz, jint nativePtr, jint val) {
    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
    const status_t err = parcel->writeInt32(val);
    if (err != NO_ERROR) {
        signalExceptionForError(env, clazz, err);
    }
}

在這裏咱們要特別注意兩個參數,一個是以前傳上去的指針以及須要保存的int數據,這兩個值分別是:
(jint nativePtr, jint val)
首先是根據這個指針,這裏說一下,指針實際上就是一個整型地址值,因此這裏使用強轉將int值轉化爲parcel類型的指針是可行的,而後使用這個指針來操做native的parcel對象,即:
const status_t err = parcel->writeInt32(val);code

writeInt32是調用了parcel中的方法,parcel的實現類是在Framework/native/libsbinderParcel.cpp,咱們看下writeInt32方法:

status_t Parcel::writeInt32(int32_t val)
{
    return writeAligned(val);
}
status_t Parcel::writeAligned(T val) {
    COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE_UNSAFE(sizeof(T)) == sizeof(T));

    if ((mDataPos+sizeof(val)) <= mDataCapacity) {
restart_write:
        *reinterpret_cast<T*>(mData+mDataPos) = val;
        return finishWrite(sizeof(val));
    }

    status_t err = growData(sizeof(val));
    if (err == NO_ERROR) goto restart_write;
    return err;
}

分析上面的以前,首先要知道mData、mDataPos、mDataCapacity三個變量的意義,mData指向parcel緩存的首地址,mDataCapacity表示parcel緩存容量(大小),mDataPos指向parcel緩存中空閒區域的首地址,整個parcel緩存是一塊連續的內存。

物理地址 = 有效地址+偏移地址,首先會判斷先寫入的int數據的字節數是否超過了data的容量,若是沒有超過,會執行數據的寫入,reinterpret_cast是c++的一種再解釋,強制轉換,上面首先會將mData+mDataPos獲得物理地址,轉成指向T類型的指針(T類型就是你傳進來的變量的類型),而後將val賦值給指針指向的內容。而後修改偏移地址,finishWrite(sizeof(val)):

status_t Parcel::finishWrite(size_t len)
{
    if (len > INT32_MAX) {
        // don't accept size_t values which may have come from an
        // inadvertent conversion from a negative int.
        return BAD_VALUE;
    }

    //printf("Finish write of %d\n", len);
    mDataPos += len;
    ALOGV("finishWrite Setting data pos of %p to %zu", this, mDataPos);
    if (mDataPos > mDataSize) {
        mDataSize = mDataPos;
        ALOGV("finishWrite Setting data size of %p to %zu", this, mDataSize);
    }
    //printf("New pos=%d, size=%d\n", mDataPos, mDataSize);
    return NO_ERROR;
}

上面主要是將修改偏移地址,將偏移地址加上新增長的數據的字節數。

若是增長的數據大於容量的話,那麼首先擴展parcel的緩存空間,growData(sizeof(val)):

status_t Parcel::growData(size_t len)
{
    if (len > INT32_MAX) {
        // don't accept size_t values which may have come from an
        // inadvertent conversion from a negative int.
        return BAD_VALUE;
    }

    size_t newSize = ((mDataSize+len)*3)/2;
    return (newSize <= mDataSize)
            ? (status_t) NO_MEMORY
            : continueWrite(newSize);
}

擴展成功,就繼續goto restart_write,在writeAligned方法中有restart_write,執行restart_write後面code,寫入數據。

經過上面的解釋相信你們已經明白int類型的數據寫入parcel緩存了,既然知道存數據,那咱們也要明白取數據了,在取數據的時候,咱們會經過this.age = in.readInt();來取得int類型數據

static jint android_os_Parcel_readInt(jlong nativePtr)
{
    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
    if (parcel != NULL) {
        return parcel->readInt32();
    }
    return 0;
}

調用的parcel的readInt32方法:

int32_t Parcel::readInt32() const
{
    return readAligned<int32_t>();
}
T Parcel::readAligned() const {
    T result;
    if (readAligned(&result) != NO_ERROR) {
        result = 0;
    }

    return result;
}
status_t Parcel::readAligned(T *pArg) const {
    COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE_UNSAFE(sizeof(T)) == sizeof(T));

    if ((mDataPos+sizeof(T)) <= mDataSize) {
        const void* data = mData+mDataPos;
        mDataPos += sizeof(T);
        *pArg =  *reinterpret_cast<const T*>(data);
        return NO_ERROR;
    } else {
        return NOT_ENOUGH_DATA;
    }
}

讀取數據的時候,首先咱們會從parcel的起始地址+parcel偏移地址,獲得讀取的數據的地址,而後取出數據,而後將parcel的偏移地址+取出的數據的字節數,這樣指針就能夠指向下一個數據,這樣說太抽象了,舉個例子:
好比咱們如今有一個對象,裏面是

stu{
    int age = 32;
    double score = 99;
}

咱們在寫數據的時候,會在一塊parcel的內存地址中,寫32,99,而後讀取的時候,會從起始地址+讀取的字節數,來一一讀取,首先讀取parcel起始地址指向的數據,取出32,而後將指針地址偏移int字節數,指針指向99的地址,而後讀取99,而後取出數據,這也就是parcelable在實現的時候爲何須要存和讀取的順序須要一致的緣由。

3.在咱們瞭解了,parcelable的實現原理的時候,咱們就能夠解答引言上面的問題了。

3.1 對BaseBean類實現了parceable接口,當一個Activity中跳轉到另外一個Activity的時候,intent.putExtra("key",childBean),另外一個Activity可否用拿到數據?

答:由於在父類的BaseBean裏面都有實現BaseBean中字段的讀寫,因此BaseBean中字段的數據是能夠拿到的。

3.2 在用ChildBean=getIntent().getParcelableExtra()的時候出現了類型轉換錯誤,用BaseBean=getIntent().getParcelableExtra()確沒有問題?

答:其實這裏是要看BaseBean中讀數據,返回的對象是什麼了?

public static final Parcelable.Creator<BaseBean> CREATOR = new Parcelable.Creator<BaseBean>() {
        @Override
        public BaseBean createFromParcel(Parcel source) {
            return new BaseBean(source);
        }

        @Override
        public BaseBean[] newArray(int size) {
            return new BaseBean[size];
        }
    };

很明顯,在這裏返回的BaseBean的對象,當你用ChildBean去接收的時候確定會出現類型轉換錯誤啦,若是還以爲想用ChildBean來接收的話(前提是有強迫症),能夠重寫createFromParcel方法

@Override
    public BaseBean createFromParcel(Parcel source) {
            ChildBean childBean = new ChildBean();
            childBean.setName(source.readString());
            childBean.setPrice(source.readDouble());
            return childBean;
        }

這不返回ChildBean不就能夠了,固然無論你是哪一種方式,若是childBean沒有實現parceable的話,對於childBean中的字段是沒法傳遞的.

attention:這個和Serializable的實現是不一樣的,Serializable是父類實現了Serializable,子類不須要實現Serializable,子類的數據也可以傳遞了,由於在寫入數據的判斷(obj instanceof Serializable),若是父類實現Serializable,子類確定也是instanceof Serializable。

3.3 若是咱們須要用到一個公共的界面,這個公共的界面多是經過泛型T t =getIntent().getParcelableExtra()來獲取數據的解決方案?

答:這裏咱們的BaseBean不該該是一個類,最合適的話,應該是一個interface,好比咱們公共界面是用到了t.getName()來獲得顯示的數據,這個時候

class ChildBean implements BaseBean,Parcelable{
    ...
    @Override
    public String getName(){
        return "WelliJohn";
    }
    ...
}

當用到了傳值的時候,ChildBean再自身實現了Parcelable接口,這樣代碼就完美了。這樣若是真的在公共界面有個特殊的類型的話,判斷下T的類型(ChildBean.class.isInstance(t)),強轉下也能夠進行某個特殊數據處理了。

4.總結

serialization parcable
文件操做,且用到了反射 單獨的內存空間,速度快
會創造大量的讀寫對象 直接操做內存讀寫
實現簡單 實現複雜,並且讀和取的數據要一致
寫入的時候,會有字段名,長度等 只是寫入數據,節省資源
由於寫在文件中,適合持久化數據 不適合持久化數據,可能會變化

若是大家有對3.3的解決方案感受有更好的處理思路的話,歡迎提出來共同探討

若是大家以爲文章對你有啓示做用,但願大家幫忙點個贊或者關注下,謝謝

相關文章
相關標籤/搜索