1:首先熟悉几个LLDB的指令
LLDB打印isa信息:x/4gx:读取内存,打印内存中存储的数据p/x:以16进制形式输出0x00007ffffffffff8:是ISA_MASK的值,&该值的目的是取出NONPOINTER_ISA中的isa值
1:类信息分析
编写代码, 并打断点
- 为什么用
isa&mask就能得到类地址呢? 与上mask,其实是首地址位运算,之前我们讲过,类的信息,都是存储在isa_t的shiftcls中,而shiftcls如上图,是位于 3 字节之后,所以为了能 取出shiftcls, 我们需要进行位移运算,而mask是系统提供的,在 _x86_64系统下的掩码,可以快速运算;
在上面的例子中, 我们打印实例变量p的isa 输出的是LGPerson 类. 那么下面我们打印一下LGPerson类的isa, 看一下他的输出.
- 上图LLDB制定所打印的内容:
-
p/x p打印LGPerson的实例对象p的内存地址 -
x/4gx 0x00000001005b3870格式化打印0x00000001005b3870地址下的连续地址空间内存储的数据,拿到了LGPerson对象的首地址,也就是isa指针地址 -
p/x0x011d800100008365&0x00007ffffffffff8通过isa指针和ISA_MASK的与操作,解析了LGPerson类对象的首地址 -
po 0x0000000100008360打印这个地址数据,得到了LGPerson -
x/4gx 0x0000000100008360通过实例对象的isa指向类对象,我拿到了类对象内存地址0x0000000100008360,格式化输出类对象的内存地址 -
p/x 0x0000000100008338 & 0x00007ffffffffff8将类对象的首地址(isa指针地址)0x0000000100008338和ISA_MASK做与操作,得到了元类·的内存地址0x0000000100008338 -
0x0000000100008360VS0x0000000100008338虽然地址不同,但是打印都是LGPerson,是类地址与元类地址的区别 -
LGPerson: 猜想 类会和我们的对象 无限开辟 内存不止有一个类 为什么打印实例变量
p的isa和类的isa输出都是LGPerson.
然后我们继续验证一下类对象在内存里是不是存在多份, 打印4种方式获取类对象的地址:
通过上面的打印结果,说明类对象并没有多个,而且这个类对象地址和实例对象的
isa指向的地址一致。类对象的isa指向的是一个新的东西即元类。
那么LGPerson类的地址是什么呢?
这就引出了元类, 通过对象的isa,我们摸到了类,通过类的isa,我们摸到了元类
- 总结一下规律:
- 实例变量的
isa指向类LGPerson LGPerson类的isa指向另一个LGPerson类(这两个LGPerson类不是同一个,因为他们的地址不同,实际上第二个LGPerson类是元类)- 打印
LGPerson类的内存地址,通过此验证得知,对象的isa指向类,类的isa指向元类
- 实例变量的
但是,在iOS中,从未有过元类这个定义;我们换另一种方式,查看一下元类是否真实存在:
将工程的可执行文件,拖入MachOView工具中,查看oc底层文件
我们在符号表中搜索class关键字,发现了_OBJC_METACLASS_$_JSPerson,说明编译器确实在编译期生成了元类对象。
现在我们都知道编译器会帮我们生成元类, 那么接下来 我们看一下元类对象是否也有isa指针? 接下来,我们用LLDB继续探索。
1:
x/4gx 0x00000001000081f8
0x1000081f8: 0x00000001000081d0 0x0000000202874d08
0x100008208: 0x0000000197304e60 0x0000802900000000
(lldb) po 0x00000001000081d0 & 0x00007ffffffffff8
LGPerson
2:
x/4gx LGPerson.class
0x1000081f8: 0x00000001000081d0 0x0000000202874d08
0x100008208: 0x0000000197304e60 0x0000802900000000
(lldb) p/x 0x00000001000081d0 & 0x00007ffffffffff8
(long) $8 = 0x00000001000081d0
(lldb) po 0x00000001000081d0
LGPerson
3:
x/4gx 0x00000001000081d0
0x1000081d0: 0x0000000202874ce0 0x0000000202874ce0
0x1000081e0: 0x0003000100515e50 0x0002e03500000000
(lldb) p/x 0x0000000202874ce0 & 0x00007ffffffffff8
(long) $10 = 0x0000000202874ce0
(lldb) po 0x0000000202874ce0
NSObject
4:
(lldb) p/x NSObject.class
(Class) $12 = 0x0000000202874d08 NSObject
(lldb) x/4gx 0x0000000202874ce0
0x202874ce0: 0x0000000202874ce0 0x0000000202874d08
0x202874cf0: 0x00070001005ba780 0x0001e03400000000
(lldb) p/x 0x0000000202874ce0 & 0x00007ffffffffff8
(long) $13 = 0x0000000202874ce0
(lldb) po 0x0000000202874ce0
NSObject
通过4次打印, 前两次为LGPerson 后两次为NSObject.
通过对LGPerson的层层探索, 最终我们找到了NSObject.
然后, 我们在打印一下LGPerson类在内存中的地址
isa走向总结:
通过上述过程,来绘制一个简单的流程图:
通过对象的isa, 我们探索到了类. 再通过类的isa, 探索到元类, 继续探索到根元类,也就是NSObject, 接下来就形成了闭环,回归NSObject.
通过这些探索,我们统一思想,iOS里,任何对象,他的元类的父类,都是根元类,也就是NSObject,所以NSObject是一切类的基础。
类继承链:
iOS 中,类是由继承关系的。但是最终的父类是谁,父类又源至谁,我们都比较模糊,接下来,我们就彻底理清一下这些继承关系. 基本概念
- 根类:在
OC中几乎所有的类都继承自NSObject类,NSObject类就是根类,根类的父类为nil - 元类:在我们平时开发中会用到类方法和实例方法,但是在底层的实现中并没有这种区分,实际上都是通过实例方法去查找的,底层为了区分这两种方法,引入元类的概念,然后将实例方法存储在类中,类方法存储在元类中。类的
isa指向元类。 - 根元类:即为根类
NSObject的isa指向的类
关系图:
总结下上图:
-
isa的指向:- 实例变量的
isa指向对应的类 - 类的
isa指向对应的元类 - 元类的
isa指向根元类 - 根元类的
isa指向自身
- 实例变量的
-
类的继承:
- 类的
superclass指向父类 - 父类的
superclass指向根类 - 根类的
superclass指向nil
- 类的
-
元类的继承:
- 元类的
superclass指向对应类的父类的元类 - 父类的元类的
superclass指向根元类 - 根元类的
superclass指向根类 - 根类的
superclass指向nil
- 元类的
补充:结构体内存平移取值
举例:
struct EBStruct {
int a; // 偏移:0, 占4字节
char b; // 偏移:4, 占1字节
int c; // 偏移:8, 占4字节
long d; // 偏移:16,占8字节(成员变量的开始位置为当前成员变量所占内存的整数倍)
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct EBStruct s;
s.a = 9;
s.b = 'n';
s.c = 8;
s.d = 7;
}
return 0;
}
取出某个成员变量的值:
- 先确定结构体变量地址
- 确定成员变量的偏移位置
- 计算成员变量的内存地址:结构体变量地址+该变量偏移位置
- 将计算得到的地址转为该变量对应的类型的指针类型(例如
a是int类型,则将计算的地址转为int *类型,这样转的目的是取出int类型的值) 下面来实际操作一下:
类的存储:
-
通过前面的分析,我们对类之间的关系和类的结构有了一个大致的了解,但是我们还不是很清楚类在内存中具体是如何存储的?
- 属性存储在哪里?
- 成员变量存储在哪里?
- 方法存储在哪里?
-
下面我们通过
LLDB结合源码的方式分析下类在内存中的存储
类的bits分析
下载好objc的源码, 并定义EBPerson类:
#import <Foundation/Foundation.h>
@interface EBPerson : NSObject {
NSString *subject;
}
@property (nonatomic, copy) NSString *ebName;
@property (nonatomic, strong) NSString *nickName;
- (void)say;
+ (void)play;
@end
@implementation EBPerson
- (**instancetype**)init{
if (self = [super init]) {
self.name = @"Er Bao";
}
return self;
}
- (void)say {}
+ (void)play {};
@end
首先从源码中搜索到类的定义:
struct objc_class : objc_object {
Class ISA; // isa指针
Class superclass; // 存储当前父类
cache_t cache; // 用于缓存指针和 vtable,加速方法的调用
class_data_bits_t bits;
// 相当于 class_rw_t 指针加上 rr/alloc 的标志,`bits` 用于存储类的方法、属性、遵循的协议等信息的地方; `class_data_bits_t 内部,包装有写入bits的方法 data(),最终返回class_rw_t的结构体`
........
}
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
我们之前可以获取LGPerson类的地址,类的地址,其实就是当前内存结构里的首地址,我们可以按照内存平移的步骤,获取类里的bits;
首先看下cache_t的定义:
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<mask_t> _maybeMask; // 4字节(uint32_t)
#if __LP64__
uint16_t _flags; // 2字节
#endif
uint16_t _occupied; // 2字节
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
// 静态变量和方法是不占用内存的
};
从cache_t的定义可以看出:
-
第一个成员变量
_bucketsAndMaybeMask为uintptr_t类型,实际为unsigned long类型,占8字节 -
第二个成员变量为联合体(联合体成员共享同一块内存)
- 联合体的第一个成员为结构体,最大占
8字节 - 联合体的第二个成员为指针类型,占
8字节 - 所以,联合体占内存大小即为最大成员占内存大小,即为
8字节
- 联合体的第一个成员为结构体,最大占
-
所以,
cache_t占内存为16字节bits数据的获取: -
通过前面介绍的结构体内存平移取值的方法可以获取
bits的值 -
在获取
bits的值之前需要先确定bits的偏移量 -
前两个成员变量
ISA和superclass都是Class类型,而Class为structobjc_class *类型,占8字节 -
第三个成员变量
cache为cache_t类型,占16字节 -
所以,成员变量
bits的偏移地址为32字节(可以通过结构体内存地址+0x20计算得到)
下面来看一下class_data_bits_t的定义:
struct class_data_bits_t {
friend objc_class;
// Values are the FAST_flags above.
uintptr_t bits;
// ...
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
// ...
};
- 从
class_data_bits_t的定义可以看出,可以通过data()获取数据,结果为class_rw_t类型 下面看一下class_rw_t的定义:
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif
explicit_atomic<uintptr_t> ro_or_rw_ext;
Class firstSubclass;
Class nextSiblingClass;
// ...
const class_ro_t *ro() const {
auto v = get_ro_or_rwe();
if (slowpath(v.is<class_rw_ext_t *>())) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
}
return v.get<const class_ro_t *>(&ro_or_rw_ext);
}
// ...
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
} else {
return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
} else {
return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
}
}
// ...
};
从上面class_rw_t的定义可以看出有几个重要的方法:
methods()properties()ro()