美文网首页
iOS进阶专项分析(二)、OC对象大小及内存分配原理

iOS进阶专项分析(二)、OC对象大小及内存分配原理

作者: 溪浣双鲤 | 来源:发表于2020-06-18 22:15 被阅读0次

    本篇会多角度多种方式分析内存地址部分内容,需掌握一些计算机基础知识:

    1、大小端模式 传送门

    2、常用数据类型所占存储空间

    常用数据类型所占存储空间表.png

    3、与OC内存对齐算法相同的移位运算 传送门

    下面开始本篇的分析:

    一、OC对象大小


    新建工程,新建一个类 BMPerson ,添加三个属性,代码如下

    @interface BMPerson : NSObject
    @property (nonatomic, assign)int age;//年龄
    @property (nonatomic, copy)NSString * name;//姓名
    @property (nonatomic, assign)int height;//身高
    @end
    
    

    viewDidLoad新建一个该对象的实例,并给这三个属性赋值,然后引入 #import <objc/runtime.h> ,使用runtimeAPI提供的方法 class_getInstanceSize() 来打印大小

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        BMPerson * person = [[BMPerson alloc] init];
        person.name = @"张三";
        person.age = 18;
        person.height = 180;
        
        NSLog(@" class_getInstanceSize 大小%zd", class_getInstanceSize([person class]));
        NSLog(@" malloc_size 大小%zu", malloc_size((__bridge const void *)(person)));
    }
    
    

    打印结果如下:

    2020-06-18 23:29:36.955972+0800 TextProject[18720:1508857]  class_getInstanceSize 大小24
    2020-06-18 23:29:36.956165+0800 TextProject[18720:1508857]  malloc_size 大小32
    
    

    这两种大小是如何计算的呢?接下来我们进入这两个方法的底层实现进行分析,打开objc源码:

    1. 搜索alloc {进入方法实现
    2. 进入方法_objc_rootAlloc
    3. 进入方法callAlloc
    4. callAlloc中找到class_createInstance,点击进入,记住此时的两个入参,一个是cls,一个是0
    5. 继续进入到_class_createInstanceFromZone方法中,发现这个函数会先后调用cls->instanceSize,calloc这两个方法,先找到 cls->instanceSize;
    6. 进入这个instanceSize找到实现部分代码,贴出来如下:
    size_t instanceSize(size_t extraBytes) {
        size_t size = alignedInstanceSize() + extraBytes;
            // CF requires all objects be at least 16 bytes.
            if (size < 16) size = 16;
            return size;
    }
    
    

    由于我们之前 extraBytes 传的是0,所以相当于 `` 内部仅仅是调用了这个函数alignedInstanceSize 以及 做了一个size最小值16的限制。

    其实runtime提供的这个API方法class_getInstanceSize底层实现也是一样的

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

    都是调用了alignedInstanceSize这个方法,我们看一下它的实现部分

    uint32_t alignedInstanceSize() {
       return word_align(unalignedInstanceSize());
    }
    
    

    我们发现 alignedInstanceSize()直接返回了一个函数 word_align() , 入参是一个函数unalignedInstanceSize(), 这里先看一下 unalignedInstanceSize() 这个函数,这个函数直接返回了 data()->ro->instanceSizeunalignedInstanceSize这个函数实际上就是去类里面的取值。

    我们知道类在未添加任何属性时,例如BMPerson类经过编译后如下:(可以用clang命令进行编译得到)

    struct BMPerson_IMPL {
          struct NSObject_IMPL NSObject_IVARS;
    }
    

    那么BMPerson对象大小其实是下面这种情况

    struct NSObject_IMPL NSObject_IVARS;//isa  8个字节
    int _age; //int 4个字节
    NSString * _name;//NSString  8字节
    int _height;//int  4个字节
    
    

    所以 unalignedInstanceSize()返回的是8+4+8+4=24个字节。

    不过这里还有一个关键操作word_align()字节对齐,点击进入这个函数的定义部分

    #ifdef __LP64__
    #   define WORD_SHIFT 3UL
    #   define WORD_MASK 7UL
    #   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) {
        return (x + WORD_MASK) & ~WORD_MASK;
    }
    
    

    分析一下上面这串代码,等同于下面

    static inline uint32_t word_align(uint32_t x) {
        return (x + 7) & ~7;
    }
    
    

    下面我们开始分析这个函数的return部分

    当输入x = 4
    
    x+7 = 11,11用二进制表示
    0000 1011
    
    右边的7,用二进制表示
    0000 0111
    ~是非运算符,~7就是对7的二进制取反,得到
    1111 1000
    
    然后把上面两个数字进行 &(与)操作
    
    0000 1011
    1111 1000
    相同为1,不同为0,得到
    0000 1000
    
    结果为8
    
    即输入4,经过以上结果得到的是8,参考一下文章开头第3条传送门的 移位运算 向上取整 是不是感觉很相似?
    
    

    再举个例子:

    当输入X = 15
    
    X+7 = 22,22用二进制表示
    0001 0110
    
    右边的7,用二进制表示
    0000 0111
    ~是非运算符,~7就是对7的二进制取反
    1111 1000
    
    然后把上面两个数字进行 &(与)操作
    
    0001 0110
    1111 1000
    相同为1,不同为0,得到
    0001 0000
    
    结果为16
    
    即输入x = 15,得到的结果为16
    
    

    我们根据BMPerson的成员变量算出结果是24,24个字节以8的倍数进行对齐,还是24,效果不明显,但是如果再给BMPerson加一个int类型的属性,unalignedInstanceSize()计算方式还是8+4+8+4+4 = 28,经过word_align()字节对齐就是32了,所以class_getInstanceSize返回的就会是32,这个我已经验证过了,放个截图:

    字节对齐验证.png

    我们之前分析alloc流程会进入到_class_createInstanceFromZone方法中,发现这个函数会先后调用cls->instanceSize,calloc这两个方法,前面这个函数和class_getInstanceSize实现是基本相同的,我们已经分析完成了,接下来我们要分析的后面这个函数callocmalloc_size关键实现部分也是相通的,因为alloc的流程本身就是先计算这个类所需要的内存空间,然后系统再根据这个内存空间值进行calloc操作开辟实际的内存空间,从最开始打印的结果来看,class_getInstanceSize得出的结果是24,而系统实际开辟的内存空间大小是32,为什么会不同呢?接下来我们继续深入calloc,看看它的底层是如何实现的:

    我们点击这个calloc函数底层,发现点不进去了,其实这个函数的底层在不在objc中,而在libmalloc库中 ,还需要引入头文件#import <malloc/malloc.h>

    1. 找到calloc实现函数nano_calloc,找到其中_nano_malloc_check_clear方法,然后在其中找到segregated_size_to_fit

    下面贴上_nano_malloc_check_clear这个方法的实现

    static void *
    _nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
    {
        MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);
    
        void *ptr;
        size_t slot_key;
        size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
        mag_index_t mag_index = nano_mag_index(nanozone);
    
        nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
    
        ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
        
        ...此处省略
        
        return ptr;
    }
    
    

    调用流程就不分析了,重点来了,在_nano_malloc_check_clear这个函数中我们发现 segregated_size_to_fit,点击进入

    static MALLOC_INLINE size_t
    segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
    {
        size_t k, slot_bytes;
    
        if (0 == size) {
            size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
        }
        k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
        slot_bytes = k << SHIFT_NANO_QUANTUM;                           // multiply by power of two quanta size
        *pKey = k - 1;                                                  // Zero-based!
    
        return slot_bytes;
    }
    
    

    这里就是我们要分析的地方:

    k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; 
        
    slot_bytes = k << SHIFT_NANO_QUANTUM;
    
    

    点击这几个宏,我们发现

    #define NANO_MAX_SIZE           256 /* Buckets sized {16, 32, 48, ..., 256} */
    #define SHIFT_NANO_QUANTUM      4
    #define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM)   // 16
    
    

    那么上面这个代码就可以变成

    k = (size + 16 - 1) >>4<<4
    
    

    和文章开头第3个一样的位移算法,内存16位对齐,这里的对齐数字是 NANO_MAX_SIZE这个宏,是16的倍数

    #define NANO_MAX_SIZE           256 /* Buckets sized {16, 32, 48, ..., 256} */
    
    

    到这里我们就明白了,为什么之前得到的size=24 经过 calloc(1,24)操作后,打印的结果会是32,就是再把 24 进行16倍数对齐,所以是32

    接下来我们换个角度,通过汇编分析的方式,换个角度来分析BMPerson实例在内存中的地址

    二、从汇编角度分析对象大小及内存分配


    把断点断到NSLog处,运行

    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        BMPerson * person = [[BMPerson alloc] init];
        person.name = @"张三";
        person.age = 18;
        person.height = 180;
        
        NSLog(@" class_getInstanceSize 大小%zd", class_getInstanceSize([person class]));
        NSLog(@" malloc_size 大小%zu", malloc_size((__bridge const void *)(person)));
    }
    
    
    

    使用lldb调试打印person的地址

    (lldb) po person
    <BMPerson: 0x6000039f0380>
    
    (lldb) x 0x6000039f0380
    0x6000039f0380: 58 26 85 0e 01 00 00 00 12 00 00 00 b4 00 00 00  X&..............
    
    0x6000039f0390: 18 00 85 0e 01 00 00 00 00 00 00 00 00 00 00 00  ................
    (lldb) 
    
    (lldb) p 0x00000012
    (int) $2 = 18
    
    (lldb) p 0x000000b4
    (int) $3 = 180
    
    (lldb) p (NSString *)0x000000010e850018
    (__NSCFConstantString *) $5 = 0x000000010e850018 @"张三"
    
    

    由于iOS是小端模式,所以上面这串地址的数据分布如下图:

    存储方式.png

    总结:

    1、alloc底层流程就是先 调用class_getInstanceSize()获取实例变量最小的分配空间,然后再调用calloc返回系统实际分配的内存空间大小。

    2、由runtimeAPI提供的class_getInstanceSize()方法,用来计算该对象的实例最少需要分配多少空间,其中底层实现包含内存对齐算法,使用时需要导入#import <objc/runtime.h>头文件。

    3、malloc_size()返回系统实际分配的内存空间大小,其中底层实现包含内存对齐算法,需要导入 #import <malloc/malloc.h>头文件。

    再来补充几个基本概念:

    数据成员对⻬规则: 结构(struct)(或联合体(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储。

    结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b ,b 里有char int double的元素,那b应该从8的整数倍开始存储)

    结构体的总大小(同sizeof获取)**必须是其内部最大成员的整数倍,不足的要对齐
    **

    溪浣双鲤的技术摸爬滚打之路

    相关文章

      网友评论

          本文标题:iOS进阶专项分析(二)、OC对象大小及内存分配原理

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