iOS 底层探究之 alloc

我们通过几个问题来探究下一个iOS如何获取到一个对象:

  1. alloc和init的区别?
  2. alloc方法做了哪些事情?

alloc 和 init的区别

从字面意思上,我们可以知道alloc是用来分配内存,init是用来初始化数据。下面我们通过代码来验证一下:

NSObject *obj1 = [NSObject alloc];
NSObject *obj2 = [obj1 init];
NSObject *obj3 = [obj1 init];
NSObject *obj4 = [NSObject alloc];
NSLog(@"obj1: %@, %p, %p", obj1, obj1, &obj1);
NSLog(@"obj2: %@, %p, %p", obj2, obj2, &obj2);
NSLog(@"obj3: %@, %p, %p", obj3, obj3, &obj3);
NSLog(@"obj4: %@, %p, %p", obj4, obj4, &obj4);

obj1: <NSObject: 0x6000000fc580>, 0x6000000fc580, 0x7ffee64db358
obj2: <NSObject: 0x6000000fc580>, 0x6000000fc580, 0x7ffee64db350
obj3: <NSObject: 0x6000000fc580>, 0x6000000fc580, 0x7ffee64db348
obj4: <NSObject: 0x6000000fc6a0>, 0x6000000fc6a0, 0x7ffee64db340

分析NSObject对象的打印:

  1. obj1、obj2、obj3 的内存地址是一样 0x6000000fc580,和obj4 0x6000000fc6a0,说明init不会分配,调用alloc时才分配了栈地址,
  2. obj1、obj2、obj3、obj4 变量的指针地址都不一样,而且是连续,依次变小的,因为指针地址分配在栈区,栈区分配内存是连续的。
  3. 栈区和堆区的内存分配图解:
image.png

总结:

  1. alloc才会分配内存地址,init用于初始化数据。

  2. 变量指针地址分配在栈区,而且是严格根据变量声明顺序连续分配内存地址,从高到低分配。

  3. NSObject对象的内容一般存储在堆区,从低到高分配,因为堆空间分配是找到一块可用且大于需要分配内存大小的地址,有可能后分配的内存地址可能更小。

alloc方法做了哪些事情

从我对alloc的调用栈和实现逻辑,得到以下结论:

  1. 分配对象所需的内存,并做了内存对齐工作
  2. 将对象和所属类型通过isa属性绑定起来

准备工作

下载可编译的objc4源码[1],可以直接使用,不需要配置。如果断点不生效,我的解决方案是将target -> build phases -> compile sources -> 将要断点的文件移到最前面就生效了。

alloc调用链

  1. NSObject调用alloc

  2. 调用objc_alloc

  3. callAlloc(cls, true, false)

  4. NSObject 通过objc_msgSend调用 +alloc

  5. _objc_rootAlloc

  6. callAlloc(cls, false, true)

  7. _objc_rootAllocWithZone

  8. _class_createInstanceFromZone(): 内部实现内存分配和绑定类型
    (1). instanceSize(): 计算obj所需要的内存及实现内存对齐
    (2). calloc(): 分配内存,得到一个对象
    (3). initInstanceIsa(): 绑定类型

  9. alloc调用流程图:

image.png

分配内存,并实现内存对齐

  1. instanceSize()方法提供了两种计算内存的方法,第一个分支走hasFastInstanceSize(), 第二个分支走alignedInstanceSize()
inline 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;
}

2.判断是否可以快速计算实例化内存大小。__builtin_constant_p()函数表示如果为常数返回1,如果是变量是返回0。而且在_class_createInstanceFromZone(cls, 0, nil, OBJECT_CONSTRUCT_CALL_BADALLOC)调用时extra传入的就是0,所以if分支为真,应该调用 _flags & FAST_CACHE_ALLOC_MASK16。但是在实际运行中,发现走的是_flags & FAST_CACHE_ALLOC_MASK。我通过 po __builtin_constant_p(extra) == 0发现是true,因为无法看到__builtin_constant_p的实现,这里也就不深究了。最后结果返回的YES,所以下一步调用 fastInstanceSize().

