关于NSDictionary的底层实现
1. NSDictionary底层是数组,一段连续的内存,存储时使用key的hash值在数组中查找,如果查找的位置已经有值,则使用拉链法解决,就是使用链表将值串联起来,这时用的不连续的内存。查找时,使用key的hash值在数组中查找,如果查找的位置已经有值,则在链表根据key查找(key和vaule可能组成一个结构体)。
另外一种说法是_CFDictionary有两个变量_keys; _values;
dictionary之所以采用这种设计,其一出于查询性能的考虑;其二dictionary在使用过程中总是会很快的被释放,不会长期占用内存。associated object是iOS开发常用的机制之一,它实现了不通过继承来增加属性这种需求。通过阅读objc-references源码,可以发现关联对象内部使用了嵌套dictionary的结构实现了对象的扩展属性管理,也就是使用开放定址法的解决方案。
-
AssociationsHashMap是一个dictionary,以对象hash结果存储了一个dictionary,用OC的泛型声明来看,就是一个NSDictionary<id, NSDictionary *>的结构变量,这个变量是全局的。 -
ObjectAssociationMap是被上面嵌套的dictionary,这个结构存储了实际绑定的属性值。在我们调用objc_setAssociatedObject的时候,会将传入的key和value存储在这里面。
开放定址法在大量数据存储时,会造成大量的空间占用,为什么associated object采用全局对象的情况下依旧使用这种方案。这是因为虽然苹果使用了一个全局的AssociationsHashMap对象存储了全部的关联对象,但在对象dealloc时会移除这些数据,同一时间占用的内存也是可接受的:
3. NSArray底层是数组,一段连续的内存。
NSMutableArray用了环形缓冲区 (circular buffer)。这个数据结构相当简单,只是比常规数组或缓冲区复杂点。环形缓冲区的内容能在到达任意一端时绕向另一端。
环形缓冲区有一些非常酷的属性。尤其是,除非缓冲区满了,否则在任意一端插入或删除均不会要求移动任何内存。我们来分析这个类如何充分利用环形缓冲区来使得自身比 C 数组强大得多。在任意一端插入或者删除,只是修改offset参数,不需要移动内存,我们访问的时候只是不和普通的数组一样index多少就是多少,这里会计算加上offset之后处理的值取数据,而不是插入头和尾巴的时候,环形结构会根据最少移动内存指针的方式插入,例如要在A和B之间插入,按照C的数组,我们需要把B到E的元素移动内存,但是环形缓冲区的设计,我们只要把A的值向前移动一个单位内存,即可,同时修改offset偏移量,就能保证最小的移动单元来完成中间插入.NSDictionary和NSMutableArray底层原理(哈希表和环形缓冲区)
4. C++的map,是基于排序的平衡二叉树(C++标准库的map使用该数据结构),而如果用二叉树的话道就会涉及链式节点。
5. Java的HashMap,JDK1.8之前,使用数组,用hash值查找,冲突时使用拉链法解决,就是链表。JDK1.8后,使用数组,用hash值查找,冲突时使用拉链法解决,就是链表,但如果链表长度超过8之后,则转换为红黑树,已减少查找时间,链表插入删除时间复杂度为O(1),查找时间复杂度为O(n),红黑树插入删除时间复杂度为O(logn),查找时间复杂度为O(logn)。
6. SideTables,ios的内存管理使用SideTables,是一个数组,使用对象的内存地址作为key,然后hash运算,查找值,如果冲突,则使用拉链法,查找到SideTable对象,SideTable有属性RefCountMap和weak_entries,分别存储了对象的计数器和weak引用此对象的数组,RefCountMap是一个c++的map,使用对象指针查找对应的value,value是存放了计数器,weak_entries是一个数组,使用对象指针hash查找,冲突时使用拉链法解决。
java.util 中的集合类包含 Java 中某些最常用的类。最常用的集合类是 List 和 Map。
List的实现类分别有ArrayList,LinkedList,Vector。
ArrayList:相当于是new了个Object的类型的数组,默认值是10,底层数据结构使数组结构,查询速度快,增删改慢。
LinkedList:底层使用链表结构,增删速度快,是根据地址传递的,两个之间有相互关联的地址:查询稍慢,删除新增快,LinkedList也有get()取值方法,但是它是从第一个查询完所有的之后,在从中用get()取值。
Vector:底层是数组结构,线程同步ArrayList是线程不同步;
ArrayList当初始化容量超过10时,会重新new一个相当于之前150%的集合 ,把原来的东西放入这150%中;Vector:当容量超过10时,则会new一个200%的集合放进去,浪费内存。
CopyOnWriteArrayList:在写时拷贝,也就是如果需要对CopyOnWriteArrayList的内容进行改变,首先会拷贝一份新的List并且在新的List上进行修改,最后将原List的引用指向新的List。使用CopyOnWriteArrayList可以线程安全地遍历,因为如果另外一个线程在遍历的时候修改List的话,实际上会拷贝出一个新的List上修改,而不影响当前正在被遍历的List。
Map 提供了一个更通用的元素存储方法。Map 集合类用于存储元素对(称作“键”和“值”),其中每个键映射到一个值。从概念上而言,您可以将 List 看作是具有数值键的 Map。而实际上,除了 List 和 Map 都在定义 java.util 中外,两者并没有直接的联系。
HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。
Entry就是数组中的元素,每个Entry其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。
HashMap在底层将key-value当成一个整体进行
处理,这个整体就是一个Entry对象。HashMap底层采用一个Entry[]数组来保存所有的key-value对,当需要存储一个Entry对象时,会根据hash算法来决定其在数组中的存储位置,再根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。
默认是构建一个初始容量为16,负载因子为0.75的HashMap。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过160.75=12的时候,就把数组的大小扩展为 216=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
HashTable与HashMap大体一致,但是在操作HashTable时会默认加上一个同步锁,其它进程要操作此对象。只能等本次操作完成后解开同步锁。
ConcurrentHashMap遵从了和Hashtable一样的规范,这里指的是线程安全的规范,但是其底层的实现与Hashtable并不一致。ConcurrentHashMap底层采用的锁机制,执行put方法的线程会获得锁,只有当此线程的put方法执行结束后才会释放锁,根据多线程的知识,获得锁的线程会通知其他试图操作put方法的线程,并通知其他线程出于等待状态,直到释放锁后,其他线程才会去重新竞争锁。这一点保证了ConcurrentHashMap的线程安全。
HashMap采用链地址法解决哈希冲突,多线程访问哈希表的位置并修改映射关系的时候,后执行的线程会覆盖先执行线程的修改,所以不是线程安全的。
Hashtable采用synchronized关键字解决了并发访问的安全性问题但是效率较低。
ConcurrentHashMap其实是HashTable的升级版,使用了线程锁分段技术,每次访问只允许一个线程修改哈希表的映射关系,所以是线程安全的。
版权声明:本文为CSDN博主「谢小鱼」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_26356861/article/details/105215704
浙公网安备 33010602011771号