序列化原理(二):從源碼理解Parcelable

前言

上一篇我們研究了一下Serializable,雖然它使用方便,但是效率和兼容性上確實還存在一些問題。爲此Android提供了特有的Parcelable機制,按照官方說法,速度是Serializable的十倍左右。

正文

public class TestBean implements Parcelable {

    private int x;

    private int y;

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    public static final Parcelable.Creator<TestBean> CREATOR = new Creator<TestBean>() {
        @Override
        public TestBean[] newArray(int size) {
            return new TestBean[size];
        }

        @Override
        public TestBean createFromParcel(Parcel in) {
            TestBean bean = new TestBean();
            bean.setX(in.readInt());
            bean.setY(in.readInt());
            return bean;
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(x);
        dest.writeInt(y);
    }
}

上面是一個Parcelable的示例,對比Serializable,可以看出有以下優劣:

  • Serializable用法簡單,Parcelable實現要相對複雜
  • Serializable只能序列化所有屬性,Parcelable可以在write和read方法選擇性的序列化
  • Serializable具有可繼承性,Parcelable雖然也具有,但是仍然需要完善實現,因爲CREATOR是靜態的。

通過用法我們已經對兩者的區別有了一些認識,接下來我們看看Parcelable的源碼工作原理。 提到跨進程通信,AIDL是常見的解決方案之一。

如果你對AIDL的使用還不夠了解,可以先閱讀我之前學過的博客:
AIDL使用學習(一):基礎使用學習
AIDL使用學習(二):跨進程回調以及RemoteCallbackList
AIDL使用學習(三):源碼深入分析

我們就看生成的文件ITestInterface:

public interface ITestInterface extends android.os.IInterface {
    
    public static abstract class Stub extends android.os.Binder implements com.lzp.aidlstudy.ITestInterface {
        private static final java.lang.String DESCRIPTOR = "com.lzp.aidlstudy.ITestInterface";
        ...
       
        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            java.lang.String descriptor = DESCRIPTOR;
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(descriptor);
                    return true;
                }
                case TRANSACTION_getCalculateResult: {
                    data.enforceInterface(descriptor);
                    com.lzp.aidlstudy.bean.TestBean _arg0;
                    if ((0 != data.readInt())) {
                        _arg0 = com.lzp.aidlstudy.bean.TestBean.CREATOR.createFromParcel(data);
                    } else {
                        _arg0 = null;
                    }
                    int _result = this.getCalculateResult(_arg0);
                    reply.writeNoException();
                    reply.writeInt(_result);
                    return true;
                }
                default: {
                    return super.onTransact(code, data, reply, flags);
                }
            }
        }

        private static class Proxy implements com.lzp.aidlstudy.ITestInterface {
            ...
            @Override
            public int getCalculateResult(com.lzp.aidlstudy.bean.TestBean bean) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                int _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    if ((bean != null)) {
                        _data.writeInt(1);
                        bean.writeToParcel(_data, 0);
                    } else {
                        _data.writeInt(0);
                    }
                    mRemote.transact(Stub.TRANSACTION_getCalculateResult, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readInt();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
        }

}

我們簡化了部分與分析無關的代碼,從上面的代碼我們已經看到了Parcelable熟悉的身影,首先我們得到的代理對象Proxy,通過Proxy對象調用getCalculateResult()方法:

// 如果參數TestBean不等於null
if ((bean != null)) {
         _data.writeInt(1);
         // 調用writeToParcel把要序列化的數據寫入到某處
        bean.writeToParcel(_data, 0);
} else {
		// 參數等於null,就寫個0
       _data.writeInt(0);
}

我們先不管序列化的數據到底寫到哪去了,反正是保存起來了,接着調用:

 mRemote.transact(Stub.TRANSACTION_getCalculateResult, _data, _reply, 0);

mRemote就是Stub類的實例,通過Binder的源碼,在transact調用了onTransact:

 public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
        ......
        // 如果參數不是空的,通過反序列化得到參數
        if ((0 != data.readInt())) {
                _arg0 = com.lzp.aidlstudy.bean.TestBean.CREATOR.createFromParcel(data);
        } else {
                _arg0 = null;
        }
        // 本地計算結果,再次返回
        int _result = this.getCalculateResult(_arg0);
        reply.writeNoException();
        reply.writeInt(_result);
        return true;
        }
		......
     }
}