bool hasFastInstanceSize(size_t extra) const
{
    if (__builtin_constant_p(extra) && extra == 0) {
        return _flags & FAST_CACHE_ALLOC_MASK16;
    }
    return _flags & FAST_CACHE_ALLOC_MASK;
}

3.调用fastInstanceSize函数,这里才是实现内存对齐的地方。因为 po __builtin_constant_p(extra) == 0 所以走else分支,调用align16()实现内存对齐。

size_t fastInstanceSize(size_t extra) const
{
    ASSERT(hasFastInstanceSize(extra));
    if (__builtin_constant_p(extra) && extra == 0) {
        return _flags & FAST_CACHE_ALLOC_MASK16;
    } else {
        size_t size = _flags & FAST_CACHE_ALLOC_MASK;
        // remove the FAST_CACHE_ALLOC_DELTA16 that was added
        // by setFastInstanceSize
        return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
    }
}

4.align16()中对对象所需的做(x + size_t(15)) & ~size_t(15),目的很简单,即对16取余,当有余数是,取出这部分加上16. 比如: size_t(15)是01111,取反后是10000, 如果超过16的话,前面补1。33 二进制是100001, &10000得到100000即32。

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

5.以上在objc4实际运行的调用链,总结可得: iOS通过alloc分配内存,且做了内存对齐,对齐的字节数是16.实际上我们得对象的结尾数字不是0就是8,就是这个原因。

6.instanceSize()方法的else分支走alignedInstanceSize()方法,最终调用word_align(),同4中分析可知对齐字节是8。

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

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

#   define WORD_MASK 7UL // 64位下

总结: alloc最终通过_class_createInstanceFromZone()方法调用instanceSize()计算对象所需的内存,在64位下进行16对齐,然后通过calloc()分配内存。

绑定类型

  1. alloc最终_class_createInstanceFromZone()方法initInstanceIsa()实现类型绑定。
inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}
  1. 然后调用objc_object::initIsa()方法,在64位机器下,isa都进行了优化(nonpointer == 1),所以走else分支, 通过setClass()将obj和Class绑定起来
inline void objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
    ASSERT(!isTaggedPointer());
    isa_t newisa(0);

    if (!nonpointer) {
        newisa.setClass(cls, this);
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());

        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.setClass(cls, this);
        newisa.extra_rc = 1;
    }

    // This write must be performed in a single store in some cases
    // (for example when realizing a class because other threads
    // may simultaneously try to use the class).
    // fixme use atomics here to guarantee single-store and to
    // guarantee memory order w.r.t. the class index table
    // ...but not too atomic because we don't want to hurt instantiation
    isa = newisa;
}

总结

综上的现象,我们可知alloc()方法实现了对象的内存分配,内存对齐,将对象和类型绑定三个功能。

内存对齐实际案例

  1. Apple在64位下,对象内存对齐是16,结构体是8。

  2. 内存分配时,会根据属性或成员变量的类型length, 属性或成员的起始内存必须是该类型length的整数倍。

验证64位下内存对齐是16

  1. 在内存分配时,最终调用objc-runtime-new.h _class_createInstanceFromZone()方法中
  2. 调用顺序是:_class_createInstanceFromZone() -> instanceSize() -> cache.fastInstanceSize() -> align16()
  3. 最终调用的是align16()方法, 对分配的内存x做内存对其, 对其规则(x + size_t(15)) & ~size_t(15)

a. ~size_t(15): size_t(15)是01111,取反后是10000, 如果超过16的话,前面补1

b. (x + size_t(15)) 这是为了实现分配的内存不小于实际需要的,向上加一个16(计算机从0开始)

c. (x + size_t(15)) & ~size_t(15) 在2的部分上去除余数,

d. 比如13 + 15 = 28, 最后得到16, 28 二进制是11100, &10000 得到10000即16

