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

前言

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

内存对齐原理

内存对齐的原则

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

结构体内存对齐

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

类属性内存对齐

  • 通过测试,类属性不是按顺序进行内存分配,如果按照对象默认的属性顺序进行内存分配,在进行 属性的8字节对齐 环节时会浪费大量的内存空间,所以这里系统会帮我们把对象的属性重新排列来最大化利用我们的内存空间,这种操作被称为二进制重排

对象申请内存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的整数倍。

总结
  • 对象的属性是进行的 8 字节对齐
  • 对象自己进行的是 16 字节对齐
    • 因为内存是连续的,通过 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)
  • nonpointer:表示是否对 isa 指针开启指针优化

    • 0:纯isa指针
    • 1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等
  • has_assoc:关联对象标志位,0没有,1存在

  • has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象

  • shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤来存储类指针

  • magic:⽤于调试器判断当前对象是真的对象还是没有初始化的空间

  • weakly_referenced:标志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放

  • deallocating:标志对象是否正在释放内存

  • has_sidetable_rc:当对象引用技术大于 10 时,则需要借用该变量存储进位

  • extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容