我再来分享一个底层知识点,学到了之后不写出来总觉得不是自己的,关于cache的数据结构,首先cache是什么呢?
这个英文单词就是缓存的意思,那他缓存的是什么呢?我们大多数人肯定都不太了解,我之前获取bits,可以进行内存平移,那我获取cache是不是也可以进行内存平移呢?
一试便知,我在objc的源码工程里面写了一个demo,自定义了一个类 LGPerson 继承自 NSObject,来到main里面,通过 class 拿到这个类,然后在下一行打一个断点,如下图。
运行,停在断点处之后,我来进行万能的LLDB调试。
拿到 pClass 的地址之后给他打印出来,但是直接打印是不行的,还要进行一下强转。
强转之前,我先给他平移16个字节,就是加10,变成了0x0000000100008460,然后强转的话,转成什么类型呢?
就是 cache 的类型,那 cache 是什么类型我不记得了,我就去源码里面找一下,全局搜索objc_class。
可以在后面加一个空格,结果会少很多,找到 new.h结尾的文件,然后里面就有这个 objc_class 结构体,点进去。
这里就找到了cache,这个过程需要对底层原理有比较好的掌握,cache 是什么类型就一目了然了,是一个cache_t 类型,我拷贝出来,加个星号表示还原他的指针。
回车就得到了 cache_t 类型的 $1,然后我来看里面的数据。
打印得到了这么一长串内容,主要有这么几个变量名:_bucketsAndMaybeMask、_maybeMask、_flags、_occupied、_originalPreoptCache。
那这些东西我看不懂,不知所云,所以我需要在源码里面找到对应的说辞,怎么找呢,我 command 点击 cache_t,Jump to Definition。
很明显,cache_t 也是一个结构体,然后有一个private,里面就有这个 _bucketsAndMaybeMask 等一些变量。
跟我打印出来的一模一样,除了 _bucketsAndMaybeMask之外,其他几个都被 union 一个大括号包进去了,他们是一个联合体。
很多人可能对于这种结构不那么了解,我再来补充一个点,在真机上的app,是运行在 arm64 架构上的,模拟器呢?i386,Mac呢?
x86_64,M1 的 Mac 也是 arm64,这是跟硬件有关的,那这个联合体里面有一个 #if LP64 是什么呢?LP64 就是适用在 Linux 和 macOS 电脑上面的一个数据模型,我找了个表格给大家看一下。
就很明显了,LP64 就是指代 Unix 系列的操作系统,所以都是跑的64位,也就是说,这个 if 条件是成立的。
整个 cache_t 的数据结构就能够很清晰很直观的了解了,那么问题来了,cache是缓存,那到底缓存什么呢?要么缓存属性,要么缓存方法,这里我看到了属性,但是没看到方法。
按理说也应该缓存方法才对,如果是缓存方法的话,我应该在这个结构体里面能看到 sel 和 imp,但是并没有,是不是意味着他不是缓存这些东西呢?我先卖个关子。
接下来又有一个问题,这几个属性里面,哪一个才是我们需要关注的、最重要的那个?打印信息里面两个没有值的可以排除,剩下的我也不知道哪个最重要,那怎么办呢?
这里又有一个阅读源码的小技巧,就是看他提供的功能方法,这个cache_t,是一个缓存,也就必定是一个存储的过程,意味着他一定会有东西进行了读写,或者增删改查等等的操作。
所以,我再继续往下找方法,在481行就找到了一个 insert,也就是插入,正好命中了我刚刚的猜想,他离不开增删改查,并且他的参数正好就有 sel 和 imp。
所以,我来看看他里面有没有我想要的东西,点进去,看到了两个 sel(),都是由 bucket_t 对象中的元素进行调用,也就是对 bucktet_t 进行了一些操作,难道关键就在bucket_t ?(下图源码进行了一些删减)
正好在之前的 cache_t 结构体中找 insert 的时候也看到了不少 bucket_t。
感觉他的重心应该就是这个bucket_t,我直接点进去,看到如下图的内容。
这就恍然大明白了,就是千呼万唤的 imp 和 sel,bucket 单词是“桶”的意思,就是一个容器,装了很多的imp 和 sel,并且一个 imp 就对应了一个 sel,梳理一下。
cache 里面有一个 buckets,buckets 里面就会有一个 sel 和 imp,但是这个具体的结构,我还不是非常的清楚,但是知道这么多就已经足够了。
然后我要去验证里面的值,是不是真的有,是不是真的是这样的呢?眼见不一定为实,自己操作一遍才放心,那我继续LLDB调试。
之前已经拿到了$2,然后他的重点是buckets,所以我是不是应该打印这个_bucketsAndMaybeMask 呢?只有这个里面有 buckets 这个词,试一试。
然后我去拿他的Value,因为也没其他东西可以拿。
竟然拿不到!又进了死胡同,LLDB调试不出来了,怎么办?这个时候又回到了上面提到的调试技巧,我只能去找他有没有合适的方法。
这是LLDB调试遇到问题的时候最常见的办法,那我去 cache_t 结构体中找一下是不是有get相关的方法,别说,还真有。
这里得到了一个结构体指针 buckets,感觉事情变得简单了,我拷贝 buckets() 继续进行调试。
果真拿到了一个 bucket_t 的地址,那这个地址里面是什么我也不知道,打印出来看一下。
跟之前的源码分析一模一样,就是 imp 和 sel,但是他们什么都没有,很尴尬,不过也没关系,我这里已经可以得到一些信息了,bucket_t 结构体里面是有一个 #if 判断的。
这个条件语句里面,我是走的 if 还是 else 呢,我都不用分析这个条件,对比一下打印出来的 $4 就知道了,先 imp,然后 sel,所以走的是 arm64,大多数人在这里应该都是走的 else,为什么呢?
因为我是M1的Mac,我已经走在了时尚的最前沿,哈哈哈,大多数人都是 x86,所以走的是 else,如果是真机环境的话,走的也是 arm64,其实区别不大,了解一下就好了。
但是他们为什么都没有呢?因为没有调用方法!没有调用方法,他有个LLDB的缓存啊,那我再来调用一下方法,这个 LGPerson 我已经写好了一个实例方法 saySomething。
调完方法之后,上面的流程我再来一次。
imp 的 Value 不出所料有值了!但是你们操作的时候,可能还是会没有东西,如果你们没有的话,怎么办呢?
我也提一嘴,这是一个 buckets,他有个s字母结尾,就意味着他是一个数组,所以你可以在后面加上一个[1],取他下一个bucket,或许你就有值了。
如果没有多个就可以直接取,这里涉及到了哈希函数,因为哈希函数的下标是不一定的,普通的数组是从零开始的,但是哈希就不同,而且他还是无序的。
但是这个 $10 还不是我想看到的结果,我想看的是最终打印出 saySomething,才能证明我们的源码分析没有问题,那我还是同样的来看 bucket_t 结构体里面有没有相应的方法。
找到一个 sel(),不解释了,直接拿过来用。
打印出了 saySomething,就这?简单得很嘛,所以,我也应该同样的可以改成 imp()。
竟然报错了,问题不大,这个错误提示很熟悉,翻译过来就是参数太少了,预想的是有2个参数,但是我没有给,只有0个,如果你也这么操作了,说明你肯定没有去仔细看源码,imp() 就在 sel() 下面一点点。
这里他需要两个参数,一个 bucket_t,一个 class,这个 class 我可以给你,很简单,就是 pClass,那这个bucket_t 我怎么给?我不知道,我也不想知道,感觉很麻烦的亚子,那我直接丢个 nil,回车。
漂亮,来自 KCObjcBuild 里面的 LGPerson 的实例方法 saySomething,完美打印出来,到这里,cache的数据结构分析,就基本搞定,到此结束。
最后呢,如果对于文章内容有任何疑问都可以进行留言,objc源码我就不放了,网上一大把,如果某些内容有误也恳请帮我指出来,共同进步。