e. 18 + 15 = 33 最后得到32, 33 二进制是100001, &10000得到100000即32

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

size_t fastInstanceSize(size_t extra) const
{
    ASSERT(hasFastInstanceSize(extra));
    if (__builtin_constant_p(extra) && extra == 0) {
        return _flags & FAST_CACHE_ALLOC_MASK16;
    } else {
        size_t size = _flags & FAST_CACHE_ALLOC_MASK;
        // remove the FAST_CACHE_ALLOC_DELTA16 that was added
        // by setFastInstanceSize
        return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
    }
}

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

对象内存分析

@interface LKXObjectDemo1 : NSObject {
    // isa // 8
    int age; // 4
    double hegiht; // 8
    char chr; // 1
    double weight; // 8
}
@end

@interface LKXObjectDemo2 : NSObject {
    // isa // 8
    char chr; // 1
    int age; // 4
    double weight; // 8
    double hegiht; // 8
}
@end

@interface LKXObjectDemo3 : NSObject {
    @public
    // isa // 8
    char chr; // 1
    int age; // 4
    int idx; // 4
    double weight; // 8
    double hegiht; // 8
}
@end
  1. LKXObjectDemo1 分配内存48字节,使用内存40字节,假如起始位置是0x10020000

a. isa 占用内存8字节,起始位置是0x10020000,结束位置是0x10020007

b. int age 占用内存4字节,起始位置是0x10020008,结束位置是0x1002000B

c. double hegiht 占用内存8字节,起始位置也要是8的倍数,所以起始位置是0x10020010,结束位置是0x10020018

d. char chr 占用内存1字节,起始位置是0x10020018,结束位置是0x10020018

e. double weight占用内存8字节,起始位置也要是8的倍数,所以起始位置是0x10020020,结束位置是0x10020027

f. 0x27是40,因为对象内存对其是16,所以分配内存48

  1. LKXObjectDemo2 分配内存32字节,使用内存32字节,假如起始位置是0x10020000

a. isa 占用内存8字节,起始位置是0x10020000,结束位置是0x10020007

b. char chr 占用内存1字节,起始位置是0x10020008,结束位置是0x10020008

c. int age 占用内存4字节,起始位置也要是4的倍数,起始位置是0x1002000B,结束位置是0x1002000F

d. double weight 占用内存8字节,起始位置是0x10020010,结束位置是0x10020017

e. double hegiht 占用内存8字节,起始位置是0x10020018,结束位置是0x1002001F

f. 0x1F是32, 所以占用32字节

  1. LKXObjectDemo3 分配内存48字节,使用内存40字节,假如起始位置是0x10020000

a. isa 占用内存8字节,起始位置是0x10020000,结束位置是0x10020007

b. char chr 占用内存1字节,起始位置是0x10020008,结束位置是0x10020008

c. int age 占用内存4字节,起始位置也要是4的倍数,起始位置是0x1002000B,结束位置是0x1002000F

d. int idx 占用内存4字节,起始位置是0x10020010,结束位置是0x10020013

e. double weight 占用内存8字节,起始位置也要是8的倍数,起始位置是0x10020018,结束位置是0x1002001F

f. double hegiht 占用内存8字节,起始位置是0x10020020,结束位置是0x10020027

g. 0x27是40,因为对象内存对其是16,所以分配内存48

  1. demo3成员变量分析,从输出可以看出

a. demo3(0x101b0b840)的内存地址和chr(0x101b0b848)相差8个字节, 这个8个字节就是isa的地址, demo3指向的内存是 0x011d8001000085f9,LKXObjectDemo3 class的内存地址是 0x00000001000085f8,刚好是后9位相同,这说明isa指向类类型内存地址

b. 从chr(0x101b0b848)、chr2(0x101b0b849)相隔1字节,而且指向的内存0x0000000a00003363可以看出,3的ASCII码是33,c的的ASCII码是63

