前期准备
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:操作符。传入
数据类型,输出内存大小。编译时固定,
只与类型相关,与具体数值无关。(如:bool2字节,int4字节,对象(指针)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.png
MyStruct1 和 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 基础数据类型 字节数表:
基础数据类型字节数
MyStruct1 内存计算
MyStruct2 内存计算
结构体中的结构体
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和char4 + 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;
}
-
x/8gx person: 16进制打印8行内存信息
image.png
-
我们分析属性,
name、nickname、height都是各自占用8字节。可以直接打印出来。 -
而
age是Int占用4字节,c1和c2是char,各自占用1字节。我们推测系统可能属性重排,将他们存放在了一个块区。
image.png
特殊的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计算内存大小。
_class_createInstanceFromZone核心方法.png













网友评论