数据结构是编程工作的一项基本功,虽然现在很多应用程序的开发工作不会涉及数据结构的设计和实现,但是往往绕不开数据结构的使用,特别在一些特殊需求下,对数据结构的理解显得更为重要。
安卓应用的开发自然也会涉及一些数据结构的使用,由于其开发语言是Java,大体上来说,数据结构的很多使用场景仍在Java的集合框架之内。不过安卓自有其特殊的地方,在这些场景下,源码中是扩展了一些数据结构来进行优化,但还是基于Java集合接口的。这里,先来看看安卓几个特殊的地方。
-
安卓基于Linux系统,是一个多任务多进程的系统,其系统结构采用了分层组件化设计,这就造就了安卓中进程通信(IPC)的频繁。进程通信传递数据,有两种比较常见的方式,一是通过内存数据共享,另一个是通过磁盘IO进行数据共享,磁盘IO速度慢于内存数据传递。安卓系统基于内存数据传递设计,内存对于安卓系统的重要性不言而喻,频繁的进程通信应该对内存比较友好才对,也就是说,安卓进程通信用到的数据应该比较节省内存。通过对安卓IPC的简单分析可以知道,安卓IPC的传递数据主要涉及Parcel和Parcelable两个类。
-
组件通信,安卓系统有四大组件,组件间和组件内的通信是每个安卓开发童鞋都会碰到到的情况,这种通信可通过内存数据传递(表现为‘引用数据类型对象’实现Parcelable接口),也可通过磁盘IO传递(也会使用内存哦,表现为‘引用数据类型对象’实现Serializable接口),当然,鉴于速度和内存考虑,安卓推荐优先使用内存数据传递,即组件之间传递非基本类型数据时,采用实现Parcelable接口的方式。另外,在传递基本数据类型方面,安卓推荐使用Bundle对象。
-
缓存设计,安卓应用的良好体验离不开缓存的设计,官方比较推荐的缓存算法是最近最少使用原则,也就是LruCache这个类的设计理念,这个类也是安卓场景下扩展的一种数据结构。
上述主要分析了安卓开发常见的几种对数据结构有所要求的场景,主要涉及4个类,分别是:
- Parcel
- Parcelable
- Bundle
- LruCache
上面的这几个类都可以认为是安卓扩展的数据结构(也可认为是一些基本数据结构的封装),接下来将分析这些数据类与常规数据结构有什么关联,以及这些数据类如何达到场景/开发要求的。
1. Parcelable
一个专门的接口,专为那些对象可以被写入/读取自 Parcel 这个数据容器的类而设计,实现Parcelable接口必须有个非空,静态的成员变量Creator,其实现Parcelable.Creator接口。
主要方法:
- public void writeToParcel(Parcel dest, int flags);
提供数据写入Parcel操作; - public T createFromParcel(Parcel source);
Creator的方法,提供数据从Parcel中取出
整体上来说,该接口就是内存级别的数据序列化的抽象。
2. Parcel
职责上来说,Parcel就是个消息容器,安卓IPC过程通过IBinder机制进行,其中的数据,就是通过Parcel传递。这个容器里面可以盛放基本数据类型和引用数据类型,Parcel传递这些数据就类似于邮局快递包裹,一端打包,另一端拆包,包里面可以放很多类型的东西。另外,Parcel里也可以存放IBinder存活的对象,只是在接收方,会接收到这个IBinder对象的代理,有点远程代理的味道。
Parcel是为高性能IPC而设计的,而并不是为持久存储设计,因此,应该避免将parcel数据进行持久化。在Java空间和C++都实现了Parcel,由于它在C/C++中,直接使用了内存来读取数据,效率较高。不过呢,本篇主要着眼于数据结构方面,只挖一下在这方面Parcel的实现吧。
当涉及少量数据传递,特别是基本数据类型传递时,并不涉及数据结构,所以关于读写基本数据(及其数组)的方法如writeByte,readByte,writeDouble,readDouble,writeFloat,readFloat,writeInt,readInt,writeLong,readLong,writeString,readString等就不再分析了。重点是涉及较多数据读写时的实现方式。
- 普通表容器的数组方面,Parcel并没做特别的处理,基本采用的就是Java集合现有的实现方式;
- Map方面,Parcel推荐使用另一种容器--Bundle,这点在源码中是有所体现的。
看来,Parcel在数据结构方面的实现,需要通过Bundle这个类来研究了。
3. Bundle
一种特殊的类型安全容器,可以存放任意类型的键值对数据(包括实现了Parcelable和Serializable接口的数据),这个容器在数据读写(Parcel)方面做了很多优化,具有很好的性能,而且这个容器的 类型安全API可以有效防止在将数据传入Parcel时出现类型错误。因此,在组件间传递数据时,更推荐实现接口Parcellable的方式。
也就是说,Bundle这个类有两个主要职责:
1)性能优化过的map容器; 2)为数据转入/转出Parcel提供了途径。
Bundle继承自BaseBundle,BaseBundle为刚才提到的Bundle的两个职责提供了基本的实现。BaseBundle是一个封装过的容器,其真正的内部实现是ArrayMap,这个类是与HashMap同等级别的数据结构。
ArrayMap的内部实现是维护两个数组,一个int数组是存储key的hash,另一个数组按相邻方式保存key和value。实现思路是,使用二分查找法在hash数组里找寻新key的hash对应的索引,若找到,则由此索引,在另一个数组里放入数据的key和value(若找不到,则将hash插入hash数组,得到新数组,必要时扩容,hash数组是要排序的)。在添加、删除、查找数据的时候,都会使用此类方式,带来一定的速度损耗,不过对于小数量(几百个元素)的操作,这个损耗并不明显。而HashMap内部则是数组+链表结构,其内部会维护很多HashEntryMap对象,所以HashMap比ArrayMap占用更多的内存,而其速度优势,在小数量下并不明显。可以说,ArrayMap是安卓专为小数据量传递的省内存版本。在IPC和组件进行数据传递时,ArrayMap要比HashMap等数据结构更为有优势。
相似的数据结构有:SparseArray,ArraySet等。
4. LruCache
最后是缓存方面的应用。LruCache基于LRU的基本概念,为了达到按近期最少使用排序,源码选择了LinkedHashMap来作为LruCache的存储容器。
LinkedHashMap继承自HashMap、底层使用哈希表与双向循环链表来保存所有元素以及元素的顺序,包括元素的插入顺序或者元素的访问数据。其基本操作与父类HashMap相似,它通过重写父类相关的方法,来实现自己的链接列表特性。
对于LruCache来说,它通过LinkedHashMap记录元素了的访问顺序,并将最近访问的元素置于链表的末尾,使得当缓存空间紧张时,迭代删除的都是最近使用最少的元素。另外LruCache也有一些其它操作,比如缓存大小控制,记录访问次数,失败次数等等。
参考文档
- 当然是安卓源码
- Android中Serializable和Parcelable序列化对象详解
- LRUCache 详解
- 其它一些博客,争议的点比较多,就不列举了。
网友评论