美文网首页
OC底层原理04-OC对象内存优化

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

作者: AndyGF | 来源:发表于2020-09-16 18:04 被阅读0次

    本文将结合我的另外一篇文章 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. 具有一定的容错功能, 如果是刚刚好, 那么当出错时可能访问到其他对象的内容.

    相关文章

      网友评论

          本文标题:OC底层原理04-OC对象内存优化

          本文链接:https://www.haomeiwen.com/subject/jrpsektx.html