前言:
之前我们已经学习了类的结构,里面有isa,superClass(isa走位图、继承链),bits(存储了methodlist,properties,protocol),类里面仅剩下了cache,cache顾名思义就是缓存,那么在类中,缓存的到底是什么呢?这期博客就是探究类的缓存以及底层原理。
cache结构初探—cache是什么?数据结构如何?
此图先贴为敬,不管你看不看,他就在这里。总体思路流程是沿着这个往下走的。
方法:根据我们原先对bits的探索方式,同样的,我们来探索cache
从类结构中可以看到,他在第16个字节以后(前面有isa和superClass)所以要平移16位。
得出如下结果:可以看到主要包括了bucketsAndMaybeMask、_maybeMask,_originalPreoptCache。
此时应该好奇、验证并且找到定义cache的结构体:
补充:
LP64 macOS系统,这是一个联合体,联合体互斥,所以很明显是走了上面,不会走originalPreoptCache。那么很明显整个cache占用的就是 8 + 8 = 16。
分析:
此时如果是缓存,必定缓存的是属性、方法等,那就必然有sel或者imp,这时候在实现中必然有方法。稍微浏览了一下,果然里面有很多方法,比如说开辟空间,比如说清空,还有插入。那么在缓存的时候,必然事先是要插入,才会有缓存。那么此时就找到了这个insert方法。
经过以上探究,大致可以得出这样的结构图:
通过lldb验证方法的存储
获取bucketsAndMaybeMask发现,结构体跟之前的不同,并且直接打印value是无法获取值得,这个时候的获取方式就卡住了,但是由之前调试bits的经验,我们可以通过找到里面对应实现的方法来获取里面的值。
其实这里的探究我感觉就是不断地尝试,通过打印里面的方法来获取,只不过不是瞎找瞎打印,所有的方法都遵循简明达意,在类中快就发现了buckets()这个方法,并且这个方法看起来像是一种初始化的结构体构造方法,所以尝试打印,果不其然。
注意
这里的buckets()方法的调用者是$2,也就是cache,而不是bucketsAndMaybeMask,其方法在460行。
打印$4打印并没有数据,分析原因是没有调用方法,没有产生缓存。这里也就印证了,cache里面存的是方法,而不是其他数据。
此时此刻我们调用一下类里的方法,再次打印。
补充
这里有两种打印方法的方式。
结构体通过->的方式调用,对象通过点的方式调用。
注意
在打印的时候直接通过buckets()方法无法打印出来,在occpied中确实可以看到1,表示有一个方法缓存,通过buckets[1]却可以打印出数据并且可以打出来。
打印方式时,通过源码查看,内部需要传两个参数,第一个是base不知道传什么就传他nil吧。
分析
oc对于方法缓存是有自己的处理的,存取方式是用得哈希数组存的,哈希是结合了链表和数组,方便增删查,内部是无序的。
脱离源码查找方法
lldb调试的弊端
1.需要的门槛或者说需要对底层有比较深入的了解,对方法有相当敏锐的感知,调试比较耗费时间。
2.每次都需要不停的打印,层层深入。
这里就直接把cocci老师的贴出来了。
方法缓存底层解析
通过对源码的分析一层层探索,总结如下:
cache_t是个结构体指针,首先会通过结构体指针的平移去方法区找对应的方法,取得时候上面已经分析过了,因为他是哈希数组,根据数组的方式取即可实际就是通过指针平移。存的方式也是通过哈希存。当方法区没有没有缓存时,进来默认是先创建一个bucket桶,里面可能多有个bucket,所以他叫做buckets,每个bucket里面有对应的sel和imp的地址,如果说在cache中存取,cache会越来越大,也就超过了之前定义好的大小,这样是不符合结构体定义的。存取插入的过程,内部会做dowhile循环防止哈希冲突,如果没有超过容器大小,内部是默认超出3/4溶容积,会做两倍扩容,在刚进入的时候,会有判断,是首次进入还是扩容,首次默认是1<<2,1左移2单位,就是4,oc内部做了-1操作就是3,如果是扩容状态,会调用清空操作,直接清空内存地址。原因是内存在开辟好了以后,是不能改变的,只能通过伪新增,即删除再添加,没有把原来的方法再次添加的原因是:1.数组拷贝平移需要耗费很多资源,且原先老的旧的方法直接用新的代替老的,可以节省内存,方便查询。
好家伙,非常的清晰~~!
补充
之前的分析中,没有对bucketsAndMaybeMask做说明,这里补充一下,分别输出他的地址和buckets数组的首地址。两者是一样的,可以看出,bucketsAndMaybeMask就是buckets数组的首地址。
3/4扩容:就是一个负载因子,在0.75时扩容对空间利用率高,对哈希冲突可以有一定成都的避免,过多的哈希冲突会导致系统的压力大。
后记
ios oc底层对cache的方法缓存做了很多处理,从算法到存取,获取,包括对内存的优化,其中处理到的逻辑可能还有很多遗漏,但是主流程应该就是这么走的,目前尚欠缺insert插入的闭环流程,下面的章节会讲到,等各个模块连起来,就会打通所有的知识盲区。
给所有ios开发者共勉:革命尚未成功,同志仍需努力!!!