Serializable
Serializable 是 Java所提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作。使用Serializable来实现序列化相当简单,只需要在类的声明中指定serialVersionUID
即可自动实现默认的序列化过程。
在Android中也提供了新的序列化方式,那就是Parcelable接口,使用它来实现对象的序列化,其过程要稍微复杂一些,本节先介绍Serializable接口。上面提到,想让一个对象实现序列化,只需要这个类实现Serializable接口并声明一个`serialVersionUID即可,实际上,甚至这个serialVersionUID也不是必需的,我们不声明这个同样也可以实现序列化,但是着将会对反序列化过程产生影响,具体什么影响后面在介绍。
如何进行对象的序列化和反序列化也非常简单,只需要采用ObjectOutputStream
和ObjectInputStream
即可轻松实现。下面举个简单的例子。
上述代码演示了采用Serializable方式序列化对象的典型过程,很简单,只需要把实现了接口的对象写道文件中就可快速恢复了,恢复后的对象内容完全一样,但是两者并不是同一个对象。
刚开始提到,即时不指定serialVersionUID
也可以实现序列化,那到底要不要指定呢?如果指定的话,serialVersionUID
后面那一长串数字又是什么含义呢?我们要明白,系统既然提供了这个,那么它必须是有用的。这个是用来辅助序列化和反序列化过程的,原则上序列化的数据中的serialVersionUID
只有和当前类的serialVersionUID
相同才能够正常地被反序列化。serialVersionUID
地详细工作机制是这样地:序列化地时候系统会把当前类地serialVersionUID写入序列化地文件中(也可能是其他中介),当反序列化地时候系统回去检测文件中地serialVersionUID,看它是否和当前类地serialVersionUID一致,如果一直就说明序列化地类地版本和当前类地版本是相同地,这个时候可以成功反序列化;否则就说明当前类和序列化地类相比发生了某些变换,比如成员变量地数量、类型可能发生了改变,这个时候是无法正常反序列化地,因此会报如下错误:
一般来说,我们应该手动指定serialVersionUID
地值,比如1L
,特可以让IDE
根据当前类地结构自动去生成它地hash
值,这样序列化和反序列化时两者地serialVersionUID
时相同地,因此可以正常进行反序列化。如果不手动指定serialVersionUID
地值,反序列化时当前类有所改变,比如增加或者删除了某些成员变量,那么系统就会重新计算当前类地hash
值并把它赋值给serialVersionUID
,这个时候当前类地serialVersionUID
就和序列化地数据中地serialVersionUID
不一致,于是反序列化失败,程序就会出现crash
。所以,我们可以明显感觉到serialVersionUID
地作用,当我们手动指定了它以后,就可以在很大程度上避免反序列化过程地失败。比如当版本升级后,我们可能删除了某个成员变量也可能增加了一些新地成员变量,这个时候我们地反序列化过程仍然能够成功,程序仍然能够最大限度地恢复数据,相反,如果不指定serialVersionUID
地话,程序则会挂掉。当然我们还要考虑另外一种情况,如果类结构发生了非常规性改变,比如修改了类名,修改了成员变量地类型,这个时候尽管serialVersionUID
验证通过了,但是反序列化过程还是会失败,因为类结构有了毁灭性地改变,根本无法从老版本地数据中还远出一个新地类结构地对象。
根据上面地分析,我们可以知道,给serialVersionUID
指定为1L
或者采用Eclipse
根据当前类结构去生成地hash
值,这两者并没有本质区别,效果完全一样。以下两点需要特别提一下,首先静态成员变量属于类不属于对象,所以不会参与序列化过程;其次用transient
关键字标记地成员变量不参与序列化过程。
另外,系统地默认序列化过程也是可以改变地,通过实现如下两个方法即可重写系统默认地序列化和反序列化过程,具体怎么去重写这两个方法就是很简单地事了,这里就不再详细介绍了,毕竟这不是本章的重点,而且大部分情况下我们不需要重写这两个方法。
Parcelable 接口
上一届我们介绍了通过Serializable
方式来实现序列化的方法,本届接着介绍另一种序列化方式:Parcelable
。Parcelable
也是一个接口,只要实现这个接口,一个类的对象就可以实现序列化并可以通过Intent
和Binder
传递。下面的示例是一个典型的用法。
这里先说一下Parcel
,Parcel
内部包装了可序列化的数据,可以在Binder
中自由传输。从上述代码中可以看出,在序列化过程中需要实现的功能有序列化、反序列化和内容描述。序列化功能由writeToParcel
方法来完成,最终是通过Parcel
中的一系列write
方法来完成的;反序列化功能由CREATOR
来完成,其内部标明了如何创建序列化对象和数组,帮通过Parcel
的一些列read
方法来完成反序列化过程;内容描述功能由describeContents
方法来完成,几乎在所有情况下这个方法都应该返回0
,仅当当前对象中存在文件描述符时,此方法返回1
。需要注意的是,在User(Parcel in)
方法中,由于book
是另一个可序列化对象,所以它的反序列化过程需要传递当前线程的上下文类加载器,否则会报无法找到类的错误。
详细的方法说明请参看表2-1。
系统已经为我们提供了许多实现了Parcelable
接口的类,它们都是可以直接序列化的,比如Intent、Bundle、Bitmap
等,同时List
和Map
也可以序列化,前提是它们里面的每个元素都是可序列化的。
既然Parcelable
和Serializable
都能实现序列化并且都可用于Intent
间数据传递,那么而且该如何选择呢?Serializable
是Java中的序列化接口,其使用起来简单但是开销很大,序列化和反序列化过程需要大量I/O
操作。而Parcelable
是Android中的序列化方式,因此更适合用在Android平台上,它的缺点就是使用起来稍微麻烦点,但是它的效率很高,这是Android推荐的序列化方式,因此我们要首选Parcelable
。Parcelable
主要用在内存序列化上,通过Parcelable
将对象序列化到存储设备中或者将对象序列化后通过网络传输也都是可以的(官网说:Parcel 不是通用序列化机制,您绝不能将任何 Parcel 数据存储在磁盘上或通过网络发送。) ,但是这个过程会稍微复杂,因此在这两种情况下建议大家使用Serializable
。以上就是Parcelable
和Serializable
的区别。
Binder
Binder
是一个很深入的话题,笔者也看过一些别人写的Binder
相关的文章,发现很少有人能把它介绍清楚,不是深入代码细节不能自拔,就是长篇大论不知所云,看完后都是晕晕的感觉。所以,本节笔者不打算深入探讨Binder
的底层细节,因为Binder
太复杂了。本届的侧重点是介绍Binder
的使用以及上层原理,为接下来的几节内容做铺垫。
直观来说,Binder
是Android中的一个类,它实现了IBinder
接口。从IPC角度来说,Binder
是Android中的一种跨进程通信方式,Binder
还可以理解为一种虚拟的物理设备,他的设备驱动时/dev/binder,该通信方式在Linux中没有;从Android Frameworkhi奥杜来说,Binder时ServiceManager连接各种Manager(ActivityManager、WindowManager,等等)和相应ManagerService的桥梁;从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当bindservice
的时候,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL
的服务。
Android开发中,Binder主要用在Service中,包括AIDL和Messenger,其中普通Service中Binder不涉及进程间通信,所以较为简单,无法触及Binder的核心,而Messenger的底层其实是AIDL,所以这里选择用AIDL来分析Binder的工作机制。为了分析Binder的工作机制,我们需要新建一个AIDL示例,SDK会自动为我们生成ADIL所对应的Binder类,然后我们就可以分析Binder的工作过程。还是采用文章开始时用的例子,新建Java包com.ryg.chapter_2.aidl
,然后新建三个文件Book.java、Book.aidl、IBookManager.aidl
,代码如下所示:
上面三个我呢见中,Book.java
时一个表示图书信息的类,它实现了Parcelable
接口。Book.aidl
是Book类在AIDL中的声明。IBookManager.aidl是我们定义的一个接口,里面有两个方法:getBookList和addBook,其中getBookList用于从远程服务端获取图书列表,而addBook用于往图书列表中添加一本书,当然这两个方法主要是示例用,不一定要有实际意义。我们可以看到,尽管Book类已经和IBookManager位于相同的包中,但是在IBookManager中仍然要导入Book类,这就是ADIL的特殊之处。下面我们先看一下系统为IBookManager.aidl生成的Binder类,在gen目录下的`com.ryg.chapter_2.aidl包中有一个IBookManager.java的类,这就是我们要找的类。接下来我们需要根据这个系统生成的Binder类来分析Binder的工作原理,代码如下:
上述代码是系统生成的,为了方便查看笔者稍微做了一下格式上的调整。在gen目录下,可以看到根据IBookManager.aidl系统为我们生成了IbookManager.java这个类,它继承了IInterface这个接口。这个类刚开始看起来逻辑混乱,但是实际上还是很清晰的,通过它我们可以清楚地了解到Binder地工作机制。这个类地结构其实很简单,首先,它声明了两个方法getBookList和addBook,显然这就是我们在IBookManager.aidl中所声明地方法,同时它还声明了两个整型的id风别用于标识这两个方法,这两个id用于表示在transact过程中客户端锁清秋的到底是哪个方法。接着,它声明了一个内部类Stub,这个Stub就是一个Binder类,当客户端和服务端都位于同一个进程时,方法调用不会走跨进程的transact过程,而当两者位于不同进程时,方法调用需要走transact过程,这个逻辑由Stub的内部代理类Proxy来完成。这么看来,IBookManager这个接口的确很简单,但是我们也应该认识到,这个接口的核心实现就是它的内部类Stub和Stub的内部代理类Proxy,下面详细介绍针对这两个类的每个方法的含义。
DESCRIPTOR
Binder的唯一标识,一般用当前Binder的类名表示,比如本例中的“com.ryg.chapter_2.aidl.IBookManager”。
asInterface(android.os.IBinder obj)
用于将服务端的Binder对象准换成客户端所需的AIDL接口类型的对象,这种转换过程是区分进程的,如果客户端和服务端位于同一进程,那么此方法返回的就是服务端的Stub对象对象,否则返回的是系统封装后的Stub.proxy对象。
asBinder
此方法用于返回当前的Binder对象。
onTransact
这个方法运行在服务端中的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。该方法的原型为public Boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)
。服务端通过code可以确定客户端所请求的目标方法是什么,接着从data中取出目标方法所需的参数(如果目标方法由参数的话),然后执行目标方法。当目标方法执行完毕后,就像reply中写入返回值(如果目标方法有返回值的话),onTransact
方法的执行过程就是这样的。需要注意的是,如果此方法返回false,那么客户端的请求会失败,因此我们可以利用这个特性来做权限验证,毕竟我们也不希望随便一个进程都能远程调用我们的服务。
Proxy#getBookList
这个方法运行在客户端,当客户端调用此方法时,它的内部实现是这样的:首先创建该方法所需要的输入型Parcel对象_data、输出型Parcel对象_reply和返回值对象List;然后把该方法的参数信息写入_data中(如果没有参数的话);接着调用transact方法来发起RPC请求,同时当前线程挂起;然后服务端的onTransact方法会被调用,知道RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果;最后返回_reply中数据。
Proxy# addBook
这个方法运行在客户端,它的执行过程和getBookList是一样的,addBook没有返回值,所以它不需要从_reply中取出返回值。
通过上面的分析,读者应该已经了解了Binder的工作机制了,但是有两点还是需要额外说明一下:首先,当客户端发起远程请求时,由于当前线程会被挂起直至服务端进程返回数据,所以如果一个远程方法很耗时的,那么不能在UI线程发起此远程请求;其次,由于服务端的Binder方法运行在Binder的线程池中,所以Binder方法不管是否耗时都应该采用同步的方式去实现,因为它已经运行在一个线程中了。为了更好的说明Binder,下面给出了一个Binder的工作机制图,如下图: