iOS 底层原理iOS 底层分析

iOS底层探索之对象原理(二)

2019-12-23  本文已影响0人  litongde

前言

iOS底层探索之对象原理(一)中了解到通过calloc我们对象有了内存地址,通过initInstanceIsa和我们对象有了关联,本文将继续探索如我们对象中不同属性,将如何影响开辟的内存大小,及对象结构里面的 isa 是怎么关联到我们的对象的内存地址。

内存对齐原理

内存对齐的原则

结构体内存对齐

struct StructOne {
    char a;         // 1字节
    double b;       // 8字节
    int c;          // 4字节
    short d;        // 2字节
} MyStruct1;

struct StructTwo {
    double b;       // 8字节
    int c;          // 4字节
    char a;         // 1字节
    short d;        // 2字节
} MyStruct2;

struct StructOThree {
    double b;       // 8字节      0 - 7
    char a;         // 1字节      min(8, 1) 8
    int c;          // 4字节      min(9, 4) 9不是4整数倍,则9,10,11不能用,12,13,14,15就是当前位置
    short d;        // 2字节      min(16, 2) 16是2的整数倍,则排16,17 —— 对齐为24
} MyStruct3;

NSLog(@"%lu---%lu---%lu",sizeof(MyStruct1),sizeof(MyStruct2),sizeof(MyStruct3));

打印结果: 24---16---24

从内存对齐原则来看,上面三个结构体在内存中应该是这样的: 结构体内存对齐.png

类属性内存对齐

对象申请内存VS系统开辟内存

Person *p = [Person alloc];
p.name = @"Kaemi";  //  NSString  8
p.age = 18;         //  int       4
p.height = 188;     //  long      8
p.hobby = @"game";  //  NSString  8

NSLog(@"申请内存大小为:%lu——-系统开辟内存大小为:%lu",class_getInstanceSize([p class]),malloc_size((__bridge const void *)(p)));

打印结果: 对象申请内存大小为:40---系统开辟内存大小为:48

40 个字节不难理解,是因为当前对象有 4 个属性,有三个属性为 8 个字节,有一个属性为 4个字节,再加上 isa 的 8 个字节,就是 32 + 4 = 36 个字节,然后根据内存对齐原则,36 不能被 8 整除,36 往后移动刚好到了 40 就是 8 的倍数,所以内存大小为 40。

48 个字节的话需要我们探索 calloc 的底层原理

这里还有一个注意点,就是class_getInstanceSizemalloc_size对同一个对象返回的结果不一样的,原因是malloc_size是直接返回的calloc之后的指针的大小,如

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;
}

class_getInstanceSize内部实现是:

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

也就是说class_getInstanceSize会输出 8 个字节,malloc_size会输出 16 个字节,当然前提是该对象没有任何属性。

calloc原理探索

calloc函数出发,在 libobjc 源码中无法进入具体实现,通过Xcode观察,知道calloc需通过 libmalloc 源码进行

这里有个小技巧,其实我们研究的是 calloc 的底层原理,而 libobjc 和 libmalloc 是相互独立的,所以在 libmalloc 源码里面,我们没必要去走 calloc 前面的流程了。我们通过断点调试 libobjc 源码可以知道第二个参数是 40: (这是因为当前发送 alloc 消息的对象有 4 个属性,每个属性 8 个字节,再加上 isa 的 8 个字节,所以就是 40 个字节)

接下来我们打开 libmalloc 的源码,在新建的 target 中直接手动声明如下的代码:

void *p = calloc(1, 40);
NSLog(@"%lu", malloc_size(p));

运行之后我们一直沿着源码断点下去,会来到malloc_zone_calloc中这么一段代码
ptr = zone->calloc(zone, num_items, size);

这里我们可以直接在断点处使用 LLDB 命令打印这行代码来看具体实现是位于哪个文件中

p zone->calloc
输出: (void *(*)(_malloc_zone_t *, size_t, size_t)) $1 = 0x00000001003839c7 (.dylib`default_zone_calloc at malloc.c:249)

确定default_zone_calloc,再搜索它的实现源码

static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
    zone = runtime_default_zone();
    
    return zone->calloc(zone, num_items, size);
}

但是我们发现这里又是一次zone->calloc,我们接着再次使用 LLDB 打印内存地址:

p zone->calloc
输出: (void *(*)(_malloc_zone_t *, size_t, size_t)) $0 = 0x0000000100384faa (.dylib`nano_calloc at nano_malloc.c:884)

我们再次来到nano_calloc方法

static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
    size_t total_bytes;

    if (calloc_get_size(num_items, size, 0, &total_bytes)) {
        return NULL;
    }

    if (total_bytes <= NANO_MAX_SIZE) {
        void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
        if (p) {
            return p;
        } else {
            /* FALLTHROUGH to helper zone */
        }
    }
    malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
    return zone->calloc(zone, 1, total_bytes);
}

我们简单分析一下,应该往_nano_malloc_check_clear里面继续走,然后我们发现 _nano_malloc_check_clear里面内容非常多,这个时候我们要明确一点,我们的目的是找出 48 是怎么算出来的,经过分析之后,我们来到segregated_size_to_fit

static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
    // size = 40
    size_t k, slot_bytes;

    if (0 == size) {
        size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
    }
    // 40 + 16-1 >> 4 << 4
    // 40 - 16*3 = 48

    // 16
    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;
}

这里可以看出进行的是 16 字节对齐,那么也就是说我们传入的 size 是 40,在经过 (40 + 16 - 1) >> 4 << 4 操作后,结果为48,也就是16的整数倍。

总结
内存对齐原理.png

编译器优化

在 release 模式下,OptimizationLevel 为 Fastest,Smallest,编译器会进行优化,把在汇编中进行的一些运算操作给优化了。编译器可以从下列 4 个纬度优化:

联合体位域

我们探索 isa 的时候,会发现 isa 其实是一个联合体,而这其实是从内存管理层面来设计的,以为联合体是所有成员共享一个内存,联合体内存的大小取决于内部成员内存大小最大的那个元素,对于 isa 指针来说,就不用额外声明很多的属性,直接在内部的 ISA_BITFIELD 保存信息。

isa结构

isa是存在对象中类型是isa_t的联合体,有一个结构体属性为 ISA_BITFIELD,其大小为 8 个字节,也就是 64 位

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
      ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t deallocating      : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)
上一篇下一篇

猜你喜欢

热点阅读