OC对象内存学习笔记

实例对象(instance对象)

NSObject对象创建时内存分配

NSObject的底层实现,实质上是一个结构体指针。

1
2
3
struct NSObject_IMPL {
Class isa;
};

初始化一个NSObject对象

1
NSObject *object = [[NSObject alloc] init];

NSObject 类调用alloc类方法,会向内存中申请16个字节,用来存放isa指针,但只占用8个字节(isa指针为8个字节)。object对象指向这块内存地址。

可以通过 Xcode ->Debug->View Memory 直接看内存

也可以通过LLDB指令读取内存

获取内存大小请看这里

Student对象创建时内存分配

自定义一个继承NSObject类

1
2
3
4
5
@interface Student : NSObject {
@public
int _no;
int _age;
}

转成C代码后

1
2
3
4
5
6
7
8
9
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _no;
int _age;
};
struct NSObject_IMPL {
Class isa;
};

初始化一个Student对象,并赋值

1
2
3
Student *stu = [[Student alloc] init];
stu->_no = 4;
stu->_age = 5;

[Student alloc]会向内存申请16个字节,前8个字节存isa指针,后8个字节存自己的成员变量的值。

成员变量和属性的区别

属性 = 成员变量(ivar) + setter方法 + getter方法

⚠️对象内存中只存成员变量不存方法(无论是实例方法还是对象方法),方法每个实例对象都一样,所以只需要存一份,不放在对象内存中。

实例对象的内存布局图

isa 指向其类对象,其余空间保存各级的 ivar(成员变量)

图片来自OC对象的内存布局

类对象(class对象)

获取类对象

1
2
3
4
5
6
7
NSObject *object1 = [[NSObject alloc] init];
// 通过对象方法获取
Class objectClass1 = [object1 class];
// Runtime API
Class objectClass2 = object_getClass(object1);
// 通过类方法获取
Class objectClass3 = [NSObject class];

类对象内存布局

每个类在内存中有且只有一个class对象

class对象在内存中存储的信息主要包括:

  • isa指针
  • superclass指针
  • 类的属性信息(@property)、类的对象方法信息(instance method)
  • 类的协议信息(protocol)、类的成员变量信息(ivar)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 摘自objc4-723开源源码
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable(方法缓存)
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags(获取具体的类信息)
};
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods; // 方法列表
property_array_t properties; // 属性列表
protocol_array_t protocols; // 协议列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
};
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize; // 实例对象占用的内存大小
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars; // 成员变量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
};

类对象的内存布局图

元类对象(meta-class对象)

获取元类方法

1
2
3
4
5
6
// Runtime API
Class objectMetaClass = object_getClass([NSObject class]);
// ⚠️这种方式获取到的始终是类对象,而非原类对象
Class objectMetaClass2 = [[[NSObject class] class] class];
// Runtime 还有一个函数可以判断是否是元类对象
class_isMetaClass(objectMetaClass)

元类对象

每个类在内存中有且只有一个meta-class对象

meta-class对象和class对象的内存结构是一样的,但是用途不一样,在内存中存储的信息主要包括:

  • isa指针
  • superclass指针
  • 类的类方法信息(class method)

经典总结

1.instance的isa指向class

2.class的isa指向meta-class

3.meta-class的isa指向基类的meta-class

4.class的superclass指向父类的class

如果没有父类,superclass指针为nil

5.meta-class的superclass指向父类的meta-class

基类的meta-class的superclass指向基类的class

6.instance调用对象方法的轨迹

isa找到class,方法不存在,就通过superclass找父类

7.class调用类方法的轨迹

isa找meta-class,方法不存在,就通过superclass找父类

内存相关

OC堆和栈

  • OC对象存放于堆里面(堆内存要程序员手动回收)
  • 非OC对象一般放在栈里面(栈内存会被系统自动回收)

堆里面的内存是动态分配的,所以也就需要程序员手动的去添加内存、回收内存

栈里面存放的是非对象的基本数据类型,堆内存存放着OC对象

摘自iOS 堆和栈的区别

1
2
// 实例化 object 对象
NSObject *object = [[NSObject alloc] init];

