OC底层原理04-OC对象内存优化

2020-09-16  本文已影响0人  AndyGF

本文将结合我的另外一篇文章 Object-C底层原理03-结构体内存对齐 来讲讲 OC对象的内存对齐和内存分配规则.

在开始之前, 我们先来了解一下获取内存大小的三种方式 :

  • sizeof
  • class_getInstanceSize
  • malloc_size

sizeof

  1. sizeof 是一个操作符,不是函数.
  2. 用于获取数据类型占用内存的大小.
  3. 在编译阶段就能确定所传入的数据类型占用内存大小.

基本数据类型如 int , char , double...... , 我想不用多说了,
比较特殊的是 指针, 占用 8 个字节.

sizeof 获取内存

class_getInstanceSize

class_getInstanceSizeruntime 提供的 api,用于获取 类的实例对象 所需要的内存大小.
类的本质就是结构体, 实际就是这个结构体的大小, 我们知道结构体的大小和成员的顺序有直接关系, 为了节约内存, OC 底层对这个结构体做了重排, 以达到所需内存最小.

malloc_size

malloc_size 函数是获取系统实际分配的内存大小.

malloc_size 获取实际分配到的内存大小

注意 :

  1. 对象的所需的最小内存是 8 字节.
  2. 对象被分配到的最小内存是 16 字节.

接下来我们举例说明 OC 对象的内存对齐和分配.
创建一个 GFPerson 类, 并打印对象大小

@interface GFPerson : NSObject

// isa                                     // 8  0 ~ 7
@property(nonatomic, copy) NSString *name; // 8  8 ~ 15
@property(nonatomic, copy) NSString *nick; // 8  16 ~ 23

@property(nonatomic) char c1;              // 1  24
@property(nonatomic, assign) int age;      // 4  28 ~ 31
@property(nonatomic) char c2;              // 1  32
@property(nonatomic, assign) long *height; // 8  33 ~ 40 共41字节, 最大 8,  所需 48


@end

@implementation GFPerson

@end

int main(int argc, char * argv[]) {
    
    @autoreleasepool {
        
        GFPerson *obj = [[GFPerson alloc] init];
        
        NSLog(@"obj 类型所需的内存: %lu", class_getInstanceSize([obj class]));
        NSLog(@"obj 实际分配的内存: %lu", malloc_size((__bridge const void*)(obj)));

    }
    return 0;
}

按照结构体的内存计算规则, 指针类型占 8 字节, int 占 4 字节, char 占 1 字节, 最终得到 这个类的对象至少需要 48 字节内存空间.

注意: isa 是继承自 NSObject 的, 就是我们上面据说的最小内存为 8

打印结果如下图 :

对象实际打印结果

这与我们计算的结果对不上, 不但没有超过, 41, 还比41小, 这显然是底层动了手脚. 我们知道结构体成员的顺序直接影响了结构体的大小,

猜测: 底层对成员的顺序进行了重排, 这个例子比较简单, 我们自己也可以重排一下, 再计算, 来验证

@interface GFPerson : NSObject

// isa                                     // 8  0 ~ 7
@property(nonatomic, copy) NSString *name; // 8  8 ~ 15
@property(nonatomic, copy) NSString *nick; // 8  16 ~ 23

@property(nonatomic, assign) int age;      // 4  24 ~ 27
@property(nonatomic) char c1;              // 1  28
@property(nonatomic) char c2;              // 1  29
@property(nonatomic, assign) long *height; // 8  32 ~ 39 共40字节, 最大 8,  所需 40
@end

因此我们有理由相信底层对类的成员顺序做了一定的调整, 以达到节约内存, 提高效率的目的.

那么问题来了, GFPerson 这个类需要 40 个字节, 为什么系统给分配了 48 个字节呢, 从 alloc 源码分析可以知道, 系统在分配内存时采用的是 16 字节对齐方式, 因此被分配了 48 个字节的内存, 而计算类本身大小时, 采用的是 8 字节对齐.

size_t instanceSize(size_t extraBytes) const {
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);
        }

        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }

以上这些我们都是从类的本质 - 结构体的角度出发去分析的. 接下来我们从 内存的实际情况来看一下到底是不是这样的.

main 函数中给 obj 的属性赋值,

GFPerson *obj = [[GFPerson alloc] init];
        obj.name = @"Andy";
        obj.nick = @"shuaige";
        obj.age = 18;
        
        obj.c1 = 'a';
        obj.c2 = 'b';

断点调试 obj,根据 GFPerson 的对象地址,查找出属性的值.

通过地址找到 namenick 的值:

name` 和 `nick` 的值

当我们想通过地址 0x0000001200006261找出 age 等数据时,发现是乱码,这里无法找出值的原因是苹果中针对 age , c1 , c2 属性的内存进行了重排,因为age类型占4个字节,c1c2 类型 char 分别占1个字节,通过4+1+1的方式,按照8字节对齐,不足补齐的方式存储在同一块内存中,

age 的读取通过0x00000012
c1 的读取通过0x61
c2 的读取通过0x62

age, c1, c2 的内存

为什么 c1, c2 输出的是 97 , 98 呢,

十六进制 0x61 转换为 十进制是 97, 就是 'a' 的 ASCII 码值.
十六进制 0x62 转换为 十进制是 98, 就是 'a' 的 ASCII 码值.

注意:
属性没有赋值的地址都是 0x0000000000000000

这里可以总结下苹果中的内存对齐思想:

大部分的内存都是通过固定的内存块进行读取,
尽管我们在内存中采用了内存对齐的方式,但并不是所有的内存都可以进行浪费的,苹果会自动对属性进行重排,以此来优化内存

字节对齐到底采用多少字节对齐?
到目前为止,我们在前文既提到了8字节对齐,也提及了16字节对齐,那我们到底采用哪种字节对齐呢?

我们可以通过objc4中 class_getInstanceSize 的源码来进行分析,

/** 
 * 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);

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}

static inline uint32_t word_align(uint32_t x) {
    //x+7 & (~7) --> 8字节对齐
    return (x + WORD_MASK) & ~WORD_MASK;
}

#   define WORD_MASK 7UL

内存优化总结 :

  1. 按照 8 字节内存对齐的方式对属性顺序进行重排, 当然也是按照 8 字节内存对齐方式计算需要内存大小, 这样可以最大限度利用内存, 减少内存浪费.
  2. 在实际分配内存, 开辟内存空间时, 按照 16 字节内存对齐方式进行分配, 这样 cpu 在读取数据时就可以一次读取 16 个字节, 比一次读取 8 个字节效率要高的多, 然而一个对象最多只是多开辟 8 个字节的内存空间. 以空间换时间.
  3. 具有一定的容错功能, 如果是刚刚好, 那么当出错时可能访问到其他对象的内容.
上一篇下一篇

猜你喜欢

热点阅读