iOS 对象探究二

133 阅读8分钟

在上一篇文章中iOS 对象初始化过程探究一中我们已经探究了OC 对象的初始化过程,但是一个对象的大小又跟哪些因素有关呢?或者说一个对象需要开辟的内存空间由哪些因素决定?这里我们就来探究一下。

影响对象内存大小的因素

还是一样我们先通过一个例子来看一下。首先声明一个空对象,我们分别打印对象类型的内存大小、实际占用内存大小及系统开辟内存大小。

案例 1

案例 1.png

通过案例 1 我们可以看的对象类型的内存大小是 8 字节(p 是指针类型),而实际占用内存大小跟系统开辟内存大小为 0。

案例 2

案例 2.png

现在我们再打印可以看到对象类型的内存大小是 8 字节,实际占用内存大小为 8 字节,真实开辟内存大小为 16 字节。那么是为什么alloc, init 之后实际内存跟开辟内存分别变为了 8 字节跟 16 字节呢?这里实际占用内存为 8 字节的原因是NSobject对象有一个 isa属性,而isa是一个指针类型。

案例 3

@interface LGPerson : NSObject

- (void)test1;

+ (void)test2;

@end

@implementation LGPerson

- (void)test1 {
    NSLog(@"test1");
}

+ (void)test2 {
    NSLog(@"test2");
}

@end

案例 3.png

这里我们给LGPerson添加了一个类方法跟一个实例方法,打印之后可以看到各项内存大小并没有变化,这里至少可以证明 1 点就是不管是对象方法还是实例方法对对象的内存大小都没有影响。

案例 4

案例 4.1 案例 4.2 案例 4.3

通过3 个案例我们可以看到:

  • 案例 4.1 的时候增加了一个 name属性,因为字符串是 8 字节,打印的对象实际内存是 16 字节,实际开辟的内存大小也是 16 字节。
  • 案例 4.2 的时候增加了一个 age 属性,是 4 字节,但是真实打印确是对象实际内存是 24 字节,而不是 20,实际开辟的内存大小是 32 字节(16 的整数倍)。
  • 案例 4.3 的时候增加了一个 bookNum 属性,也是 4 字节,打印内存的占用并没有增加,对象实际内存还是 24 字节,实际开辟的内存大小也还是 32 字节。

这里我们可以发现一个规律,案例 4.2 的时候因为增加了一个 4 字节的 age 属性,但是实际内存是 24 字节而不是 20 字节,24 为 8 的整数倍。实际开辟的内存大小是 32 字节为 16 的整数倍。案例 3 的时候内存没变的原因是因为 20 加 4 正还是 24,为 8 的 3 倍。且还是小于 36,实际开辟的内存大小也没有增加。

那么这里可以发现一个规律,实现内存大小都是 8 字节的整数倍,实际开辟的内存大小都是 16 字节的整数倍。这里其实是系统内部做了内存对齐。下面我们来看一下结构体的对齐原则。

结构体内存对齐原则

  1. 数据成员对齐规则:结构体(struct)或联合体(union)的数据成员,第一个数据成员放在 offset 为 0 的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如 int 为 4 字节,则要从 4 的整数倍地址开始存储)。
  2. 结构体作为成员:如果一个结构体里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里存有 struct b,b 里有 char,int ,double 等元素,那么 b 应该从 8 的整数倍开始存储)。
  3. 结构体的总大小,也就是 sizeof 的结果,必须是其内部最大成员的整数倍,不足的要补齐。

Objective-C不同数据类型占用字节大小.png

这里我们通过几个案例来验证一下这上面的对齐原则是否正确。

案例1

struct LGStruct1 {
    double a;
    char b;
    int c;
    short d;
}struct1;

