首先推荐一下alloc流程中篇,alloc流程下篇,alloc流程上篇,这些对于本文的理解是很有帮助的哦
一.分析类对象的ISA
1.获取ISA地址
首先我们还是先自定义一个对象YCXPerson,打印他的内存地址
根据之前重识alloc流程下篇所提到的,ISA指针肯定是作为首地址,那么自然而然就是0x001d800100008375,然后进行位运算,去掉首尾的地址部分,最终打印结果就是YCXPerson的相关信息了,这里大家可能会疑惑为什么以0x00007ffffffffff8进行运算,这个就是重识alloc流程下篇提到的在x86_64架构下的掩码,帮助快速运算用的
# define ISA_MASK 0x00007ffffffffff8ULL
2.获取ISA中的类信息
大家也都看到了0x0000000100008370存放的是关于类的信息,那么我再来获取以下这快地址中还有哪些东西呢?
还是先取ISA地址的首地址进行运算,结果发现也是YCXPerson,那么问题就来了,为什么会有2个地址不一样的YCXPerson呢
3.元类以及MachOView的使用
要想论证这个观点,首先得知道这两个地址到底是不是同一个类,那么我就通过几种不同的方式去打印,却发现结果都一样都是0x100008370,说明一点上面所提到的另一个地址其实并不是YCXPerson类,可能是在编译阶段做了一些处理,就像之前提到的alloc优化处理一样
既然这样我们就需要了解在编译的时候是不是又生成了其他的类,这是就需要用到一个新的工具,也就是MachOView,俗称烂苹果,把工程生成的可执行文件,直接拖拽到MachOView中就可以获取相关的信息
通过MachOView解析,找到了YCXPerson,YCXTeacherCooci,继续继续
终于在Symbol Table 终于找到了,此时此刻,掌声在哪里?果然在编译的时候生成了一个新的类_OBJC_METACLASS_YCXPerson ,而它也就是所谓的元类
3.根元类
既然有元类的存在,相信大家也会好奇,元类的isa又会是什么呢?
感觉有点绕晕了,😄😄,不过最终发现到了根元类之后就结束了,最终都会指向NSObject,当然如果觉得使用LLDB麻烦,也可以直接代码,如下
YCXPerson *object1 = [YCXPerson alloc];
// NSObject类
Class class = object_getClass(object1);
// NSObject元类
Class metaClass = object_getClass(class);
// NSObject根元类
Class rootMetaClass = object_getClass(metaClass);
// NSObject根根元类
Class rootRootMetaClass = object_getClass(rootMetaClass);
NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
最终打印结果其实也是一样的
4.继承链
通过上面的各种测试发现走到根元类就到头了,那么如果是父类又或者是根类(如:NSObject)呢,他们的情况又会是什么样子呢,我们继续摸索
通过各式各样的测试发现,根类的元类就是自己,而子类的元类的父类和父类的元类是相同的,于是乎,经典的,嗯哼哼。。学不来这腔调,难受。。
5.ISA的关系图
苹果官方提供的ISA的关联图
父类,根类的元类走势图
二.内存偏移
1.地址的平移
话不多说,上图(帅气的一批)
相信大家都知道内存地址都是从高到底地址去存的,通过上述打印发现,一般我们创建的局部变量,在栈中的地址是连续的,这个就不用我去计算了吧,实在不行,掰手指算😄
那么我再来看一下数组的情况
通过测试发现,当我用int d = c,其实也就是取了数组的首地址,然而+1,其实就是以该数组的内部数据类型所占的内存大小,作为一个单位的偏移量去计算,int类型4字节,+1就相当于指针d的地址平移4字节,然后(d),就意味着取这块地址的值,所以就有了上述的结果,可能大家会奇怪,说好的分析类,怎么突然就讲到内存偏移呢,不着急不着急,好戏在后头!!!😄
三.类的结构分析
1.cache的内存计算
在之前alloc流程下篇,里面就得出过结论,其实对象在底层的本质是结构体,而OC层面的id ,Class都是结构体指针,那么我们接下来就看看Class结构体中存放着哪些东西呢
通过在objc源码中搜索我们发现结构体中主要就是superclass(isa),cache,bits,superclass(isa),cache这个单词相信都不陌生,缓存,我们点进去瞧一瞧
通过对cache内部结构分析,它的内存大小应该是16字节,再加上首地址isa占8字节,以及superclass占8字节,也就是32字节,根据前面所说的内存地址从高到低的连续性,那么就意味着我们只需要在结构体指针objc_class的首地址处进行内存偏移,就可以获取到bits的相关信息了
2.bits结构分析
老规矩,分析前,先点进去看看
发现了一个 class_rw_t* data(),相信大家都会对 data()有些敏感,貌似是存放数据的地方,为了验证猜想,还是进行LLDB调试一下,毕竟实践才是检验真理的唯一标准
根据前面的计算,从首地址偏移32字节,就可以找到bits的地址,通过打印data()发现,里面有个firstSubclass,这个是干嘛的呢?后面一一道来,通过打印data()里面的数据发现并没有类里面的相关属性,然后我打印了properties(),果然有东西,但是并没有我们想要的,那么我在源码中点进去看下
通过层层查看,发现properties()里面其实嵌套了多个数组,那么我们就通过LLDB层层剥开,获取里面真正想要的内容
剥开面纱,终于看到了熟悉的身影,看到了属性name,hobby,但是没有发现subject,问题1产生了
接下来我们再来查找方法的存放位置,再查找之前还是要了解methods()里面的数据结构
通过methods()和properties()对比可以看出问题关在于结构体property_t和method_t
最终发现method_t内部还有一层结构体big,而这里才是我们需要的东西
果然方法全都在这里了,但是似乎少了一个类方法+ (void)say666,那么这里就有问题了,为什么类方法不在类的信息里面呢,还有前面的属性subject有存放在哪呢
四.寻找类方法和ivar
1.寻找类方法
- 思路:其实在找类方法之前首先我们要锊一下思路,类的实例对象方法存在类中,而通过之前的学习,我们又知道在编译过程中,其实是有生成类的元类的,那么我们是不是可以通过类的元类去寻找呢
开始LLDB调试喽,代码里有说明
(lldb) p/x YCXPerson.class
(Class) $0 = 0x0000000100004680 YCXPerson //类对象指针
(lldb) x/4gx $0 //获取类对象的内存分部地址 而首块地址必然就是ISA,指向元类的内存地址的指针
0x100004680: 0x00000001000046a8 0x0000000100353140
0x100004690: 0x000000010112e490 0x0002802800000003
(lldb) p/x 0x00000001000046a8+0x20 //通过元类首地址isa偏移32字节得出元类bits
(long) $1 = 0x00000001000046c8 //后面就是重复工作了
(lldb) p (class_data_bits_t *)$1
(class_data_bits_t *) $2 = 0x00000001000046c8
(lldb) p $2->data()
(class_rw_t *) $3 = 0x000000010112e430
(lldb) p *$3
(class_rw_t) $4 = {
flags = 2684878849
witness = 0
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4294984984
}
}
firstSubclass = nil
nextSiblingClass = 0x00007fff8e9fbc60
}
(lldb) p $3.methods()
(const method_array_t) $5 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x0000000100004560
}
arrayAndFlag = 4294985056
}
}
}
Fix-it applied, fixed expression was:
$3->methods()
(lldb) p $5.list
(const method_list_t_authed_ptr<method_list_t>) $7 = {
ptr = 0x0000000100004560
}
(lldb) p $7.ptr
(method_list_t *const) $8 = 0x0000000100004560
(lldb) p *$8
(method_list_t) $9 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 1)
}
(lldb) p $9.get(0).big
(method_t::big) $10 = {
name = "say666"//一把鼻涕一把泪啊 终于出现了
types = 0x0000000100003f77 "v16@0:8"
imp = 0x0000000100003e00 (KCObjcBuild`+[YCXPerson say666])
}
Fix-it applied, fixed expression was:
$9.get(0).big()
(lldb)
由于调试内容过多,过程有点绕,讲真的,我测试的时候错了好多次,快晕了,还好最终还是得出了结果我就把调过代码拷贝下来,以便更好查看,解决一个问题了,那么下一个
2.寻找成员变量
在前面我们提到过properties(),里面是有成员变量的信息,那么我们在去看看相关的参数
接下来就开始手动调试
(lldb) x/6gx p1
0x10072c240: 0x011d800100004681 0x0000000000000000
0x10072c250: 0x00000001000040b0 0x0000000000000000
0x10072c260: 0x011dffff8e9f5311 0x61636f4c796c6e4f
(lldb) p (class_data_bits_t *)0x10072c260
(class_data_bits_t *) $2 = 0x000000010072c260
(lldb) p $2->data()
(class_rw_t *) $3 = 0x00007fff8e9f5310
(lldb) p *$3
(class_rw_t) $4 = {
flags = 2392806200
witness = 32767
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 140735586206672
}
}
firstSubclass = 0x000000010072c600
nextSiblingClass = 0x0001802000000003
}
(lldb) p $4.ro()
(const class_ro_t *) $5 = 0x00007fff8e9f83d0
(lldb) p $5->ivars
(const ivar_list_t *const) $6 = 0x0000000100353140
(lldb) p *$6
(const ivar_list_t) $7 = {
entsize_list_tt<ivar_t, ivar_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 3485936, count = 1)
}
(lldb) p $7.get(0)
(ivar_t) $8 = {
offset = 0x0000000000000000
name = 0x000000010072c680 "\364E\241z
type = 0x0001801000000003 ""
alignment_raw = 25182468
size = 1
}
(lldb)