c. 从chr(0x101b0b848)age(0x101b0b84c)idx(0x101b0b850)的内存地址是相邻的,而且相隔4字节,说明成员属性分配内存必须是其类型长度的整数倍,因为int类型长度是4。因为char类型长度是1,所以没有影响。

d. weight(0x101c042c8)height(0x101c042d0)各占8字节

demo3->chr = 'c';
demo3->age = 10;
demo3->idx = 1;
demo3->weight = 120;
demo3->hegiht = 170;
NSLog(@"chr: %p, age: %p, idx: %p, weight: %p, height: %p", &(demo3->chr), &(demo3->age), &(demo3->idx),
&(demo3->weight), &(demo3->hegiht));

demo3: 0x101b0b840 
chr: 0x101b0b848, chr2: 0x101b0b849, 
age: 0x101b0b84c, idx: 0x101b0b850, 
weight: 0x101b0b858, height: 0x101b0b860

0x101b0b840: 0x011d8001000085f9 0x0000000a00003363
0x101b0b850: 0x0000000000000001 0x405e000000000000
0x101b0b860: 0x4065400000000000 0x0000000000000000
0x101b0b870: 0x0000000000000000 0x0000000000000000

p [LKXObjectDemo3 class]
(Class) $1 = 0x00000001000085f8

struct 内存分析

struct StructDemo1 {
    char ch; // 1
    double height; // 8
    float weight; // 4
    char *name; // 8
    int age; // 4
} StructDemo1;

struct StructDemo2 {
    char ch; // 1
    int age; // 4
    char *name; // 8
    double height; // 8
    float weight; // 8
} StructDemo2;

struct StructDemo3 {
    struct StructDemo1 s1; // 40
    struct StructDemo2 s2; // 32
    float weight; // 4
    char chr; // 1
    int index; // 4
    double height; // 8
} StructDemo3;
  1. StructDemo1内存是大小是40字节, 因为每个属性都必须是其类型length,假如起始位置是0x10020000

a. char ch 占用1字节,那么ch的起始位置是0x10020000, 结束位置是0x10020000

b. double height 占用8字节,起始位置也要是8的倍数,那么height的起始位置是0x10020008, 结束位置是 0x1002000F

c. float weight 占用4字节,weight的起始位置是0x10020010, 结束位置是 0x10020014

d. char *name 占用8字节,name的起始位置是0x10020018, 结束位置是 0x1002001F

e. int age 占用4字节,age的起始位置是0x10020020, 结束位置是 0x10020023

f. 0x23是36,因为struct内存对其是8字节,所以最终分配了40字节

  1. StructDemo2内存是大小是32字节,假如起始位置是0x10020000

a. char ch 占用1字节,那么ch的起始位置是0x10020000, 结束位置是0x10020000

b. int age 占用4字节,起始位置也要是4的倍数, age的起始位置是0x10020004, 结束位置是 0x10020007

c. char *name 占用8字节,name的起始位置是0x10020008, 结束位置是 0x1002000F

d. double height 占用8字节,那么height的起始位置是0x10020010, 结束位置是 0x10020017

e. float weight 占用4字节,weight的起始位置是0x10020018, 结束位置是 0x1002001B

f. 0x1B是28,因为struct内存对其是8字节,所以最终分配了32字节

  1. StructDemo1内存是大小是96字节,假如起始位置是0x10020000

a. struct StructDemo1 s1 占用40字节, s1起始位置是0x10020000,结束位置0x10020027

b. struct StructDemo2 s2 占用32字节, s1起始位置是0x10020028,结束位置0x10020047

c. float weight 占用4字节,weight的起始位置是0x10020048, 结束位置是 0x1002004B

d. char chr 占用1字节,那么chr的起始位置是0x1002004C, 结束位置是0x1002004C

e. int index 占用4字节,起始位置也要是4的倍数, index的起始位置是0x10020050, 结束位置是 0x10020053

f. double height 占用8字节,那么height的起始位置是0x10020058, 结束位置是 0x1002005F

g. 0x5F是96,刚好使用了96字节

补充

为什么要内存对齐?

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