// 根据结构体(struct)的数据成员,第一个数据成员放在 offset 为 0 的地方,因为 a 为double 类型,这里的案例都是在 64 位系统下,所以 a 为 8 位,存储在 0 到 7 的位置上
// a: [0...7]
// 根据以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小的整数倍开始,因为 char 为 1 位,所以 b 存储在第 8 位
// b: [8]
// 还是根据后面每个成员存储的起始位置要从该成员大小成员大小的整数倍开始,因为 int 为 4 字节,所以要从 12 开始,9、10、11 空出,c 存储在 12 到 15 的位置上
// c: 9,10,11 [12,13,14,15]
// 因为 short 为 2 个字节,且 16 为 2 的整数倍,所以 d 存储在 16 到 17 的位置上
// d: [16,17]
// 最后根据结构体的总大小,也就是 sizeof 的结果,必须是其内部最大成员的整数倍,不足的要补齐,因为最大成员 a 为 8 字节,所以结构体 struct1 的 size大 小为 24

通过结构体内存对齐原则我们推导出 struct1 的大小是 24,通过打印也是 24 个字节。

案例2

struct LGStruct2 {
    double a;
    int b;
    char c;
    short d;
}struct2;

// 根据结构体(struct)的数据成员,第一个数据成员放在 offset 为 0 的地方,因为 a 为double 类型,这里的案例都是在 64 位系统下,所以 a 为 8 位,存储在 0 到 7 的位置上
// a: [0...7]
// 根据以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小的整数倍开始,因为 int 为 4 位,所以 b 的存储从第 8 位开始,存储在 8 到 11 位
// b: [8,9,10,11]
// 还是根据后面每个成员存储的起始位置要从该成员大小成员大小的整数倍开始,因为 char 为 1 字节,12 为 1 的整数倍,所以存储在 12 的位置上
// c: [12]
// 因为 short 为 2 个字节,且 14 为 2 的整数倍,所以 d 存储在  14 到 15 的位置上
// d: [14,15]
// 最后根据结构体的总大小,也就是 sizeof 的结果,必须是其内部最大成员的整数倍,不足的要补齐,因为最大成员 a 为 8 字节,所以结构体 struct2 的 size大 小为 16

通过打印 struct2 的大小也是 16 字节,这里可以看出一个问题,struct1 跟 struct2的成员变量都一样,只是位置不同,就造成了他们的最终大小不同,所以成员变量的位置顺序也会影响结构体的大小。但是对象不会,因为编译器会对对象的成员变量进行重排。

案例3

struct LGStruct3 {
    double a;
    int b;
    char c;
    short d;
    int e;
    struct LGStruct1 str;
}struct3;

// 根据结构体(struct)的数据成员,第一个数据成员放在 offset 为 0 的地方,因为 a 为double 类型,这里的案例都是在 64 位系统下,所以 a 为 8 位,存储在 0 到 7 的位置上
// a: [0...7]
// 根据以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小的整数倍开始,因为 int 为 4 位,所以 b 的存储从第 8 位开始,存储在 8 到 11 位
// b: [8,9,10,11]
// 还是根据后面每个成员存储的起始位置要从该成员大小成员大小的整数倍开始,因为 char 为 1 字节,12 为 1 的整数倍,所以存储在 12 的位置上
// c: [12]
// 因为 short 为 2 个字节,且 14 为 2 的整数倍,所以 d 存储在  14 到 15 的位置上
// d: [14,15]
// 根据以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小的整数倍开始,因为 e 为 int 型,为 4 字节,所以 e 的存储从第 16 位开始,存储在 16 到 19 位
// e: [16,17,18,19]
// 根据如果一个结构体里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。因为 LGStruct1 内部最大元素 a 为 double 类型,8 个字节,所以要从 8 的整数倍位置开始存储,也就是 24 开始存储,案例 1 里面已经讲了,LGStruct1 总共占用 17 个字节,所以 str 存储的位置是从 24 开始,加上 17,等于 41,根据结构体的总大小,必须是其内部最大成员的整数倍,不足的要补齐。因为最大成员为 8 字节,所以最终 stuct3 的大小为 48 字节。

最终打印的大小也是 48 字节。