第二篇:iOS的内存分布探索
1.对象的内存分布
首先我们那看下HPWPerson这个对象,其占用了多少的内存空间,我们通过malloc_size((__bridge const void *)(p))进行打印后其显示的是48字节,那为什么是48字节呢,我们带着这个问题去探究下。
@interface HPWPerson : 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
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
appDelegateClassName = NSStringFromClass([AppDelegate class]);
HPWPerson *p = [HPWPerson new];
NSLog(@"%lu",malloc_size((__bridge const void *)(p)));
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
2022-04-20 12:53:53.236808+0800 001--对象的内存分布[53714:782522] 48
我们这个时候去分析下HPWPerson里的成员变量,通过下面的各个成员变量占用的空间为8+8+4+8+2=30,因为oc里其对象都有一个isa指针,这个指针占8字节,所以一起是38.但是苹果是8字节对其的最小为16字节所以HPWPerson创建的对象需要分配48字节的空间
@property (nonatomic ,copy) NSString *name; NSString占用8字节
@property (nonatomic ,copy) NSString *hobby; NSString占用8字节
@property (nonatomic ,assign) int age; int占用4字节
@property (nonatomic ,assign) double hight; double占用8字节
@property (nonatomic ,assign) short number; short占用2字节
那我们如果在HPWPerson类里添加一个类方法+ (void)test,一个实例方法- (void)test,那分配的空间会改变吗?其实答案是不变的,这个是因为,我们的对象存储的是isa指针和成员变量的值。
@interface HPWPerson : 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;
- (void)test;
+ (void)test;
@end
HPWPerson *p = [HPWPerson new];
p.name = @"鹏伟";
p.hobby = @"girl";
p.hight = 1.80;
p.age = 18;
p.number = 123;
接着我们再分析下,苹果分配空间是按成员变量里的顺序,还是按我们给每个变量赋值时候的顺序来的,这个时候我们需要用到一些lldb调试,其中用到了p指令,po指令,p/x指令打印16进制数据,p/o指令打印8进制数据,p/t指令打印2进制数据,p/f指令打印浮点类型数据。x/6gx指令指的是以16进制形式去打印6个8字节的内存地址(g代表每个为8字节大小,x代表16进制形式 )
(lldb) x/6gx p
0x600001df4060: 0x010000010280d5c1 0x00000012007b0000
0x600001df4070: 0x00000001028080c8 0x00000001028080e8
0x600001df4080: 0x3ffccccccccccccd 0x0000000000000000
第一个0x010000010280d5c1地址存的是8字节的isa指针,打印第3个和第5个地址如下:
(lldb) po 0x00000001028080c8
鹏伟
(lldb) p/f 0x3ffccccccccccccd
(long) $3 = 1.8
通过上面我们发现,复制的时候 p.name = @"鹏伟";是放在第一位的,并且在HPWPerson的成员变量里name也是放在第一位的。但是我们通过指令打印的时候其并不是近排在isa指针后面的,而是放在第三个地址里的,这个是因为苹果其里面有自动重排属性的顺序进行优化操作,其还会把几个不满足8字节的排在一起进行优化。
分析到这里,我们再来分析个有趣的现象:
我们创建一个OSTestObject1类,它继承OSTestObject类,然后在OSTestObject类,也就是父类里改变int count的位置,发现其实际占用的内存空间不同。
@interface OSTestObject : NSObject
{
@public
int count;
NSObject *objc1;
NSObject *objc2;
// int count;
}
@end
@implementation OSTestObject
@end
@interface OSTestObject1 : OSTestObject
{
@public
int _count2;
}
@end
@implementation OSTestObject1
@end
- (void)viewDidLoad {
[super viewDidLoad];
OSTestObject1 *objc1 = [[OSTestObject1 alloc] init];
NSLog(@"objc1实际占用的内存空间为%zd",class_getInstanceSize([OSTestObject1 class]));
NSLog(@"系统为objc1开辟的内存空间为%zd",malloc_size((__bridge const void *)objc1));
}
当为这个顺序的时候,其实际内存空间为40字节
@interface OSTestObject : NSObject
{
@public
int count;
NSObject *objc1;
NSObject *objc2;
// int count;
}
2022-04-20 13:56:40.160391+0800 Test[54706:831054] objc1实际占用的内存空间为40
2022-04-20 13:56:40.160449+0800 Test[54706:831054] 系统为objc1开辟的内存空间为48
当为这个顺序的时候,其实际内存空间为32字节
@interface OSTestObject : NSObject
{
@public
// int count;
NSObject *objc1;
NSObject *objc2;
int count;
}
2022-04-20 13:58:22.191690+0800 Test[54755:832662] objc1实际占用的内存空间为32
2022-04-20 13:58:22.191746+0800 Test[54755:832662] 系统为objc1开辟的内存空间为32
通过上面两个输出打印发现父类里的成员变量顺序不同的时候,其实际占用的内存空间不同,这个是为什么呢?这个是因为苹果在重排的时候子类不影响父类的内存分配,如果在排到父类最后里有位置可以塞子类里成员变量的时候,就会把子类里往里面塞。当我们子类继承父类的时候,其父类是一块连续的内存空间,子类是无法修改父类的空间排列。
2.结构体的内存分布
这个我们来分析下结构体里暂用的内存大小,LGStruct1的大小是4字节,char是占1字节。LGStruct2占用的是1字节。
struct LGStruct1 {
char a;
char b;
char c;
char d;
}struct1;
struct LGStruct2 {
// 1的位置代表: 位域
char a : 1;//有的时候不需要8字节存储,只要1个byte位就可以
char b : 1;
char c : 1;
char d : 1;
}struct2;
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
NSLog(@"%lu %lu",sizeof(struct1),sizeof(struct2));
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
2022-04-20 14:39:10.946945+0800 [55403:857022] 4 1
如果这里设置如下:那么是占用4字节,因为不满8byte可以自行换到另外一个8byte里
struct LGStruct2 {
// 1的位置代表: 位域
char a : 7;
char b : 2;
char c : 7;
char d : 2;
}struct2;
3.结构体和联合体的内存分布区别
struct LGTeacher1 {
char *name;
int age;
double height;
}t1;
union LGTeacher2 {
char *name;
int age;
int height;
}t2;
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
t1.name = "HPW";
t1.age = 18;
t1.height = 2.2;
t2.name = "HPW";
t2.age = 18;
t2.height = 2.2;
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
打断点调试后打印如下:
t1 LGTeacher1
name char * "HPW" 0x0000000100b43e1f
age int 18
height double 2.2000000000000002
t2 LGTeacher2
name char * "" 0x0000000100000002
age int 2
height int 2
通过上面打印发现,结构体里打印是各个属性的值,但是联合体打印的都是乱七八糟的东西,这个是因为联合体分配的是一个空间,联合体里面的各个属性同时共用这个空间,只有在走到对应赋值语句时候打印才能显示对应的地址的值,接着继续运行这个空间就会释放了,就会看到一些乱值,这里char,int都是指针,所以联合体最大占用8字节,联合体总结是能容纳最大的成员变量,通过1计算出的大小必须是最大的成员变量(基础数据类型整数倍)。
1)结构体里的变量可以共存,内存开辟比较粗放
2)联合体里的变量互斥,可节省一定的内存空间
为什么我们这里要说到联合体呢,其实我们的isa指针就是个联合体,下面我们来对isa指针进行一个探索:
4.isa指针探索
之前我们说了alloc一个对象后,如何和分配的内存地址进行一个关联呢,这个时候就用到了一个isa的指针,我们惊奇的发现isa指针竟然是一个联合体,这里我们再讲一个nonpointerIsa的概念,这个是苹果用来进行优化的,因为其是8字节,有64个byte位存储,像我们引用计数就存在里面。
WechatIMG1912.jpeg
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
private:
// Accessing the class requires custom ptrauth operations, so
// force clients to go through setClass/getClass by making this
// private.
Class cls;
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
bool isDeallocating() {
return extra_rc == 0 && has_sidetable_rc == 0;
}
void setDeallocating() {
extra_rc = 0;
has_sidetable_rc = 0;
}
#endif
void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);
Class getDecodedClass(bool authenticated);
};
我们看到在isa指针里union这个联合体里有struct结构体,我们看到有ISA_BITFIELD这个,我们搜索下也就是对应isa指针的结构,在这里模拟器或者真机调试时候,区分是x86结构,还是arm64结构等,不同的结构其数据架构是不同的,nonpointer是对isa指针的优化。其中我们看到 uintptr_t shiftcls_and_sig : 52; 这个里面其实存的是类对象也就是HPWPerson
# 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 //3
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)