OC底层原理六: 内存对齐
前期准备
1.lldb打印规则
po
: 对象信息
(lldb) po person
<HTPerson: 0x101875c70>
p
: 对象信息
(lldb) p person
(HTPerson *) $1 = 0x0000000101875c70
x
: memory read
的简写,读取内存信息 (iOS是小端模式,内存读取要反着读)
例如: e5 22 00 00 01 80 1d 00
应读取为0x001d8001000022e5
(lldb) memory read person
0x1024aef20: c9 21 00 00 01 80 1d 00 00 00 00 00 00 00 00 00 .!..............
0x1024aef30: 2d 5b 4e 53 56 69 73 75 61 6c 54 61 62 50 69 63 -[NSVisualTabPic
(lldb) x person
0x1024aef20: c9 21 00 00 01 80 1d 00 00 00 00 00 00 00 00 00 .!..............
0x1024aef30: 2d 5b 4e 53 56 69 73 75 61 6c 54 61 62 50 69 63 -[NSVisualTabPic
x/4gx
: 打印4条16进制的16字符长度的内存信息
(lldb) x/4gx person
0x101875c70: 0x001d8001000022e5 0x0000000000000012
0x101875c80: 0x0000000100001010 0x0000000100001030
x/4gw
: 打印4条16进制的8字符长度的内存信息
(lldb) x/4gw person
0x1024aef20: 0x000021c9 0x001d8001 0x00000000 0x00000000
p/t
: 二进制打印
(lldb) p/t person
(HTPerson *) $2 = 0b0000000000000000000000000000000100000010010010101110111100100000
2.获取内存大小
-
sizeof
:操作符。传入
数据类型
,输出内存大小。编译时固定
,
只与类型相关,与具体数值无关。(如:bool
2字节,int
4字节,对象
(指针)8字节) -
class_getInstanceSize
:runtime
的api,传入对象
,输出对象所占的内存大小
,本质是对象中成员变量的大小
。 -
malloc_size
:获取
系统实际分配
的内存大小,符合前面章节align16
对齐标准
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSObject * objc = [[NSObject alloc] init];
NSLog(@"[sizeof] 内存大小: %lu字节", sizeof(objc));
NSLog(@"[class_getInstanceSize] 内存大小: %lu字节", class_getInstanceSize([objc class]));
NSLog(@"[malloc_size] 内存大小: %lu字节", malloc_size((__bridge const void *)(objc)));
}
return 0;
}
image.png
- 今天我们就来了解,
对象内部
的内存对齐
。
内存对齐
我们知道对象
对外,苹果系统会采用align16
字节对齐开辟内存大小,提高系统存取性能。
那对象内部
呢?
-
对象的本质是
结构体
,这个在后续篇章中我们会详细了解。所以研究对象内部的内存
,就是研究结构体
的内存布局
。 -
内存对齐目的:
最大程度提高资源利用率
。
我们从一个小案例开始入手
struct MyStruct1 {
char a; // 1字节
double b; // 8字节
int c; // 4字节
short d; // 2字节
NSString * e; // 8字节(指针)
} MyStruct1;
struct MyStruct2 {
NSString * a; // 8字节(指针)
double b; // 8字节
int c; // 4字节
short d; // 2字节
char e; // 1字节
} MyStruct2;
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%lu - %lu", sizeof(MyStruct1), sizeof(MyStruct2));
}
return 0;
}
打印结果:
image.pngMyStruct1 和 MyStruct2 的构成元素都一样,为何打印出的内存大小不一致?
- 结构体内部的
元素排序
影响内存大小
。这就是内存字节对齐
的作用。
结构体内存对齐规则
每个特定平台上的编译器都有自己的默认“对齐系数”
(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n)
,n=1,2,4,8,16来改变这一系数,其中的n
就是你要指定的“对齐系数”。在ios中,Xcode默认
为#pragma pack(8),即8字节对齐
注意: 这里的
8字节
对齐是结构体内部对齐规则
,对象在系统中对外
实际分配的空间是遵循16字节对齐
原则。
【三条结构体对齐规则】:
(先把规则写出来,我们下面用实例来理解)
-
数据成员的对齐规则可以理解为
min(m, n)
的公式, 其中m
表示当前成员的开始位置
,n
表示当前成员所需位数
。如果满足条件m 整除 n
(即 m % n == 0), n 从m 位置
开始存储
,反之
继续检查 m+1
能否整除 n, 直到可以整除, 从而就确定
了当前成员的开始位置
。 -
数据成员为结构体:当结构体
嵌套
了结构体
时,作为数据成员的结构体的自身长度
作为外部结构体的最大成员的内存大小,比如结构体a嵌套结构体b,b中有char、int、double等,则b的自身长度为8 -
最后
结构体的内存大小
必须是结构体中最大成员
内存大小的整数倍
,不足的需要补齐。
iOS 基础数据类型 字节数表:
结构体中的结构体
struct MyStruct3 {
NSString * a; // 8字节(指针)
double b; // 8字节
int c; // 4字节
short d; // 2字节
char e; // 1字节
struct MyStruct2 str;
} MyStruct3;
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"MyStruct3内存大小: %lu", sizeof(MyStruct3));
NSLog(@"MyStruct3中的结构体(MyStruct2)内存大小 %lu", sizeof(MyStruct2));
}
return 0;
}
image.png
MyStruct3 内存计算
内存优化(属性重排)
-
我们观察到
MyStruct1
和MyStruct2
的成员属性一样,但是在内存管理上,MyStruct2
比MyStruct1
利用率更高(白色空白区域更少
)。 -
MyStruct2
中int
、short
和char
4 + 2 + 1组合,空间利用得更合理。 -
苹果
会进行属性重排
,对属性进行合理排序,尽可能保持保持
属性之间的内存连续
,减少padding
(白色部分,属性之间置空的内存)。
如果你还记得
align16
对齐方式,你应该能理解属性重排的好处了
- align16, 是空间换取时间,保障系统在
处理对象
时能快速存取
- 属性重排,保障一个对象尽可能少的占用内存资源。
属性重排案例
- 创建
HTPerson
类
@interface HTPerson : NSObject
@property(nonatomic, copy) NSString * name;
@property(nonatomic, copy) NSString * nickname;
@property(nonatomic, assign) int age;
@property(nonatomic, assign) long height;
@property(nonatomic, assign) char c1;
@property(nonatomic, assign) char c2;
@end
-
main.m
加入测试代码
#import "HTPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson * person = [[HTPerson alloc]init];
person.age = 18;
person.height = 190;
person.name = @"mark";
person.nickname = @"哈哈";
person.c1 = 'A';
person.c2 = 'B';
NSLog(@"%@", person);
}
return 0;
}
-
image.pngx/8gx person
: 16进制打印8行内存信息
-
我们分析属性,
name
、nickname
、height
都是各自占用8字节。可以直接打印出来。 -
而
age
是Int占用4字节,c1
和c2
是char,各自占用1字节。我们推测系统可能属性重排
,将他们存放在了一个块区。
特殊的double
和float
我们尝试把height
属性类型修改为double
@property(nonatomic, assign) double height;
image.png
我们发现直接
po
打印0x4067c00000000000
,打印不出来height
的数值190。 这是因为编译器po
打印默认当做int
类型处理。
-
p/x (double)190
:我们以16进制
打印double
类型值打印,发现完全相同。
如果
height
熟悉换成float
,也是一样的使用p/x (float)190
验证。
我们可以封装2个验证函数:
// float转换为16进制
void ht_float2HEX(float f){
union uuf { float f; char s[4];} uf;
uf.f = f;
printf("0x");
for (int i = 3; i>=0; i--) {
printf("%02x", 0xff & uf.s[i]);
}
printf("\n");
}
// float转换为16进制
void ht_double2HEX(float f){
union uuf { float f; char s[8];} uf;
uf.f = f;
printf("0x");
for (int i = 7; i>=0; i--) {
printf("%02x", 0xff & uf.s[i]);
}
printf("\n");
}
image.png
为什么对象内部字节对齐是8字节
我们在objc4源码中搜索class_getInstanceSize
,可以在runtime.h
找到:
/**
* Returns the size of instances of a class.
*
* @param cls A class object.
*
* @return The size in bytes of instances of the class \e cls, or \c 0 if \e cls is \c Nil.
*/
OBJC_EXPORT size_t
class_getInstanceSize(Class _Nullable cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
在objc-class.mm
可以找到:
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
进入alignedInstanceSize
:
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
进入word_align
:
#ifdef __LP64__ // 64位操作系统
# define WORD_SHIFT 3UL
# define WORD_MASK 7UL // 7字节遮罩
# define WORD_BITS 64
#else
# define WORD_SHIFT 2UL
# define WORD_MASK 3UL
# define WORD_BITS 32
#endif
static inline uint32_t word_align(uint32_t x) {
// (x + 7) & (~7) --> 8字节对齐
return (x + WORD_MASK) & ~WORD_MASK;
}
可以看到,系统内部设定64位操作系统,统一使用8字节对齐
总结
-
外部处理,系统面对的对象太多,我们统一按照
align16
为内存块
来存取,效率很快。(所以malloc_size
读取的都是16的倍数) -
但为了
避免浪费
太多内存空间。系统会在每个对象内部
进行属性重排
,并使用8字节对齐
,使单个对象占用的资源尽可能小。(所以class_getInstanceSize
读取的都是8
的倍数) -
外部使用16字节对齐,给类留足够间距,避免越界访问,对象内部使用8字节对齐完全足够。
至此, OC底层原理三:探索alloc (你好,alloc大佬 )中提到的三大核心方法,我们已掌握了initstanceSize
计算内存大小。