前言
在底层研究 - 对象的底层探索(上)已经探索完对象alloc底层原理,对象的内存对齐和结构体的内存对齐,同时也知道了结构体内顺序对结构体的内存分配大小产生影响,接下来继续探究对象的内存分布
一些用到的lldb指令
- p/x 以十六进制打印数据
- p/o 以八进制打印数据
- p/t 以二进制打印数据
- p/f 以浮点形式打印数据
- x/4gx 输出对象的内存地址,x/4gx中4代表输出4个,g代表每一个是8字节大小,x代表以16进制打印。
1、影响对象内存的因素
创建一个Person类,并且实例化一个对象并对其进行赋值,发现打印的结构是48.
@interface Person : NSObject
@property (nonatomic ,copy) NSString *name;
@property (nonatomic ,copy) NSString *hobby;
@property (nonatomic ,assign) int age;
@property (nonatomic ,assign) double hight;
@property (nonatomic ,assign) short number;
@property (nonatomic ,assign) char a;
@end
@implementation Person
@end
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
Person *p = [Person new];
p.name = @"小明";
p.hobby = @"boy";
p.hight = 1.8;
p.age = 18;
p.number = 123;
p.a = 5;
NSLog(@"%lu",malloc_size((__bridge const void *)(p)));
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
//打印结果:48
我们打个断点看看p的内存分布情况,由于第一个8字节是isa,直接从第二个8字节开始打印。
通过打印结果可以发现,第二个地址打印的值并不是我们赋值的成员变量的值,而且age、number、a这三个成员变量的值并没有发现。我们唯一的着手的就只有第二个莫名其妙的地址,尝试把它拆开分成三断打印
可以发现age、number、a这三个成员变量的值出现了,说明它们在同一个内存地址里。由此我们也可以得出一个结论,成员变量的值在存储时自动重新排序。那为什么这么做呢?可以推测是为了优化内存。
对象的内存是8字节对齐,a和age以及number总共占用1+4+2 = 7 个字节,并没有超出8字节,可以放在同一个内存地址内,从而节省了内存的消耗。
2、对象的内存分布
既然对象的属性在赋值后会自动重排,那对象本身的成员变量或者继承自父类的属性会不会也重排呢?
@interface Person : NSObject
{
@public
int age;
NSString *name;
NSString *sex;
int age1;
}
@end
@interface Person1 : NSObject
{
@public
int age;
NSString *name;
int age1;
NSString *sex;
}
@end
@interface Person2 : Person
{
@public
char a;
}
@end
@interface Person3 : Person1
{
@public
char a;
}
@end
@implementation Person
@end
@implementation Person1
@end
@implementation Person2
@end
@implementation Person3
@end
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
Person *person = [[Person alloc] init];
person->age1 = 18;
person->name = @"小明";
person->age = 19;
person->sex = @"男";
Person1* person1 = [[Person1 alloc] init];
person1->age1 = 18;
person1->name = @"小明";
person1->age = 19;
person1->sex = @"男";
Person2* person2 = [[Person2 alloc] init];
person2->age1 = 18;
person2->name = @"小明";
person2->age = 19;
person2->sex = @"男";
person2->a = 5;
Person3* person3 = [[Person3 alloc] init];
person3->age1 = 18;
person3->name = @"小明";
person3->age = 19;
person3->sex = @"男";
person3->a = 5;
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
从上面的打印结果可以得出两个结论
- 通过对比person和person1可以发现,自己声明的成员变量并不会被自动重排顺序,并且在内存中的分布是按照成员变量声明的顺序存储的
- 通过对比person2和person3可以发现,当子类继承自父类时,子类的内存是否被优化,取决于父类的成员变量位置,因为person最后一个成员变量是int,不够8字节,而person1是nsstring类型,已经是8字节。
person2的现象这其实是一个系统级别的优化,并不是重排导致
那如果是属性继承的话,会不会触发属性重排呢?
@interface Person : NSObject
@property (nonatomic ,copy) NSString *name;
@property (nonatomic ,copy) NSString *hobby;
@property (nonatomic ,assign) int age;
@property (nonatomic ,assign) double hight;
@property (nonatomic ,assign) short number;
@end
@interface Person1 : Person
@property (nonatomic ,assign) char a;
@end
@implementation Person
@end
@implementation Person1
@end
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
Person1 *p = [Person1 new];
p.name = @"小明";
p.hobby = @"boy";
p.hight = 1.8;
p.age = 18;
p.number = 123;
p.a = 5;
NSLog(@"%lu",malloc_size((__bridge const void *)(p)));
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
从上图打印的结果可以发现事实上父类自动生成的成员变量并没有和子类自动生成的成员变量一起被系统自动重排顺序,而是各自进行重排。这里出现这样的原因是当子类在继承父类的数据结构时,父类是一块连续的内存空间,子类是没有办法去修改父类的数据结构的,也就是说系统在进行属性重排的时候只是基于某一个类,并不会把子类的成员变量和父类的成员变量重排在一起。
3、位域和联合体
接下来我们先讲解下位域和联合体。
- 位域:在一个结构体中以位为单位来指定其成员所占内存,但指定的内存大小不能超过该成员类型所占的最大内存大小。
struct Struct1 {
char a;
char b;
char c;
char d;
}struct1;
struct Struct2 {
// a: 位域名 4:位域长度
char a : 1;
char b : 1;
char c : 1;
char d : 1;
}struct2;
一个正常的结构体,它所占的内存空间由它的数据结构决定,如果不使用位域,struct1占用4个字节,但struct2只占用了1个字节(实际上是1位,8位1字节)
struct Struct3 {
char a : 7;
char b : 1;
char c : 1;
char d : 1;
}struct3;
当1个字节不够存储时,会自动存入下一个字节
,也就是struct3占用了2个字节。
- 联合体:将几种不同类型的变量存放到同一段内存单元中,几个变量互相覆盖,联合体的作用是节省一定的内存空间,所占内存取决于最大成员变量,且必须是其最大成员变量(基本数据类型)的整数倍。
union Person {
char *name;
int number;
double height;
}p1;
根据规则计算出来:Person结构体的内存大小为8字节
当联合体中有数组时:
union Person {
char a[7];
int number;
double height;
}p1;
联合体Person中最大的成员变量是数组,内存占用7个字节,但因其不是基本数据类型,因此Person是double的整数倍,该联合体占8个字节。
联合体和结构体的区别:
结构体(struct)中所有变量是“共存”的,⽽联合体(union)中是各变量是“互斥”的,只能存在⼀个。struct内存空间的分配是粗放的,不管⽤不⽤,全部分配。这样带来的⼀个坏处就是对于内存的消耗要⼤⼀些。但是结构体⾥⾯的数据是完整的。联合体⾥⾯的数据只能存在⼀个,但优点是内存使⽤更为精细灵活,也节省了内存空间。
4、nonPointerIsa
有了联合体和位域的知识,我们看下objc底层是怎么使用的,来到_class_createInstanceFromZone方法
进入initInstanceIsa方法内,我们发现其内部也是调用了initIsa方法
进入initIsa方法内,我们发现了其内部就是对对象的isa指针进行初始化,同时我们发现了isa_t的数据类型
进入isa_t发现它就是联合体,根据我们掌握的联合体知识可以发现它的目的是兼容旧版本的isa(Class cls)。
如今的系统采用的是nonPointerIsa,相较于旧版本,它节省了内存空间。因为对象的isa是一个8字节的Class类型的结构体指针,主要是用来存储对象所属类对象的内存地址的,而存储类对象的内存地址不需要使用8字节这么大的内存空间,所以系统就把一些与对象息息相关的信息也存储到isa的内存空间内,而nonPointerIsa的信息都存储在ISA_BITFIELD这样一个结构体内。
# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# define ISA_MASK 0x007ffffffffffff8ULL
# define ISA_MAGIC_MASK 0x0000000000000001ULL
# define ISA_MAGIC_VALUE 0x0000000000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 0
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t weakly_referenced : 1; \
uintptr_t shiftcls_and_sig : 52; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# endif
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# error unknown architecture for packed isa
# endif
这个nonPointerIsa的结构体中有不少成员,下面我们看下这些成员变量的作用:
//nonpointer:表示是否对isa指针开启指针优化,0:纯isa指针,1:不止是类对象地址,isa包含了类信息、对象的引用计数等
uintptr_t nonpointer : 1;
//has_assoc:关联对象标志位,0没有,1存在
uintptr_t has_assoc : 1;
//has_cxx_dtor:该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象
uintptr_t has_cxx_dtor : 1;
//shiftcls:存储类指针的值。开启指针优化的情况下,在arm64(真机)架构中用33位存储类指针
uintptr_t shiftcls : 33;
//magic:用于调试器判断当前对象是真的对象还是没有初始化的空间
uintptr_t magic : 6;
//weakly_referenced:标志对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放
uintptr_t weakly_referenced : 1;
//unused:没有使用(在之前旧版本,该位置表示 deallocating: 标志对象是否正在释放内存)
uintptr_t unused : 1;
//has_sidetable_rc:是否需要使用sidetable来存储引用计数,当对象引用计数大于10时,则需要借用该变量存储进位
uintptr_t has_sidetable_rc : 1;
//extra_rc:表示该对象的引用计数值,实际上是引用计数值减1,例如,如果对象的引用计数为10,那么extra_rc为9.如果引用计数大于10,则需要使用到上面的has_sidetable_rc
uintptr_t extra_rc : 19;
知道nonPointerIsa后,我们看下如何利用nonPointerIsa来获取类对象!
5、利用isa得到类对象
通过nonPointerIsa的定义,我可以知道nonPointerIsa内存储的类对象地址空间在不同架构下占用的位域分别为52、33、44
# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
.....
uintptr_t shiftcls_and_sig : 52; ......
# else
......
uintptr_t shiftcls : 33;
.....
# endif
# elif __x86_64__
.....
uintptr_t shiftcls : 44;
......
# endif
在源码的main文件内创建一个person对象,因为是电脑是x86_64架构的,所以对象占用的位域长度是44位来存储。我们可以分别使用两种方式来获取类对象:
-
ISA_MASK
我们可以直接用源码提供的ISA_MASK & 上对象地址,就可以获得类对象 -
位运算
通过上面的分析我们已经知道isa的内存分配情况,要取出完整的类对象信息,只需要把其余的数据全部清零即可。首先右移3位到最右边,左移20位(3 + 17)到最左边,再右移17位返回原来的位置
属性是从低位往高位存,x86_64架构又是小端模式,地址低位存放在低地址(高位存放在高地址)
例如:0x12345678
-> 大端:12 34 56 78
-> 小端:78 56 34 12
6、new方法
我们经常发现创建对象的方式除了alloc方法外,还有new方法,那么二者有什么区别呢?
我们直接看下objc的源码,找到new方法
通过源码我们发现,new方法调用callAlloc函数后调用了init方法,也就是说new方法本质上就是alloc + init。
7、总结
- 对象⾥⾯存储了⼀个isa指针 + 成员变量的值,isa指针是固定的,占8个字节,所以影响对象内存的只有成员变量(属性会⾃动⽣成带下划线的成员变量)
- 在对象的内部是以8字节进⾏对⻬的。
苹果会⾃动重排成员变量的顺序,将占⽤不⾜ 8 字节的成员挨在⼀起,凑满 8 字节,以达到优化内存的⽬的。 - 自己写的成员变量的顺序就按照书写顺序来的,苹果会重拍属性的顺序,但是在重排的时候不会考虑父类,父类和子类各自重排属性。
4.结构体(struct)中所有变量是“共存”的,⽽联合体(union)中是各变量是“互斥”的,只能存在⼀个。 - 位域的宽度不能超过前⾯数据类型的最⼤⻓度。
- nonPointerIsa是内存优化的⼀种⼿段,通过位域和联合体兼容了旧版本的isa和存储其他的信息在isa的内存空间中。
- new = alloc + init