OC 对象原理探索(二)
目录
- 1. 对象的内存影响因素
- 2. 成员变量在内存中的存储情况
- 3. 结构体内存对齐
3.1 类型占用字节数表格
3.2 内存对齐原则
1、数据成员对齐规则
2、结构体作为成员
3、结构体总大小
3.3 举例验证- 4.
sizeof
、class_getInstanceSize
、malloc_size
- 5.
malloc
源码分析
5.1 源码下载
5.2 源码分析- 6. 总结
1. 对象的内存影响因素
先创建一个SSLPerson
类:
@interface SSLPerson : NSObject {
NSString *_hobby;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
- (void)eat;
@end
打印一下它的内存大小:
把nickName
属性注释掉:
//@property (nonatomic, copy) NSString *nickName;
查看打印结果:
把eat
方法注视掉:
//- (void)eat;
查看打印结果:
把成员变量_hobby
注视掉:
// NSString *_hobby;
查看打印结果:
通过上面的打印,我们发现属性
和成员变量
影响了对象内存,方法
没有影响,所以我们可以得出最终结论:只有成员变量
会影响对象内存。
2. 成员变量在内存中的存储情况
我们修改一下SSLPerson
类:
@interface SSLPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, copy) NSString *hobby;
@property (nonatomic, assign) int age;
@property (nonatomic) double height;
@property (nonatomic) char a;
@property (nonatomic) char b;
@end
创建SSLPerson
类的实例p
,为属性赋值,用x/8gx
命令打印出这些值在内存中的存储情况:
找出ssl
、王老五
、180.5
:
找出24
、a
、b
:
注:
a
的ASCII码
为97
,b
的ASCII码
为98
。
通过打印结果我们发现24
、a
、b
被存储到了同一个8
字节内,这里就涉及到了内存对齐的一些原则,下面继续探究。
3. 结构体内存对齐
3.1 类型占用字节数表格
C | OC | 32位 | 64位 |
---|---|---|---|
bool |
BOOL(64位) |
1 | 1 |
signed char |
(__signed char)int8_t、BOOL(32)位 |
1 | 1 |
unsigned char |
Boolean |
1 | 1 |
short |
int16_t |
2 | 2 |
unsigned short |
unichar |
2 | 2 |
int int32_t |
NSInteger(32位)、boolean_t(32位) |
4 | 4 |
unsigned int |
boolean_t(64位)、NSUInteger(32位) |
4 | 4 |
long |
NSInteger(64位) |
4 | 8 |
unsigned long |
NSUInteger(64位) |
4 | 8 |
long long |
int64_t |
8 | 8 |
float |
CGFloat(32位) |
4 | 4 |
double |
CGFloat(64位) |
8 | 8 |
3.2 内存对齐原则
1、数据成员对齐规则
结构体struct
或联合体union
的数据成员,第一个数据成员放在offset
为0
的位置,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说数组,结构体等)的整数倍数
开始(比如int
为4
个字节,则要从4
的整数倍地址开始存储)。
2、结构体作为成员
如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素
大小的整数倍
地址开始存储,(struct
a
中存有struct
b
,b
中有char
,int
,double
等元素,那么b
应该从8
的整数倍开始存储),因为double
为最大子元素,占用8
个字节。
3、结构体总大小
结构体总大小,也就是sizeof
的结果,必须是其内部最大成员
的整数倍
,不足的要补齐。
3.3 举例验证
struct Struct1 {
double a; // 占8字节 存放在[0 7]
char b; // 占1字节 下一个索引8是1的整数倍,存放在[8]
int c; // 占4字节 下一个索引9不是4的整数倍,所以空出9,10,11,存放在 [12 13 14 15]
short d; // 占2字节 下一个索引16是2的整数倍,存放在[16 17]
}struct1; // 总区间为[0...17],大小为18,取最大元素double8字节的整倍数,所以总大小为24
struct Struct2 {
double a; // 占8字节 存放在[0 7]
int b; // 占4字节 下一个索引8是4的整数倍,存放在[8 9 10 11]
char c; // 占1字节 下一个索引12是1的整倍数,存放在[12]
short d; // 占2字节 下一个索引13不是2的整倍数,所以空出13 存放在[14 15]
}struct2; // 总区间为[0...15],大小为16,取最大元素double8字节的整倍数,所以总大小为16
struct Struct3 {
double a; // 占8字节 存放在[0 7]
int b; // 占4字节 下一个索引8是4的整数倍,存放在[8 9 10 11]
char c; // 占1字节 下一个索引12是1的整数倍,存放在[12]
short d; // 占2字节 下一个索引13不是2的整数倍,所以空出13,存放在[14 15]
int e; // 占4字节 下一个索引16是4的整数倍,存放在[16 17 18 19]
struct Struct1 str1; // 占24字节 下一个索引20不是str1中double8字节的整数倍,所以空出20 21 22 23,最后存放在[24.....47]
struct Struct2 str2; // 占16字节 下一个索引48是str2中double8字节整数倍,存放在[48.....64]
short f; // 占2字节 下一个索引65不是2的整数倍,所以空出65,存放在[66,67]
}struct3; // 总区间为[0...67],大小为68,取最大元素double 8字节的整倍数,所以总大小为72
NSLog(@"Struct1:%lu -- Struct2:%lu -- Struct3:%lu",
sizeof(struct1), sizeof(struct2), sizeof(struct3));
查看打印结果:
Struct1:24 -- Struct2:16 -- Struct3:72
4. sizeof
、class_getInstanceSize
、malloc_size
修改SSLPerson
类:
@interface SSLPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@end
main.m
中代码:
#import "SSLPerson.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
SSLPerson *person = [SSLPerson alloc];
person.name = @"lee";
person.nickName = @"ssl";
NSLog(@"person %@",person);
NSLog(@"sizeof %lu",sizeof(person));
NSLog(@"person %lu",class_getInstanceSize([SSLPerson class]));
NSLog(@"person %lu",malloc_size((__bridge const void *)(person)));
}
return 0;
}
查看打印结果:
解释:
-
sizeof
这里计算的是person
指针的大小,指针统一为8
字节; -
class_getInstanceSize
计算的是isa
指针加成员变量
占用的内存:name
(NSString``8
字节) +nickName
(NSString``8
字节) +age
(int``4
字节) +height
(long``8
字节) +isa
(来自NSObject``8
字节) =36
字节,按照8
字节对齐,最终为40
字节; -
malloc_size
计算的是实际向系统申请开辟的内存空间:40
字节向系统申请时,遵循16
字节对齐原则,最终为48
字节。
malloc
是如何申请内存的呢,我们接下来通过源码来进行分析。
5. malloc
源码分析
5.1 源码下载
工程中点击malloc_size
定位源码所在位置:
malloc 源码下载地址,我们以317.40.8
版本为例进行分析。
5.2 源码分析
我们在main.m
中添加代码,用40
字节去申请内存:
#import <Foundation/Foundation.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 用 40 字节去申请内存
void *p = calloc(1, 40);
NSLog(@"开辟字节数:%lu",malloc_size(p));
}
return 0;
}
查看打印结果:
确实还是开辟了48
字节的空间,下面通过断点调试查看源码。
断点进入calloc
:
断点进入:_malloc_zone_calloc
:
通过返回值找到核心代码ptr = zone->calloc(zone, num_items, size)
,点击进入calloc(zone, num_items, size)
发现找不到方法实现。
可以通过po
的方式找到应该调用的方法default_zone_calloc
:
- 为什么可以打印获取:因为有赋值,就会有存储值,就可以打印输出。
- 第二种方式:除了输出的方式,还可以通过
汇编找方法
的方式找到方法的真实调用。
断点进入default_zone_calloc
:
还是找不到方法,通过p zone->calloc
获取到方法nano_calloc
:
注:
p
比po
打印的更详细。
断点进入nano_calloc
:
根据返回值定位核心代码:_nano_malloc_check_clear
,断点进入:
根据返回值找到segregated_size_to_fit
关键函数,断点进入:
这里是16
字节对齐算法,40
经过对齐后得到48
,这就是最终会开辟48
字节的原因。
6. 总结
- 堆区中,对象的内存以
16
字节对齐; - 成员变量,以
8
字节对齐。