object 指针存在栈区,指向的是堆区实例对象isa的地址。

1
2
3
4
5
6
7
8
9
10
@interface Student : NSObject
@property (nonatomic, strong) NSObject *object;
@end
@implementation Student
@end
// 实例化 xiaoMing 对象
Student *xiaoMing = [[Student alloc] init];
xiaoMing.object = [[NSObject alloc] init];

这时候 object 指针是存在堆区,因为 object 作为 Student 类的成员变量,在实例化后xiaoMing这个对象会存在堆区。也就是 object 指针会从堆区指向堆区。

OC的指针是什么

isa

无论是实例对象、类对象还是元类对象,都有isa指针,isa在方法调用起到至关重要的作用。

OC的消息发送机制,给对象发送消息时没有对象方法和类方法之分。

实例对象的isa指向类对象的内存地址

  • 当调用对象方法时,通过instance的isa找到class,最后找到对象方法的实现进行调用

类对象的isa指向元类对象的内存地址

  • 当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用

元类对象的isa指向ROOT元类对象,ROOT元类对象的isa指向自己的内存地址

isa计算

从64bit开始,isa需要进行一次位运算(& ISA_MASK),才能计算出真实地址

即,实例对象的isa存的地址& ISA_MASK,才能得出类对象的内存地址。

1
2
3
4
5
6
// 摘自objc4-723开源源码
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# endif

SEL

获取内存大小

  • 创建一个实例对象,至少需要多少内存
1
2
#import <objc/runtime.h>
class_getInstanceSize([NSObject class]);
  • 创建一个实例对象,实际上分配了多少内存
1
2
#import <malloc/malloc.h>
malloc_size((__bridge const void *)obj);
  • 获取一个表达式或类型的内存大小(在编译阶段已经完成计算)
1
sizeof(expression-or-type)

底层研究

OC代码转成C/C++代码

OC的底层实现是C和C++,在研究底层的对象时,需要转成C和C++代码,以便理解其原理。

将OC代码转成C/C++代码

  • 用命令行工具定位到该OC代码文件

  • 运行命令clang -rewrite-objc main.m -o main.c

    mian.m为OC文件,main.c转成的C文件。如果要转成C++,就用cpp后缀,mian.cpp

将OC代码转成指定平台C/C++代码

  • 用命令行工具定位到该OC代码文件

  • 运行命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o mian-arm64.cpp

    模拟器(i386)、32bit(armv7)、64bit(arm64)

Apple开源源码地址

研究一些OC提供的数据类型/方法/…时,可以直接看源码。

需要C++和C的基础。

LLDB常用指令

LLDB使用篇

计算机组成原理

位、字节、字、KB

  • 位:”位(bit)”是电子计算机中最小的数据单位。每一位的状态只能是0或1。
  • 字节:8个二进制位构成1个”字节(Byte)”,它是存储空间的基本计量单位。1个字节可以储存1个英文字母或者半个汉字,换句话说,1个汉字占据2个字节的存储空间。
  • 字:”字”由若干个字节构成,字的位数叫做字长,不同档次的机器有不同的字长。例如一台8位机,它的1个字就等于1个字节,字长为8位。如果是一台16位机,那么,它的1个字就由2个字节构成,字长为16位。字是计算机进行数据处理和运算的单位。
  • KB:1KB表示1K个Byte,也就是1024个字节。

1KB = 1024 Bytes = 8192 Bits

2个16进制“位” = 1个字节

小端模式&大端模式

举一个例子,比如数字0x12 34 56 78在内存中的表示形式为:

1)大端模式:

低地址 —————–> 高地址
0x12 | 0x34 | 0x56 | 0x78

2)小端模式:

低地址 ——————> 高地址
0x78 | 0x56 | 0x34 | 0x12

iOS是小端模式

大端模式和小端模式起源很有意思。

结构体内存对齐

⚠️OC对象最少占用16个字节

操作系统在分配内存时也会做对齐(iOS 在分配内存时16个字节的倍数,16,32,64…..256)

iOS对齐系数16字节

如何理解 struct 的内存对齐?