OC对象的本质
OC中的对象,底层都是通过 C/C++ 结构体进行实现的
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *object = [[NSObject alloc] init];
NSLog(@"%@", object);
}
return 0;
}
复制代码
验证:OC对象的本质
把OC代码转化为 C/C++ 代码 ↓↓↓
打开终端, cd 到指定的OC文件所在目录下,键入指令
clang -rewrite-objc 对应OC文件.m -o OC文件同名.cpp
也可以指定架构,键入指令
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc 对应OC文件.m -o OC文件同名.cpp
打开对应的 .cpp 文件,搜索 nsobject_impl {
,可以看到对应代码块
...
typedef struct objc_object NSObject;
...
struct NSObject_IMPL {
Class isa;
};
...
复制代码
NSObject_IMPL就是NSObject结构体的实现,在结构体内部
Class isa; // Class的本质为 typedef struct objc_class *Class;
说白了,isa就是一个Class类型的指针
思考:对象在内存中的分配和布局
一个NSObject对象占用多大的内存空间?
一个NSObject对象的结构体中,只有isa一个成员,isa是指针类型。因此,在64位架构中,占用8个字节的内存。在这里,结构体中只有一个成员,所以isa的地址,就是结构体的地址,就是NSObject对象的地址。
struct NSObject_IMPL {
Class isa;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *object = [[NSObject alloc] init];
struct NSObject_IMPL *objStruct = (__bridge struct NSObject_IMPL *)(object);
NSLog(@"实例对象占用的内存空间: %zdByte", class_getInstanceSize(NSObject.class));
NSLog(@"NSObject的对象地址: %p", object); // object 指向内存中NSObject的对象地址
NSLog(@"isa变量的地址: %p", &(objStruct->isa)); // isa的地址 == 结构体NSObject_IMPL的地址 == NSObject的对象地址
}
return 0;
}
复制代码
一个自定义类的对象占用多大的内存空间?
@interface Animal : NSObject
{
@public
int _age;
}
@end
@implementation Animal
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Animal *animal = [[Animal alloc] init];
animal->_age = 2;
}
return 0;
}
复制代码
窥探自定义类的内部结构,首先在终端,使用指令
clang -rewrite-objc 对应OC文件.m -o OC文件同名.cpp
然后,找到对应实现的代码块
struct Animal_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
};
复制代码
结构体中第一个成员是 NSObject_IMPL 类型。而 NSObject_IMPL 内部其实就是 Class isa
struct NSObject_IMPL {
Class isa;
};
复制代码
因此,上面 Animal_IMPL 这个结构体就等价于
struct Animal_IMPL {
Class isa;
int _age;
};
复制代码
先来分析 animal 对象的地址,是不是和上面NSobject对象的地址一样,也等同于其内部成员isa的存储地址
struct Animal_IMPL {
Class isa;
int _age;
};
@interface Animal : NSObject
{
@public
int _age;
}
@end
@implementation Animal
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Animal *animal = [[Animal alloc] init];
animal->_age = 2;
struct Animal_IMPL *animalStruct = (__bridge struct Animal_IMPL *)animal;
NSLog(@"animal对象的内存地址: %p", animal);
NSLog(@"animal结构体的内存地址: %p", animalStruct);
NSLog(@"animal->isa的内存地址: %p", &(animalStruct->isa));
}
return 0;
}
复制代码
通过上图打印结果,可以看到三者的内存地址是一样的,进一步思考到底是谁决定了结构体在内存中的存储地址?
我们交换一下接口体内部成员的顺序,得到
struct Animal_IMPL {
int _age;
Class isa;
};
复制代码
再次打印结果会发现,地址值出现了变化,结构体中isa的地址不再和对象地址相同
这时,当我们打印成员 _age 的地址时,会发现此时对象地址和结构体成员_age的地址是相同的。
在结构体中,结构体的内存地址,也就是说结构体的首地址,和结构体内第一个成员的地址是相同的。之所以如此,是取决于Struct中内存地址的连续性,当我们找到了结构体的首地址,根据地址连续性,也就能找到其他成员对应的内存地址。
在OC对象中,我们总是把isa作为对象结构体的第一个成员,所以我们说对象的地址就是isa的内存地址
接下来看一下 animal 对象所占用的内存大小,结构体 Animal_IMPL 中,isa占用8个字节,_age占用4个字节,一共 12Byte。但是,通过打印得到的结果却是 16Byte
NSLog(@"animal对象的大小: %zd", class_getInstanceSize(Animal.class));
复制代码
窥探对象的内存结构
方式一: Xcode -> Debug -> Debug Workflow -> View Memory
通过打断点,然后在Address中键入对象的内存地址,可以看到他在内存中的结构如下
根据打印结果我们找到29 81 00 00 01 80 1D 01 02 00 00 00 00 00 00 00
这一段内存,
前8个字节29 81 00 00 01 80 1D 01
是isa,
中间4个字节02 00 00 00
是_age,
最后4个字节00 00 00 00
是编译器为我们填充的。
之所以会产生上面的结果,取决于结构体的内存对齐
方式二:通过lldb指令
Printing description of animal:
<Animal: 0x100536d10>
(lldb) x 0x100536d10
0x100536d10: 11 81 00 00 01 80 1d 01 01 00 00 00 00 00 00 00 ................
0x100536d20: 2d 5b 4e 53 50 69 63 6b 65 72 54 6f 75 63 68 42 -[NSPickerTouchB
(lldb) x/4xw 0x100536d10 // "/"后面表示如何读取数据,4表示读取4次,x表示以16进制的方式读取数据,w表示4个字节4个字节读取(b:byte 1字节,h:half word 2字节,w:word 4字节,g:giant word 8字节)
0x100536d10: 0x00008111 0x011d8001 0x00000001 0x00000000
(lldb) x/4dw 0x100536d10
0x100536d10: 33041
0x100536d14: 18710529
0x100536d18: 1
0x100536d1c: 0
(lldb) memory write 0x100536d18 2
(lldb) po animal->_age
2
复制代码
结构体的内存对齐
结构体字节对齐的细节和具体的编译器实现相关,但一般来说遵循3个原则:
- 变量的起始地址能够被其对齐模数整除,结构体的对齐模数取结构体最宽基本类型成员的大小和编译器默认对齐模数中较小的那一个。
- 结构体每个成员相对于起始地址的偏移量能够被其自身对齐模数整除,如果不能则在前一个成员后面补充字节。
- 结构体总体大小能够被结构体的对齐模数整除,如不能则在后面补充字节。
补充: 编译器的默认对齐值:64位,8个字节;32位,4个字节
速记:
原则1: 前面的地址必须是后面的地址正数倍,不是就补齐。
原则2: 整个Struct的地址必须是最大字节的整数倍,不是就在后面补齐。
animal对象的最宽基本类型成员的大小为8个字节,首地址存放isa指针需要8个字节,;第二个地址要存放_age成员变量需要4个字节,根据原则一,8是4的整数倍,符合原则1,不需要补齐;目前animal对象共占据12个字节的内存,不是最宽基本类型成员大小8个字节的整数倍,需要补齐4个字节,因此animal对象占用16个字节。
考虑下面两个结构体在64位系统中的大小
struct Test
{
char a; // 1
int b; // 4
short c; // 2
};
struct Test1
{
int b; // 4
short c; // 2
char a; // 1
};
复制代码
OC对象的分类
- instance对象(实例对象)
- class对象(类对象)
- meta-class对象(元类对象)
instance对象
instance对象在内存中存储的信息 (成员变量的值) 包括:
- isa指针
- 其他成员变量
instance对象就是通过类alloc出来的对象,每次调用alloc都会产生新的instance对象
NSObject *objc = [[NSObject alloc] init]; // objc 就是一个instance对象
复制代码
class对象
class对象在内存中存储的信息 (属性名称,类型…) 主要包括:
- isa指针
- superclass指针
- 类的属性信息(Property),类的成员变量信息(Ivar)
- 对象方法信息(Instance Method),类的协议信息(Protocol)
Class cls = [objc class]; // 通过class方法获取
Class cls1 = object_getClass(object1); // 通过runtime获取
复制代码
通过打印两者的地址,可以得出每一个类在内存中有且只有一个class对象
meta-class对象
meta-class对象在内存中存储的信息主要包括:
- isa指针
- superclass指针
- 类方法信息(Class Method)
Class metaCls = object_getClass(NSObject.class); // 只能通过调用runtime函数,传入类对象获取
NSLog(@"cls: %p", cls);
NSLog(@"metaCls: %p", metaCls);
NSLog(@"%p", [[[NSObject class] class] class]);
复制代码
通过打印结果,我们得出无论调用多少次class方法,都只会得到类对象
meta-class对象 和 class对象 一样,每个类在内存中有且只有一个meta-class对象
meta-class对象和class对象的内存结构是一样的,所以meta-class中也有类的属性信息,类的对象方法信息等,但是其中的值可能是空的。
isa 指针指向哪里
instance对象的isa指向class对象,当调用对象方法时,通过instance对象的isa找到class对象,最后找到对象方法的实现进行调用。
class对象的isa指向meta-class对象,当调用类方法时,通过class对象的isa找到meta-class对象,最后找到类方法的实现进行调用。
- instance的isa指向class
- class的isa指向meta-class
- meta-class的isa指向基类的meta-class,基类的isa指向自己
- class的superclass指向父类的class,如果没有父类,superclass指针为nil
- meta-class的superclass指向父类的meta-class,基类的meta-class的superclass指向基类的class
- 调用对象方法的轨迹,instance对象的isa找到class,方法不存在,就通过superclass找父类
- 调用类方法的轨迹,class对象的isa找到meta-class,方法不存在,就通过superclass找父类
验证:isa 的指向
NSObject *objc = [[NSObject alloc] init]; // instance对象
Class cls = object_getClass(objc); // class对象
Class metaCls = object_getClass(NSObject.class); // meta-class对象
复制代码
我们发现objc与objc->isa的地址不同,这是因为从64位架构开始,isa需要进行一次位运算,才能得到真正指向的地址。
位运算的值可以通过objc源码找到。
# if __arm64__
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# define ISA_MASK 0x007ffffffffffff8ULL
...
# else
# define ISA_MASK 0x0000000ffffffff8ULL
...
# endif
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
...
# endif
复制代码
通过验证我们可以看到,在位运算之后得到的值就是 类对象 的地址值,也就是说 objc->isa 指向了 类对象,同理可以得到 类对象的isa 指向 元类对象
Class内部探究
Class 其实是 objc_class 的结构体
Class cls = object_getClass(objc); // typedef struct objc_class *Class;
复制代码
在源码中,我们可以看到如下结构体,这就是class对象和meta-class对象内部的结构
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
复制代码