過程就是這麼簡單,現在我們唯一的困惑是:

這些序列化的數據到底寫到哪去了呢

Parcelable只是一個接口,具體的序列化原理是藉助Parcel,我們剛纔也看到這樣的代碼:

 android.os.Parcel _data = android.os.Parcel.obtain();
 bean.writeToParcel(_data, 0);

Parcel的write和read方法全是native方法:

	// 截取的部分native方法
    @FastNative
    private static native void nativeWriteInt(long nativePtr, int val);
    @FastNative
    private static native void nativeWriteLong(long nativePtr, long val);
    @FastNative
    private static native void nativeWriteFloat(long nativePtr, float val);
    @FastNative
    private static native void nativeWriteDouble(long nativePtr, double val);
    static native void nativeWriteString(long nativePtr, String val);
    private static native void nativeWriteStrongBinder(long nativePtr, IBinder val);
    private static native long nativeWriteFileDescriptor(long nativePtr, FileDescriptor val);

一般來說使用JNI的速度肯定是比使用Java方法要快,因爲他繞過了Java層的api,不過這個並不是速度的保證,真正的優勢是避免的大量的反射操作,減少了臨時變量的創建,提高了序列化的效率。

根據Parcel的註釋,我們瞭解了數據的去向:

有一個專門負責IBinder傳輸數據的容器。
(一個可以進程共享的內存區)
Parcel可以把數據壓入,另一端的Parcel可以把數據取走
(通過實現我們可以推斷出保存數據的是一個先進先出的堆棧)
Parcel僅僅是爲了實現高性能的IPC通信,在其他的持久化方案在並不推薦。

到這裏Parcelable的序列化機制就已經分析結束了,如果我們非要把Parcelable保存到本地怎麼辦呢?

我這裏給出一個簡單的示例:

private fun writeTestData() {
        Thread(Runnable {
            val student = Student("zhangsan", 18)
            val parcel = Parcel.obtain()
            // 因爲Parcel內部有緩存複用
            // 設置數據的位置指針爲頭部
            parcel.setDataPosition(0)
            student.writeToParcel(parcel, 0)
            val fos = FileOutputStream(path)
            fos.write(parcel.marshall())
            fos.flush()

            fos.close()
            parcel.recycle()

        }).start()
    }

    private fun readTestData() {
        Thread(Runnable {
            val fis = FileInputStream(path)
            val data = fis.readBytes()
            val parcel = Parcel.obtain()
            // 因爲Parcel內部有緩存複用
            // 設置數據的位置指針爲頭部
            parcel.unmarshall(data, 0, data.size)
            parcel.setDataPosition(0)
            val student = Student(parcel)
			parcel.recycle()
            runOnUiThread {
                findViewById<TextView>(R.id.text).text = student.toString()
            }


        }).start()
    }

剛纔提到了Parcel內部有緩存,推薦使用Parcel.obtain()來獲取一個可用的Parcel對象,類似的還有Message.obtain():

/**
     * Retrieve a new Parcel object from the pool.
     */
    public static Parcel obtain() {
        final Parcel[] pool = sOwnedPool;
        synchronized (pool) {
            Parcel p;
            for (int i=0; i<POOL_SIZE; i++) {
                p = pool[i];
                if (p != null) {
                    pool[i] = null;
                    if (DEBUG_RECYCLE) {
                        p.mStack = new RuntimeException();
                    }
                    p.mReadWriteHelper = ReadWriteHelper.DEFAULT;
                    return p;
                }
            }
        }
        return new Parcel(0);
    }

總結

最後對Parcelable的序列化做一個總結:

  • Parcelable的序列化需要藉助Parcel。
  • Parcel通過JNI把序列化數據寫入到進程的共享內存中,或從進程共享內存中讀數據。
  • Parcel推薦使用Parcel.obtain()方法獲取可用實例。
  • 與Serializable相比,Parcelable避免了大量反射操作,在效率上有很大提升。
  • Parcelable僅僅是IPC的高效實現方案,其他場景慎用。

ok,這一篇就結束了,有什麼問題歡迎大家留言指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章