这两天有个同事在使用泛型的过程当中,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
答:当咱们对一个对象实现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的序列化和反序列化会创造大量的对象和写入数据的时候,会写入除去真实数据之外的其它数据,好比序列化协议,版本等等。指针
首先咱们在一个实体对象在实现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在实现的时候为何须要存和读取的顺序须要一致的缘由。
答:由于在父类的BaseBean里面都有实现BaseBean中字段的读写,因此BaseBean中字段的数据是能够拿到的。
答:其实这里是要看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。
答:这里咱们的BaseBean不该该是一个类,最合适的话,应该是一个interface,好比咱们公共界面是用到了t.getName()来获得显示的数据,这个时候
class ChildBean implements BaseBean,Parcelable{ ... @Override public String getName(){ return "WelliJohn"; } ... }
当用到了传值的时候,ChildBean再自身实现了Parcelable接口,这样代码就完美了。这样若是真的在公共界面有个特殊的类型的话,判断下T的类型(ChildBean.class.isInstance(t)),强转下也能够进行某个特殊数据处理了。
serialization | parcable |
---|---|
文件操做,且用到了反射 | 单独的内存空间,速度快 |
会创造大量的读写对象 | 直接操做内存读写 |
实现简单 | 实现复杂,并且读和取的数据要一致 |
写入的时候,会有字段名,长度等 | 只是写入数据,节省资源 |
由于写在文件中,适合持久化数据 | 不适合持久化数据,可能会变化 |
若是大家有对3.3的解决方案感受有更好的处理思路的话,欢迎提出来共同探讨
若是大家以为文章对你有启示做用,但愿大家帮忙点个赞或者关注下,谢谢