mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-24 20:00:37 +00:00
1196 lines
36 KiB
Markdown
1196 lines
36 KiB
Markdown
# 对象在内存中的存储底层原理
|
||
|
||
## 一、 栈、堆、BSS、数据段、代码段是什么?
|
||
|
||
栈(stack):又称作堆栈,用来存储程序的局部变量(但不包括static声明的变量,static修饰的数据存放于数据段中)。除此之外,在函数被调用时,栈用来传递参数和返回值。
|
||
|
||
堆(heap):用于存储程序运行中被动态分配的内存段,它的大小并不固定,可动态的扩张和缩减。操作函数(malloc/free)
|
||
|
||
BSS段(bss segment):通常用来存储程序中未被初始化的全局变量和静态变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段输入静态内存分配
|
||
|
||
数据段(data segment):通常用来存储程序中已被初始化的全局变量和静态变量和字符串的一块内存区域
|
||
|
||
代码段(code segment):通常是指用来存储程序可执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量。
|
||
|
||

|
||
|
||
|
||
|
||
## 二、 类的本质
|
||
|
||
Demo1
|
||
|
||
```objective-c
|
||
#import <Foundation/Foundation.h>
|
||
|
||
int main(int argc, const char * argv[]) {
|
||
@autoreleasepool {
|
||
NSObject *obj = [[NSObject alloc] init];
|
||
}
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
因为 OC 本质就是 c/c++,所以转成 c/c++ 来窥探下,采用指令 `xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp`(不要采用 `clang -rewrite-objc main.m -o main.cpp` 因为这样生成的 c++ 文件由于没有指明设备和对应的架构指令集,不够准确)。
|
||
|
||
产看生成的 `main-arm64.cpp` 其中有段代码是定义结构体。
|
||
|
||
```c++
|
||
struct NSObject_IMPL {
|
||
Class isa;
|
||
};
|
||
```
|
||
|
||
然后点击 NSObject 跳转官方的声明可以看到
|
||
|
||
```c++
|
||
@interface NSObject <NSObject> {
|
||
#pragma clang diagnostic push
|
||
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
|
||
Class isa OBJC_ISA_AVAILABILITY;
|
||
#pragma clang diagnostic pop
|
||
}
|
||
// 剔除一些无效信息后
|
||
@interface NSObject {
|
||
Class isa;
|
||
}
|
||
```
|
||
|
||
因此可以知道,OC 的类底层是由 c/c++ 的继承实现的。
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/OCObjectLayoutWhenISA.png" style="zoom:45%">
|
||
|
||
由于 obj 对象没有任何属性和方法,只有一个 isa 指针,且类的本质就是结构体,所以当结构体只有1个成员时,该成员的地址值,就是该结构体的地址。
|
||
|
||
即 isa 指针值为 0x100200110,结构体地址为 0x100200110,obj 指针值为 0x100200110。
|
||
|
||
|
||
|
||
Demo2
|
||
|
||
```objective-c
|
||
@interface Student : NSObject
|
||
{
|
||
@public
|
||
int _no;
|
||
int _age;
|
||
}
|
||
@end
|
||
|
||
@implementation Student
|
||
@end
|
||
|
||
int main(int argc, const char * argv[]) {
|
||
@autoreleasepool {
|
||
Student *st = [[Student alloc] init];
|
||
st->_no = 1;
|
||
st->_age = 29;
|
||
}
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
采用指令 `xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp` 转换为 c++ 代码
|
||
|
||
```c++
|
||
struct NSObject_IMPL {
|
||
Class isa;
|
||
};
|
||
|
||
struct Student_IMPL {
|
||
struct NSObject_IMPL NSObject_IVARS; // 父类的所有 ivars。由于 Student 父类是 NSObject,所以其实只有 isa
|
||
int _no;
|
||
int _age;
|
||
};
|
||
```
|
||
|
||
`struct NSObject_IMPL NSObject_IVARS;` 代表父类的所有 ivars。由于 Student 父类是 NSObject,所以其实只有 isa
|
||
|
||
由于 `NSObject_IMPL` 结构体只有1个 isa 成员,所以上面代码等价于
|
||
|
||
```c++
|
||
struct Student_IMPL {
|
||
Class isa;
|
||
int _no;
|
||
int _age;
|
||
};
|
||
```
|
||
|
||
类的本质是结构体,结构体成员内存紧挨着。内存布局如图所示:
|
||
|
||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StudentClassLayout.png" style="zoom:45%">
|
||
|
||
|
||
|
||
|
||
|
||
如果上述结论正确,那是不是可以声明一个 ` Student_IMPL` 类型的结构体指针,指向 st 指针指向的对象。然后通过结构体指针访问成员变量,看看取值是不是正确的
|
||
|
||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StructPointerVistorClassIvars.png" style="zoom:25%">
|
||
|
||
发现是可以正确访问的。
|
||
|
||
为什么 `class_getInstanceSize` 打印出16?因为本质是计算所有 ivars 的内存大小,1个 isa,2个 int 就是16.
|
||
|
||
而 `malloc_size` 是系统真正分配的,由于最小分配16,所以刚好就是16.
|
||
|
||
|
||
|
||
Demo3
|
||
|
||
```objective-c
|
||
@interface Person : NSObject
|
||
{
|
||
@public
|
||
int _age;
|
||
}
|
||
@end
|
||
|
||
@implementation Person
|
||
@end
|
||
|
||
@interface Student : Person
|
||
{
|
||
@public
|
||
int _no;
|
||
}
|
||
@end
|
||
|
||
@implementation Student
|
||
|
||
@end
|
||
|
||
int main(int argc, const char * argv[]) {
|
||
@autoreleasepool {
|
||
Person *p = [[Person alloc] init];
|
||
NSLog(@"%zd", class_getInstanceSize([Person class])); // 16
|
||
NSLog(@"%zd", malloc_size((__bridge const void *)p)); // 16
|
||
Student *st = [[Student alloc] init];
|
||
NSLog(@"%zd", class_getInstanceSize([Student class])); // 16
|
||
NSLog(@"%zd", malloc_size((__bridge const void *)st)); // 16
|
||
}
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
采用指令 `xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp` 转换为 c++ 代码
|
||
|
||
```c++
|
||
struct NSObject_IMPL {
|
||
Class isa;
|
||
};
|
||
|
||
struct Person_IMPL {
|
||
struct NSObject_IMPL NSObject_IVARS;
|
||
int _age;
|
||
};
|
||
|
||
struct Student_IMPL {
|
||
struct Person_IMPL Person_IVARS;
|
||
int _no;
|
||
};
|
||
```
|
||
|
||
为什么 `class_getInstanceSize([Person class])` 也是16,不是8+4吗?因为存在内存对齐,结构体的大小必须是最大成员大小的倍数(Person 中也就是8的倍数)
|
||
|
||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StudentClassExtendsFromPersonClass.png" style="zoom:25%">
|
||
|
||
|
||
|
||
Demo4
|
||
|
||
```objective-c
|
||
@interface Person : NSObject
|
||
{
|
||
@public
|
||
int _age;
|
||
int _no;
|
||
int _height;
|
||
}
|
||
@end
|
||
|
||
@implementation Person
|
||
@end
|
||
|
||
int main(int argc, const char * argv[]) {
|
||
@autoreleasepool {
|
||
Person *p = [[Person alloc] init];
|
||
NSLog(@"%zd", class_getInstanceSize([Person class])); // 24
|
||
NSLog(@"%zd", malloc_size((__bridge const void *)p)); // 32
|
||
}
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
采用指令 `xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp` 转换为 c++ 代码
|
||
|
||
```c++
|
||
struct NSObject_IMPL {
|
||
Class isa;
|
||
};
|
||
struct Person_IMPL {
|
||
struct NSObject_IMPL NSObject_IVARS;
|
||
int _age;
|
||
int _no;
|
||
int _height;
|
||
};
|
||
// ->
|
||
struct Person_IMPL {
|
||
Class isa;
|
||
int _age;
|
||
int _no;
|
||
int _height;
|
||
};
|
||
```
|
||
|
||
2个问题:
|
||
|
||
- `class_getInstanceSize` 不是一个 isa 8 + 3个 Int 4 = 8 + 3 * 4 = 20吗?查看源码知道 `class_getInstanceSize` 返回的是内存对齐后的成员变量内存大小
|
||
- `malloc_size` 为什么是32?只需要24字节,为什么分配32?
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
## 三、研究下对象在内存中如何存储?
|
||
|
||
```objective-c
|
||
Person *p1 = [Person new]
|
||
```
|
||
|
||
看这行代码,先来看几个注意点:
|
||
|
||
new底层做的事情:
|
||
|
||
* 在堆内存中申请1块合适大小的空间
|
||
* 在这块内存上根据类模版创建对象。类模版中定义了什么属性就依次把这些属性声明在对象中;对象中还存在一个属性叫做 **isa**,是一个指针,指向对象所属的类在代码段中地址
|
||
* 初始化对象的属性。这里初始化有几个原则:
|
||
* 如果属性的数据类型是基本数据类型则赋值为 0
|
||
* 如果属性的数据类型是 C 语言的指针类型则赋值为 NULL
|
||
* 如果属性的数据类型为 OC 的指针类型则赋值为 nil
|
||
|
||
* 返回堆空间上对象的地址
|
||
|
||
|
||
|
||
注意:
|
||
|
||
* 对象只有属性,没有方法。包括类本身的属性和一个指向代码段中的类isa指针
|
||
* 如何访问对象的属性?指针名->属性名;本质:根据指针名找到指针指向的对象,再根据属性名查找来访问对象的属性值
|
||
* 如何调用方法?[指针名 方法];本质:根据指针名找到指针指向的对象,再发现对象需要调用方法,再通过对象的isa指针找到代码段中的类,再调用类里面方法
|
||
|
||
为什么不把方法存储在对象中?
|
||
|
||
* 因为以类为模版创建的对象只有属性可能不相同,而方法相同,如果堆区的对象里面也保存方法的话就会很重复,浪费了堆空间,因此将方法存储与代码段
|
||
|
||
* 所以一个类创建的n个对象的isa指针的地址值都相同,都指向代码段中的类地址
|
||
|
||
做个小实验
|
||
|
||
```Objective-C
|
||
#import <Foundation/Foundation.h>
|
||
@interface Person : NSObject{
|
||
@public
|
||
int _age;
|
||
NSString *_name;
|
||
int *p;
|
||
}
|
||
|
||
-(void)sayHi;
|
||
@end
|
||
|
||
@implementation Person
|
||
|
||
-(void)sayHi{
|
||
NSLog(@"Hi, %@",_name);
|
||
}
|
||
|
||
@end
|
||
|
||
int main(int argc, const char * argv[]) {
|
||
Person *p1 = [Person new];
|
||
Person *p2 = [Person new];
|
||
Person *p3 = [Person new];
|
||
p1->_age = 20;
|
||
p2->_age = 20;
|
||
|
||
[p1 sayHi];
|
||
[p2 sayHi];
|
||
[p3 sayHi];
|
||
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
`Person *p1 = [Person new];` 这句代码在内存分配原理如下图所示
|
||
|
||

|
||
|
||
**结论**
|
||
|
||

|
||

|
||
|
||
**可以 看到Person类的3个对象p1、p2、p3的isa的值相同。**
|
||
|
||
|
||
|
||
## 四、一个对象占用多少内存空间?
|
||
|
||
Demo
|
||
|
||
```objective-c
|
||
int main(int argc, const char * argv[]) {
|
||
@autoreleasepool {
|
||
NSObject *obj = [[NSObject alloc] init];
|
||
// 获取类的实例对象的成员变量所占用内存大小
|
||
NSLog(@"%zd", class_getInstanceSize([NSObject class])); // 8
|
||
// 获取 obj 指针,所指向内存大小
|
||
NSLog(@"%zd", malloc_size((__bridge const void *)obj)); // 16
|
||
}
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
为什么一个是8,一个是16?查看 objc 源代码可以看到:
|
||
|
||
```objectivec
|
||
size_t class_getInstanceSize(Class cls)
|
||
{
|
||
if (!cls) return 0;
|
||
return cls->alignedInstanceSize();
|
||
}
|
||
// Class's ivar size rounded up to a pointer-size boundary.
|
||
uint32_t alignedInstanceSize() {
|
||
return word_align(unalignedInstanceSize());
|
||
}
|
||
```
|
||
|
||
```objectivec
|
||
id class_createInstance(Class cls, size_t extraBytes)
|
||
{
|
||
return _class_createInstanceFromZone(cls, extraBytes, nil);
|
||
}
|
||
|
||
static __attribute__((always_inline))
|
||
id
|
||
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
|
||
bool cxxConstruct = true,
|
||
size_t *outAllocatedSize = nil)
|
||
{
|
||
if (!cls) return nil;
|
||
|
||
assert(cls->isRealized());
|
||
|
||
// Read class's info bits all at once for performance
|
||
bool hasCxxCtor = cls->hasCxxCtor();
|
||
bool hasCxxDtor = cls->hasCxxDtor();
|
||
bool fast = cls->canAllocNonpointer();
|
||
|
||
size_t size = cls->instanceSize(extraBytes);
|
||
if (outAllocatedSize) *outAllocatedSize = size;
|
||
|
||
id obj;
|
||
if (!zone && fast) {
|
||
obj = (id)calloc(1, size);
|
||
if (!obj) return nil;
|
||
obj->initInstanceIsa(cls, hasCxxDtor);
|
||
}
|
||
else {
|
||
if (zone) {
|
||
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
|
||
} else {
|
||
obj = (id)calloc(1, size);
|
||
}
|
||
if (!obj) return nil;
|
||
|
||
// Use raw pointer isa on the assumption that they might be
|
||
// doing something weird with the zone or RR.
|
||
obj->initIsa(cls);
|
||
}
|
||
|
||
if (cxxConstruct && hasCxxCtor) {
|
||
obj = _objc_constructOrFree(obj, cls);
|
||
}
|
||
|
||
return obj;
|
||
}
|
||
|
||
size_t instanceSize(size_t extraBytes) {
|
||
size_t size = alignedInstanceSize() + extraBytes;
|
||
// CF requires all objects be at least 16 bytes.
|
||
if (size < 16) size = 16;
|
||
return size;
|
||
}
|
||
```
|
||
|
||
`alloc` 本质上调用的就是 `_objc_rootAllocWithZone` ,继续查看源码
|
||
|
||
```c++
|
||
// NSObject.mm
|
||
id
|
||
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
|
||
{
|
||
id obj;
|
||
|
||
#if __OBJC2__
|
||
// allocWithZone under __OBJC2__ ignores the zone parameter
|
||
(void)zone;
|
||
obj = class_createInstance(cls, 0);
|
||
#else
|
||
if (!zone) {
|
||
obj = class_createInstance(cls, 0);
|
||
}
|
||
else {
|
||
obj = class_createInstanceFromZone(cls, 0, zone);
|
||
}
|
||
#endif
|
||
|
||
if (slowpath(!obj)) obj = callBadAllocHandler(cls);
|
||
return obj;
|
||
}
|
||
```
|
||
|
||
继续调用的是 `class_createInstance`
|
||
|
||
```c++
|
||
// objc-runtime-new.mm
|
||
id
|
||
class_createInstance(Class cls, size_t extraBytes)
|
||
{
|
||
return _class_createInstanceFromZone(cls, extraBytes, nil);
|
||
}
|
||
|
||
static __attribute__((always_inline))
|
||
id
|
||
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
|
||
bool cxxConstruct = true,
|
||
size_t *outAllocatedSize = nil)
|
||
{
|
||
if (!cls) return nil;
|
||
|
||
assert(cls->isRealized());
|
||
|
||
// Read class's info bits all at once for performance
|
||
bool hasCxxCtor = cls->hasCxxCtor();
|
||
bool hasCxxDtor = cls->hasCxxDtor();
|
||
bool fast = cls->canAllocNonpointer();
|
||
|
||
size_t size = cls->instanceSize(extraBytes);
|
||
if (outAllocatedSize) *outAllocatedSize = size;
|
||
|
||
id obj;
|
||
if (!zone && fast) {
|
||
obj = (id)calloc(1, size);
|
||
if (!obj) return nil;
|
||
obj->initInstanceIsa(cls, hasCxxDtor);
|
||
}
|
||
else {
|
||
if (zone) {
|
||
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
|
||
} else {
|
||
obj = (id)calloc(1, size);
|
||
}
|
||
if (!obj) return nil;
|
||
|
||
// Use raw pointer isa on the assumption that they might be
|
||
// doing something weird with the zone or RR.
|
||
obj->initIsa(cls);
|
||
}
|
||
|
||
if (cxxConstruct && hasCxxCtor) {
|
||
obj = _objc_constructOrFree(obj, cls);
|
||
}
|
||
|
||
return obj;
|
||
}
|
||
```
|
||
|
||
可以看到调用的是 c 的 `obj = (id)calloc(1, size);`,其中 size 是前面计算好的,继续看看这个 size 的计算 `size_t size = cls->instanceSize(extraBytes);`
|
||
|
||
```c++
|
||
// objc-runtime-new.h
|
||
size_t instanceSize(size_t extraBytes) {
|
||
size_t size = alignedInstanceSize() + extraBytes;
|
||
// CF requires all objects be at least 16 bytes.
|
||
if (size < 16) size = 16;
|
||
return size;
|
||
}
|
||
```
|
||
|
||
可以看到计算好 size 之后有个判断,如果小于16,则赋值为16,也就是最小为16(`CF requires all objects be at least 16 bytes`)。
|
||
|
||
结论:我们用2种方式获取内存大小,其中
|
||
|
||
- `class_getInstanceSize([NSObject class])` :8,返回实例对象内存对齐后的的成员变量所占用的内存大小(即代码注释的 `Class's ivar size rounded up to a pointer-size boundary.` ),一个空对象,只有 isa 指针,所以只有8字节。可以理解为 **创建一个对象,至少需要多少内存**
|
||
- `malloc_size((__bridge const void *)obj)` :16,Apple 规定,对象至少16个字节。但是只有一个 isa,所以只占用8个字节。
|
||
内存对齐:结构体的最终大小必须是最大成员的倍数。可以理解为**创建一个对象,实际上分配了多少内存**
|
||
|
||
- 系统分配了16个字节给 NSObject 对象(通过 malloc_size 函数获得)
|
||
- 但 NSObject 对象内部只使用了8个字节的空间(64位环境下,通过 class_getInstanceSize 函数获得)
|
||
|
||
|
||
|
||
## 五、属性和方法
|
||
|
||
```objective-c
|
||
@interface Person : NSObject
|
||
{
|
||
@public
|
||
int _age;
|
||
}
|
||
@property (nonatomic, assign) NSInteger height;
|
||
@end
|
||
```
|
||
|
||
采用指令 `xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp`
|
||
|
||
```c++
|
||
struct NSObject_IMPL {
|
||
Class isa;
|
||
};
|
||
|
||
struct Person_IMPL {
|
||
struct NSObject_IMPL NSObject_IVARS;
|
||
int _age;
|
||
NSInteger _height;
|
||
};
|
||
```
|
||
|
||
我们知道 `@property` 的本质是生成一个带下划线的 ivar,即 `_height`,还有 height 的 getter、setter 方法。为什么在结构体里没有看到方法?因为对象可以存在多个,这些方法的实现需要公用,没必要每个对象里都保存一份。
|
||
|
||
|
||
|
||
## 五、类继承的本质
|
||
|
||
QA: 结构体计算大小为什么需要内存对齐?
|
||
|
||
iOS 分配内存,为什么需要内存对齐?libmalloc 可以看到至少是16的倍数。
|
||
|
||
写一个最基础的 Person 类
|
||
|
||
```objectivec
|
||
@interface Person : NSObject
|
||
@end
|
||
|
||
@implementation Person
|
||
@end
|
||
```
|
||
|
||
clang 转为 c 代码看看,因为同样的代码经过 clang 后转成 c/c++ 后,不同平台具有不同的实现,所以为了精确研究 iOS,最好指明 arm64 架构后再研究,具体指令为:`xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp`
|
||
|
||
```c
|
||
struct NSObject_IMPL {
|
||
Class isa;
|
||
}
|
||
|
||
struct Person_IMPL {
|
||
struct NSObject_IMPL NSObject_IVARS;
|
||
};
|
||
```
|
||
|
||
如果给 Person 增加属性
|
||
|
||
```objective-c
|
||
@interface Person : NSObject
|
||
@property (nonatomic, assign) double height;
|
||
@property (nonatomic, assign) double weight;
|
||
@property (nonatomic, assign) int salary;
|
||
- (void)test;
|
||
@end
|
||
|
||
@implementation Person
|
||
@end
|
||
```
|
||
|
||
创建一个继承自 Person 的 Student 类
|
||
|
||
```objective-c
|
||
@interface Student : Person
|
||
@property (nonatomic, assign) NSInteger score;
|
||
- (void)test;
|
||
@end
|
||
|
||
@implementation Student
|
||
@end
|
||
```
|
||
|
||
利用 `xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc Person.m -o Person-arm64.cpp` 转换为 c++,将主要的摘出来
|
||
|
||
```c++
|
||
struct NSObject_IMPL {
|
||
Class isa;
|
||
};
|
||
|
||
struct Person_IMPL {
|
||
struct NSObject_IMPL NSObject_IVARS;
|
||
int _salary;
|
||
double _height;
|
||
double _weight;
|
||
};
|
||
|
||
struct Student_IMPL {
|
||
struct Person_IMPL Person_IVARS;
|
||
NSInteger _score;
|
||
};
|
||
```
|
||
|
||
结构体 `Person_IMPL` 等价于
|
||
|
||
```c++
|
||
struct Person_IMPL {
|
||
Class isa;
|
||
int _salary;
|
||
double _height;
|
||
double _weight;
|
||
};
|
||
```
|
||
|
||
结构体 `Student_IMPL` 等价于
|
||
|
||
```c++
|
||
struct Student_IMPL {
|
||
Class isa;
|
||
int _salary;
|
||
double _height;
|
||
double _weight;
|
||
NSInteger _score;
|
||
};
|
||
```
|
||
|
||
首先我们可以知道一个 OC 类的属性可以写多种数据类型,那么大概率是 C 结构体实现。用 clang 转为 c 可以得到印证。另外存在类的继承关系的时候:**子类结构体中第一个信息是父类结构体对象;其次是当前子类自己的信息;根节点一定是 NSObject_IMPL 结构体;且其中只有 `Class isa`**。也就是说,**一个实例对象,内部的第一个成员就是 isa 指针,其次是父类属性,最后的自己的属性。且 isa 指针地址就是当前实例对象的地址**。
|
||
|
||
|
||
|
||
观察 clang 转换后的 c 代码,发现 property 没有看到 setter、getter 方法。为什么这么设计?
|
||
**方法不会存储到实例对象中去的。因为方法在各个对象中是通用的,方法存储在类对象的方法列表中。**
|
||
|
||
```objectivec
|
||
@interface Person:NSObject
|
||
{
|
||
int _height;
|
||
int _age;
|
||
int _weight;
|
||
}
|
||
@end
|
||
|
||
@implementation Person
|
||
@end
|
||
|
||
struct NSObject_IMPL {
|
||
Class isa;
|
||
};
|
||
|
||
struct PersonIMPL {
|
||
struct NSObject_IMPL ivars;
|
||
int _height;
|
||
int _age;
|
||
int _weight;
|
||
};
|
||
|
||
struct PersonIMPL person = {};
|
||
Person *p = [[Person alloc] init];
|
||
NSLog(@"%zd", class_getInstanceSize([Person class])); // 24,这个数值代表我们这个类,这个结构体,创建出来至少只需要24字节.
|
||
NSLog(@"%zd", sizeof(person)); // 24,这个数值代表我们这个类,这个结构体只需要24字节就够
|
||
NSLog(@"%zd", malloc_size((__bridge const void *)p)); // 32。iOS 系统会做优化,比如为了加速访问速度,会按照16的倍数进行分配。
|
||
```
|
||
|
||
`class_getInstanceSize`这个数值代表我们这个类,这个结构体,创建出来至少只需要24字节,`malloc_size` iOS 系统会做优化,比如为了加速访问速度,会按照16的倍数进行分配。
|
||
|
||
iOS 中,系统分配内存,都是16的倍数。pageSize?系统在分配内存的时候也存在内存对齐。
|
||
GUN 都存在内存对齐这个概念。
|
||
`sizeof` 本质是运算符。在 Xcode 编译后就替换为真正的值。通过指令 `xcrun --sdk iphoneos clang -arch arm64 -S -emit-llvm ViewController.m -o ViewController.ll` 查看 IR。
|
||
|
||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcoedeViewSizeOfViaAssembly.png" style="zoom:25%">
|
||
|
||
|
||
|
||
实例对象:
|
||
类对象:isa、superclass、属性信息、对象方法信息、协议信息、成员变量信息...
|
||
元类对象:存储 isa、superclass、类方法信息...
|
||
一个实例对象只有一个类对象,一个实例对象只有一个元类对象。 `class_isMetaClass()`判断一个类是否为元类对象
|
||
|
||
`objc_getClass()` 如果传递 instance 实例对象,返回 class 类对象;传递 Class 类对象,返回 meta-class 元类对象;传递 meta-class 元对象,则返回 NSObject(基类)的 meta-class 对象
|
||
|
||

|
||
|
||
instance 的 isa 指向 Class。当调用方法时,通过 instance 的 isa 找到 Class,最后找到对象方法的实现进行调用
|
||
class 的 isa 指向 meta-class。当调用类方法的时,通过 class 的 isa 找到 meta-class,最后找到类方法进行调用。
|
||
|
||

|
||
|
||
当 Student 实例对象调用 Person 的对象方法时,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,在类对象的对象方法列表中找到方法实现并调用。
|
||
当 Stduent 实例对象调用 init 方法时候,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,找到 Person Claas 的 superClass 到 NSObject 类对象,在 NSObject 类对象的方法列表中找到 `init` 方法并调用。
|
||
|
||

|
||
|
||
当 Stduent 对象调用类方法的时候,先根据 isa 找到 Student 的元类对象,然后在元类对象的 superclass 找到 Person 的元类对象,再根据 Person 元类对象的 superClass 找到 NSObject 的元类对象。最后找到元类对象的方法列表,调用到对象方法。
|
||
|
||

|
||
|
||
```objectivec
|
||
@interface Student : NSObject
|
||
@end
|
||
|
||
@implementation Person
|
||
@end
|
||
|
||
@interface NSObject(TestMessage)
|
||
@end
|
||
|
||
@implementation NSObject(TestMessage)
|
||
|
||
- (void)test
|
||
{
|
||
NSLog(@"%s", __func__);
|
||
}
|
||
@end
|
||
```
|
||
|
||
奇怪的是,我们给 Student 类对象调用 test 方法,`[Student test]` 则调用成功。是不是很奇怪?站在面向对象的角度出发,Student 类对象根据 isa 找到元对象,此时元对象方法列表中没有类对象,所以根据 superclass 找到 NSObject 元类对象,发现元类对象自身也没有类方法。但是为什么调用了 `-(void)test` 对象方法?
|
||
|
||
因为NSObject 元类对象的 superClass 继承自 NSObject 的类对象,类对象是存储对象方法的,所以定义在 NSObject 分类中的 `-(void)test` 最终会被调用。
|
||
|
||
从64位开始,iOS 的 isa 需要与 ISA_MASK 进行与位运算。& ISA_MASK 才可以得到真正的类对象地址。
|
||
为了打印和研究类对象中的 superclass、isa
|
||
|
||
```objectivec
|
||
// 实例对象
|
||
Person *p = [[Person alloc] init];
|
||
Student *s = [[Student alloc] init];
|
||
// 类对象
|
||
class pclass = object_getClass(p);
|
||
class sclass = object_getClass(s);
|
||
// Mock 系统结构体 object_class
|
||
struct mock_object_class {
|
||
class isa;
|
||
class superclass;
|
||
};
|
||
// 转换如下
|
||
struct mock_object_class *person = (__bridge mock_object_class *)[[Person alloc] init];
|
||
struct mock_object_class *student = (__bridge mock_object_class *)[[Student alloc] init];
|
||
```
|
||
|
||
如何查看类真正的结构?在 Xcode 中打印出来
|
||
思路:查看 Class 内部的数据,发现是 struct,所以我们自己定义一个 struct,去承接类对象的元类对象信息
|
||
|
||
```c
|
||
#import <Foundation/Foundation.h>
|
||
|
||
#ifndef MockClassInfo_h
|
||
#define MockClassInfo_h
|
||
|
||
# if __arm64__
|
||
# define ISA_MASK 0x0000000ffffffff8ULL
|
||
# elif __x86_64__
|
||
# define ISA_MASK 0x00007ffffffffff8ULL
|
||
# endif
|
||
|
||
#if __LP64__
|
||
typedef uint32_t mask_t;
|
||
#else
|
||
typedef uint16_t mask_t;
|
||
#endif
|
||
typedef uintptr_t cache_key_t;
|
||
|
||
struct bucket_t {
|
||
cache_key_t _key;
|
||
IMP _imp;
|
||
};
|
||
|
||
struct cache_t {
|
||
bucket_t *_buckets;
|
||
mask_t _mask;
|
||
mask_t _occupied;
|
||
};
|
||
|
||
struct entsize_list_tt {
|
||
uint32_t entsizeAndFlags;
|
||
uint32_t count;
|
||
};
|
||
|
||
struct method_t {
|
||
SEL name;
|
||
const char *types;
|
||
IMP imp;
|
||
};
|
||
|
||
struct method_list_t : entsize_list_tt {
|
||
method_t first;
|
||
};
|
||
|
||
struct ivar_t {
|
||
int32_t *offset;
|
||
const char *name;
|
||
const char *type;
|
||
uint32_t alignment_raw;
|
||
uint32_t size;
|
||
};
|
||
|
||
struct ivar_list_t : entsize_list_tt {
|
||
ivar_t first;
|
||
};
|
||
|
||
struct property_t {
|
||
const char *name;
|
||
const char *attributes;
|
||
};
|
||
|
||
struct property_list_t : entsize_list_tt {
|
||
property_t first;
|
||
};
|
||
|
||
struct chained_property_list {
|
||
chained_property_list *next;
|
||
uint32_t count;
|
||
property_t list[0];
|
||
};
|
||
|
||
typedef uintptr_t protocol_ref_t;
|
||
struct protocol_list_t {
|
||
uintptr_t count;
|
||
protocol_ref_t list[0];
|
||
};
|
||
|
||
struct class_ro_t {
|
||
uint32_t flags;
|
||
uint32_t instanceStart;
|
||
uint32_t instanceSize; // instance对象占用的内存空间
|
||
#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;
|
||
};
|
||
|
||
struct class_rw_t {
|
||
uint32_t flags;
|
||
uint32_t version;
|
||
const class_ro_t *ro;
|
||
method_list_t * methods; // 方法列表
|
||
property_list_t *properties; // 属性列表
|
||
const protocol_list_t * protocols; // 协议列表
|
||
Class firstSubclass;
|
||
Class nextSiblingClass;
|
||
char *demangledName;
|
||
};
|
||
|
||
#define FAST_DATA_MASK 0x00007ffffffffff8UL
|
||
struct class_data_bits_t {
|
||
uintptr_t bits;
|
||
public:
|
||
class_rw_t* data() {
|
||
return (class_rw_t *)(bits & FAST_DATA_MASK);
|
||
}
|
||
};
|
||
|
||
/* OC对象 */
|
||
struct mock_objc_object {
|
||
void *isa;
|
||
};
|
||
|
||
/* 类对象 */
|
||
struct mock_objc_class : mock_objc_object {
|
||
Class superclass;
|
||
cache_t cache;
|
||
class_data_bits_t bits;
|
||
public:
|
||
class_rw_t* data() {
|
||
return bits.data();
|
||
}
|
||
|
||
mock_objc_class* metaClass() {
|
||
return (mock_objc_class *)((long long)isa & ISA_MASK);
|
||
}
|
||
};
|
||
|
||
#endif /* MockClassInfo_h */
|
||
|
||
Student *stu = [[Student alloc] init];
|
||
stu->_weight = 10;
|
||
|
||
mock_objc_class *studentClass = (__bridge mock_objc_class *)([Student class]);
|
||
mock_objc_class *personClass = (__bridge mock_objc_class *)([Person class]);
|
||
|
||
class_rw_t *studentClassData = studentClass->data();
|
||
class_rw_t *personClassData = personClass->data();
|
||
|
||
class_rw_t *studentMetaClassData = studentClass->metaClass()->data();
|
||
class_rw_t *personMetaClassData = personClass->metaClass()->data();
|
||
```
|
||
|
||
|
||
|
||
## 六、 内存对齐
|
||
|
||
内存对齐是指数据在内存中存储时按照一定规则对齐到特定的地址上。在 iOS 开发中,内存对齐是为了提高内存访问的效率和性能。内存对齐的原因主要包括以下几点:
|
||
1. 提高访问速度:内存对齐可以使数据在内存中的存储更加高效,因为大部分计算机体系结构都要求数据按照特定的边界对齐,这样可以减少内存访问的次数,提高访问速度。CPU访问非对齐的内存时需要进行多次拼接。
|
||
如下图,比如需要读取从[2, 5]的内存,需要分别读取两次,然后还需要做位移的运算,最后才能得到需要的数据。这中间的损耗就会影响访问速度。
|
||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MemoryAlignReason.png" style="zoom:30%">
|
||
|
||
2. 为了方便移植。CPU是一块块的进行进行内存访问。有一些硬件平台不允许随机访问,只能访问对齐后的内存地址,否则会报异常。
|
||
很多 CPU(如基于 Alpha,IA-64,MIPS,和 SuperH 体系的)拒绝读取未对齐数据。当一个程序要求这些 CPU 读取未对齐数据时,这时 CPU 会进入异常处理状态并且通知程序不能继续执行。举个例子,在 ARM,MIPS,和 SH 硬件平台上,当操作系统被要求存取一个未对齐数据时会默认给应用程序抛出硬件异常。
|
||
3. 硬件要求:某些硬件平台对于数据的访问有特定的要求,例如ARM架构的处理器对于某些数据类型的访问需要按照特定的对齐方式进行
|
||
4. 数据结构优化:内存对齐也有助于优化数据结构的布局,使得数据在内存中的存储更加紧凑和高效。
|
||
|
||
Demo1
|
||
|
||
```objectivec
|
||
@interface Person : NSObject
|
||
{
|
||
int _age;
|
||
int _height;
|
||
}
|
||
@end
|
||
|
||
struct Person_IMPL {
|
||
Class isa;
|
||
int _age;
|
||
int _height;
|
||
};
|
||
|
||
Person *person = [[Person alloc] init];
|
||
NSLog(@"%zd", malloc_size((__bridge const void *)person)); // 16
|
||
NSLog(@"%zd", sizeof(struct Person_IMPL)); // 16
|
||
```
|
||
|
||
`isa 指针 8字节` + `int _age 4字节` + `_hright 字节` = 16 字节
|
||
|
||
Demo2
|
||
|
||
```objectivec
|
||
@interface Person : NSObject
|
||
{
|
||
int _age;
|
||
int _height;
|
||
int _no;
|
||
}
|
||
@end
|
||
|
||
struct Person_IMPL {
|
||
Class isa;
|
||
int _age;
|
||
int _height;
|
||
int _no;
|
||
};
|
||
|
||
Person *person = [[Person alloc] init];
|
||
NSLog(@"%zd", sizeof(struct Person_IMPL)); // 24
|
||
NSLog(@"%zd", malloc_size((__bridge const void *)person)); // 32
|
||
```
|
||
|
||
`isa 指针8字节` + `int _age 4字节` + `_height 4字节` + `_no 4 字节` = 20 字节,因为存在内存对齐,因为结构体本身对齐内存对齐,必须为8的倍数,所以占据24个字节的内存。结构体成员变量,内存对齐时,对齐基数必须是各个成员变量中最大字节数的一个。
|
||
|
||
结构体占据24字节,为什么运行起来后通过 `malloc_size` 得到32个字节?这个涉及到运行时内存对齐。规定 **iOS 中内存对齐以 16 的倍数为准**。
|
||
|
||
|
||
|
||
Demo
|
||
|
||
```objectivec
|
||
void *temp = malloc(4);
|
||
NSLog(@"%zd", malloc_size(temp));
|
||
// 16
|
||
```
|
||
|
||
可以看到 malloc 申请了4个字节,但是打印却看到16个字节。
|
||
|
||
查看 libmalloc 源码也可以出来分配内存最小是以16的倍数为基准进行分配的。
|
||
|
||
```c
|
||
#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, 64, 80, 96, 112, ...} */
|
||
```
|
||
|
||
为什么系统是由16字节对齐的?
|
||
|
||
成员变量占用8字节对齐,每个对象的第一个都是 isa 指针,必须要占用8字节。举例一个极端 case,假设 n 个对象,其中 m 个对象没有成员变量,只有 isa 指针占用8字节,其中的 n-m个对象既有 isa 指针,又有成员变量。每个类交错排列,那么 CPU 在访问对象的时候会耗费大量时间去识别具体的对象。很多时候会取舍,这个 case 就是时间换空间。以16字节对齐,会加快访问速度(参考链表和数组的设计)
|
||
|
||
上述是 Apple 官方的角度出发探究的,其他系统,比如 Linux 也是存在内存对齐的。由于 Linux 也是采用 GNU 的东西,所以探索下 GNU 下 glibc malloc 的实现。从[这里](https://ftp.gnu.org/gnu/glibc/)下载 glibc 源码。然后拖到 Xcode 中查看
|
||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/GlibcInXcodeProject.png" style="zoom:25%">
|
||
|
||
可以看到 GNU 源码里面,内存对齐 `MALLOC_ALIGNMENT`
|
||
在 i386 里面是16,在非 i386 里面有个判断
|
||
```c
|
||
#define MALLOC_ALIGNMENT (2 * SIZE_SZ < __alignof__ (long double) \
|
||
? __alignof__ (long double) : 2 * SIZE_SZ)
|
||
```
|
||
三目运算符的后2个结果分别是 `__alignof__ (long double)` 和 `2 * SIZE_SZ`。其中 `SIZE_SZ` 又是一个宏定义,等价于 `(sizeof (INTERNAL_SIZE_T))`,即 `2*sizeof(size_t)`
|
||
```c
|
||
#define SIZE_SZ (sizeof (INTERNAL_SIZE_T))
|
||
# define INTERNAL_SIZE_T size_t
|
||
```
|
||
在 Xcode 打印输出, `__alignof__ (long double)` 为16,`sizeof(size_t)` 为8,即 `2 * SIZE_SZ` = 16,所以不管怎么看,在 GUN 里面内存对齐一定都是16.
|
||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/GLibcMallocAlignment.png" style="zoom:25%">
|
||
Todo: 研究探索 libmalloc 源码
|
||
|
||
|
||
|
||
## 七、class 对象
|
||
|
||
```objective-c
|
||
// instance 对象,实例对象
|
||
NSObject *obj1 = [[NSObject alloc] init];
|
||
NSObject *obj2 = [[NSObject alloc] init];
|
||
// class 对象,类对象
|
||
Class cls1 = [obj1 class];
|
||
Class cls2 = [obj2 class];
|
||
Class cls3 = object_getClass(obj1);
|
||
Class cls4 = object_getClass(obj2);
|
||
Class cls5 = [NSObject class];
|
||
NSLog(@"%p %p", obj1, obj2); // 0x600000004040 0x600000004050
|
||
NSLog(@"%p %p %p %p %p", cls1, cls2, cls3, cls4, cls5); // 0x7ff85ca74270 0x7ff85ca74270 0x7ff85ca74270 0x7ff85ca74270 0x7ff85ca74270
|
||
```
|
||
|
||
- cls1...cls5 都是 NSObject 的 class 对象,也就是类对象。
|
||
- 它都是同一个对象,每个类在内存中有且只有一个 class 对象
|
||
|
||
|
||
|
||
Class 对象在内存中存储的信息主要包括:
|
||
|
||
- isa 指针
|
||
- superclass 指针
|
||
- 类的属性信息(@property)、类的对象方法信息(instance method)
|
||
- 类的协议信息(@protocol、)类的成员变量信息(ivars)
|
||
|
||
|
||
|
||
## 八、元类对象
|
||
|
||
Objective-C 中对象分维三类:
|
||
|
||
- instance 对象,实例对象。例如 `NSObject *obj1 = [[NSObject alloc] init];`
|
||
- class 对象,类对象。例如 `Class cls1 = [obj1 class];`
|
||
- 元类对象(meta-class)。例如 `Class metaClass = object_getClass(cls1);`
|
||
|
||
如何获取元类对象?
|
||
|
||
利用 runtime `object_getClass`API,传入类对象获取。例如 `Class objectMetaClass = object_getClass([NSObject class])`
|
||
|
||
不可以通过2次调用 class 方法获取 meta-class 对象。调用 class 方法只可以获取到 class 对象。
|
||
|
||
- 每个类在 内存中有且只有一个 meta-class 对象
|
||
|
||
- meta-class 对象和 Class 对象的内存结构是一样的(都是 Class),但是用途不一样,在内存中存储的信息主要包括:
|
||
|
||
- isa 指针
|
||
- superclass 指针
|
||
- 类的类方法信息(class method)
|
||
|
||
如何判断是否是元类对象 `class_isMetaClass()`
|
||
|
||
Demo:
|
||
|
||
```objective-c
|
||
// instance 对象,实例对象
|
||
NSObject *obj1 = [[NSObject alloc] init];
|
||
NSObject *obj2 = [[NSObject alloc] init];
|
||
// class 对象,类对象
|
||
// class 方法返回的就是类对象
|
||
Class cls1 = [obj1 class];
|
||
Class cls2 = [obj2 class];
|
||
Class cls3 = object_getClass(obj1);
|
||
Class cls4 = object_getClass(obj2);
|
||
Class cls5 = [NSObject class];
|
||
NSLog(@"%p %p", obj1, obj2);
|
||
NSLog(@"%p %p %p %p %p", cls1, cls2, cls3, cls4, cls5);
|
||
|
||
// 将类对象当作参数传入,获取元类对象(meta-class)
|
||
Class metaClass = object_getClass(cls1);
|
||
Class metaClass2 = [cls2 class];
|
||
NSLog(@"%p %p", metaClass, metaClass2);
|
||
|
||
BOOL isMetaClass = class_isMetaClass(metaClass);
|
||
NSLog(@"%@", isMetaClass ? @"是元类对象" : @"不是元类对象");
|
||
|
||
isMetaClass = class_isMetaClass(metaClass2);
|
||
NSLog(@"%@", isMetaClass ? @"是元类对象" : @"不是元类对象");
|
||
|
||
// console
|
||
0x60000000c030 0x60000000c040
|
||
0x7ff85ec7c270 0x7ff85ec7c270 0x7ff85ec7c270 0x7ff85ec7c270 0x7ff85ec7c270
|
||
0x7ff85ec7c220 0x7ff85ec7c270
|
||
是元类对象
|
||
不是元类对象
|
||
```
|
||
|
||
objc 源代码中:
|
||
|
||
```c++
|
||
Class object_getClass(id obj)
|
||
{
|
||
// 传入的如果是实例对象,则返回 class 类对象
|
||
// 传入的如果是 class 类对象,则返回 meta-class 元类对象
|
||
// 传入的如果是 meta-class 元类对象,返回 NSObject(基类)的 meta-class 元类对象
|
||
if (obj) return obj->getIsa();
|
||
else return Nil;
|
||
}
|
||
|
||
Class objc_getClass(const char *aClassName)
|
||
{
|
||
if (!aClassName) return Nil;
|
||
|
||
// NO unconnected, YES class handler
|
||
return look_up_class(aClassName, NO, YES);
|
||
}
|
||
```
|
||
|
||
`object_getClass`:
|
||
|
||
- 传入的如果是实例对象,则返回 class 类对象
|
||
- 传入的如果是 class 类对象,则返回 meta-class 元类对象
|
||
- 传入的如果是 meta-class 元类对象,返回 NSObject(基类)的 meta-class 元类对象
|
||
|
||
`-(Class)class`、`+(Class)class`:返回的是类对象
|
||
|
||
|
||
|
||
|
||
|
||
## QA
|
||
|
||
### 对象的 isa 指向什么?
|
||
- Instance 对象的 isa 指向类对象(Class)
|
||
- Class 对象的 isa 指向元类对象(Meta-Class)
|
||
- Meta-Class 对象的 isa 指向基类的元类对象(Meta-Class)
|
||
|
||
但注意:
|
||
- 实例对象的 isa 需要与 ISA_MASK 按位与之后才可以得到类对象的地址值。
|
||
- 类对象的 isa 需要与 ISA_MASK 按位与之后才可以得元类对象的地址值。
|
||
|
||
|
||
### OC 的类信息存放在哪?
|
||
- Instance 对象:成员变量具体的值,存放在实例对象中
|
||
```
|
||
struct NSObject_IMPL {
|
||
class isa;
|
||
}
|
||
|
||
struct Person_IMPL {
|
||
struct NSObject_IMPL NSObject_IAVRS;
|
||
int _age;
|
||
int _height;
|
||
}
|
||
|
||
//
|
||
struct Person_IMPL {
|
||
class isa;
|
||
int _age;
|
||
int _height;
|
||
}
|
||
```
|
||
- Class 对象:属性信息、对象方法信息、成员变量信息、协议信息、superclass、isa 存放在类对象中。
|
||
- Meta-Class 对象:类方法信息,存放在元类对象中。
|
||
|