IOS底层(三): alloc相关: 初探 alloc, init, new源码分析

OC底层源码/原理合集

首先我们先看下, 这三个打印

exp

依次输出的是 内容内存地址指针地址

打印结果

result

因为3个对象的指针指向同一内存空间, 所以内容, 内存地址是相同的, 但是对象的指针是不同的

这里留意下
%p -> &p1: 是对象的指针地址
%p -> p1: 是对象指针指向的的内存地址

由上面例子我们也可以看出, alloc是申请内存空间, 而init根本不会对内存空间做任何修改, p1, p2, p3 的指针地址是不同的, 但是他们都指向同一内存空间

exp2

接下来进入我们的主题看一下 alloc, init都做了些什么

我这边准备的是 objc4-781 源码项目
下载或者自己编译相可参考我之前写的 IOS底层(一): objc4-781 官方源码编译以及调试

alloc源码探索

首先先看一张图, 希望牢牢记住, 图片内容很重要


alloc+init整体源码探索流程

接下来我们拿objc4-781 源码走下流程

首先我们见一个 NSObject 类, 我这里名字叫 SAPerson

alloc-1.1

  • 第一步
    跳转alloc源码实现
// alloc源码分析---第一步
+ (id)alloc {
    return _objc_rootAlloc(self);
}
  • 第二步
    跳转_objc_rootAlloc源码实现
// alloc源码分析---第二步
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
  • 第三步
    跳转callAlloc源码实现
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif

    // No shortcuts available.
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

这里如果我们不清楚走了哪个方法可以通过打断点来调试, 会发现这边走的是_objc_rootAllocWithZone

关于上面方法解释下
//进来之后按顺序执行, 先执行 slowpath(checkNil && !cls)这个判断

关于slowpathfastpath我们点进去看下定义

#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))

其中的__builtin_expect指令是由编译器gcc引入的

1. 目的: 编译器可以对代码进行优化, 以减少指令跳转带来的性能下降。(性能优化)

2. 作用: 允许程序员将最有可能执行的分支告诉编译器

3. 写法: __builtin_expect(EXP, N)。表示 EXP==N的概率很大。

4. fastpath: __builtin_expect(bool(x), 1)。表示x值为真的概率很大如果方法放在if判断中, 执行真的if的几率很大。

5. slowpath: __builtin_expect(bool(x), 0)。表示x值为假的概率很大如果方法放在if判断中, 执行假的else的几率很大。

6. 在日常的开发中, 也可以通过设置来优化编译器 , 达到性能优化的目的, 设置位置 Build SettingsOptimization Level

alloc-编译优化设置

我们接着回头接着看callAlloc中判断

#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif

因为进来的checkNil为false, cls存在为true, 但!clsfalse
slowpath这个方法为false不执行return nil; 继续往下走

cls->ISA()->hasCustomAWZ()是用来判断一个类是否自定义了 +allocWithZone 方法, 我们是没有的, 所以这里走if里面的代码, 即走到_objc_rootAllocWithZone

  • 第四步
    跳转_objc_rootAllocWithZone源码实现
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

我们可以看到zone参数不再使用, 接下来走类创建实例对象内存空间方法

  • 第五步
    跳转_class_createInstanceFromZone源码实现, 这块开始是alloc的核心部分
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    // 判断类是否已经实现
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    // 一次性读取类的信息为了提高性能
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;

    // 计算内存空间大小
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        // 申请内存
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) {
        // 将cls类与obj的指针(即: isa)关联
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

重点! 重点!! 重点!!!由上面源码我们可以看到, alloc核心操作

1.cls->instanceSize计算开辟内存空间大小

  1. calloc开辟内存, 返回地址指针(obj)

3.obj->initInstanceIsa 类与isa指针关联 (其中isa是指示关系)

下面普及一个小知识点, 针对于下面例子, 点击进入NSObject, 可看到

NSObject *objc = [NSObject alloc];
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
OBJC_ROOT_CLASS
OBJC_EXPORT
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

可看出, 一旦创建NSObject 就会占用一部分内存空间, 为什么呢?
NSObject 默认有个isa对象, 对象就是个指针, isa需要开辟8字节(arm64)

我们看个流程图便于理解下


alloc-_class_createInstanceFromZone流程

1. cls->instanceSize:计算所需内存大小

首先通过断点可看到extraBytes为0

extraBytes

进入instanceSize源码,

    size_t instanceSize(size_t extraBytes) const {
        //  编译器快速计算内存空间大小方法
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);
        }

        //  alignedInstanceSize 对齐我们当前的实例size
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        // 最小16 
        if (size < 16) size = 16;
        return size;
    }

我们先看下alignedInstanceSize (虽然打断点, 会发现走的时候fastInstanceSize快速计算内存空间方法)

    uint32_t alignedInstanceSize() const {
        // word_align 字节对齐
        //这个方法主要做了, 将没有字节对齐的一些东西, 调整为字节对齐返还回去
        return word_align(unalignedInstanceSize());
    }

那没有字节对齐的是什么东西呢?我们再点击进去

    uint32_t unalignedInstanceSize() const {
        ASSERT(isRealized());
        return data()->ro()->instanceSize;
    }

data是当前类的信息(类是结构体), data()->ro->instanceSize就是获取这个类所有属性内存的大小, 这里只有继承NSObject的一个属性isa——返回8字节

再看下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;
}

例如: 传进来是一个8字节的isa指针, x = 8, 因为WORD_MASK = 8
则有8 + 7 & ~7, 15 & ~7

0000 1111 & ~7, 7为 0000 0111, ~7为 1111 1000

0000 1111 &
1111 1000 =
0000 1000 = 8 即返回8字节

我们看下流程图便于理解下

alloc-instanceSize

不过通过断点调试,会走cache.fastInstanceSize快速计算内存空间大小方法

点击进入看下源码

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

这里留意下__builtin_constant_p 也是编译器gcc的内建函数 用于判断一个值是否为编译时常数,如果传入参数的值是常数,函数返回 1,否则返回 0

fastInstanceSize

通过断点调试,会执行到align16

其中

#define FAST_CACHE_ALLOC_MASK         0x1ff8
#define FAST_CACHE_ALLOC_MASK16       0x1ff0
#define FAST_CACHE_ALLOC_DELTA16      0x0008
#if __LP64__
            uint16_t                   _flags;
#endif

_flags 是个16位无符号整数 与 FAST_CACHE_ALLOC_MASK进行按位与运算
其中 FAST_CACHE_ALLOC_MASK = 0x1ff8 = 8184 = 1111111111000

按位与运算

1111111111000   &
0000000010000   = 
0000000010000   = 16

所以size = 16
FAST_CACHE_ALLOC_DELTA16 = 0x0008 = 8
extra = 0

size + extra - FAST_CACHE_ALLOC_DELTA16 = 16 + 0 - 8 = 8

align16 方法传入 8

跳转align16源码实现

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

我们可以看到, 这是个16字节对齐方法, 跟刚才类似虽然传入的是8

x + 15 & ~15
23 & ~15,  15为 0000 1111, ~ 15为 1111 0000
0001 0111  & 
1111 0000  =
00010000 = 16 可看到返回16

最后的结果为 16,即内存的大小是以16的倍数增加的

这里先介绍下几个知识点

结构体的内存布局依赖于CPU操作系统编译器及编译时的对齐选项。结构体内部成员的对齐要求,结构体本身的对齐要求。最重要的有三点

(一)成员对齐对于结构体内部成员,通常会有这样的规定:各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数。但是也可以看到,有时候某些字段如果严格按照大小紧密排列,根本无法达到这样的目的,因此有时候必须进行padding。各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节编译器会自动填充也就是padding。

(二)然后,还要考虑整个结构体的对齐需求。ANSI C标准规定结构体类型的对齐要求不能比它所有字段中要求最严格的那个宽松,可以更严格。实际上要求结构体至少是其中的那个最大的元素大小的整数倍。因为有时候我们使用的是结构体数组,所以结构体的大小还得保证结构体数组中各个结构体满足对齐要求,同时独立的结构体与结构体数组中单个结构体的大小应当是一致的。

(三)编译器的对齐指令。VC 中提供了#pragma pack(n)来设定变量以n字节对齐方式。n字节对齐就是说变量存放的起始地址的偏移量有两种情况:第一、如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式,第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。结构的总大小也有个约束条件,分下面两种情况:如果n大于所有成员变量类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数。

整理一下

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

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

  • 结构体的整体对齐规则:结构体的总大小,即sizeof的结果,必须是其内部做大成员的整数倍,不足的要补齐

那么OC为什么需要16字节对齐

  • 通常内存是由一个个字节组成的,cpu在存取数据时,并不是以字节为单位存储,而是以块为单位存取,块的大小为内存存取力度。频繁存取字节未对齐的数据,会极大降低cpu的性能,所以可以通过减少存取次数来降低cpu的开销

  • 16字节对齐,是由于在一个对象中,第一个属性isa占8字节,当然一个对象肯定还有其他属性,当无属性时,会预留8字节,即16字节对齐,如果不预留,相当于这个对象的isa和其他对象的isa紧挨着,容易造成访问混乱

  • 16字节对齐后,可以加快CPU读取速度,同时使访问更安全,不会产生访问混乱的情况

2. calloc:申请内存,返回地址指针

我们之前已经通过instanceSize计算了内存大小
申请size大小, 并赋值给obj, obj是指向内存地址的指针 指针是obj

obj = (id)calloc(1, size);

这里我们可以通过断点来验证calloc

在平常的开发中,一般一个对象的打印的格式都是类似于这样的<NSObject: 0x01111111f>(是一个指针)。为什么这里不是呢?

  • 主要是因为objc地址 还没有与传入 的cls进行关联,
  • 同时印证了 calloc的根本作用就是 开辟内存

3. obj->initInstanceIsa:类与isa关联

经过calloc,内存已经申请好了,类也已经传入进来了,接下来就需要将 类与 地址指针 即isa指针进行关联,其关联的流程图如下所示


obj->initInstanceIsa

主要过程就是初始化一个isa指针,并将isa指针指向申请的内存地址,在将指针与cls类进行关联

可以通过断点调试来印证上面的说法,在执行完initInstanceIsa后,在通过po obj可以得出一个对象指针


initInstanceIsa

总结
通过对alloc源码的分析,可以得知alloc的主要目的就是开辟内存,而且开辟的内存需要使用16字节对齐算法,现在开辟的内存的大小基本上都是16的整数倍, 核心步骤有3步:计算 -- 申请 -- 关联

init源码探索

先看下init源码, 有两种类方法init, 实例方法init

类方法init

+ (id)init {
    return (id)self;
}

实例方法init

- (id)init {
    return _objc_rootInit(self);
}
id
_objc_rootInit(id obj)
{
    // In practice, it will be hard to rely on this function.
    // Many classes do not properly chain -init calls.
    return obj;
}

也可以看到返回的是传入的self 本身

new源码探索

先看底层, 可看到本身调用了callAlloc, init函数, 可以得出new等价于 [XXX alloc ] init]

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}

但是针对于重写初始化方法

- (instancetype)init {
    self = [super init];
    if (self != nil) {
        
        NSLog(@"1");
   
    }
    return self;
}

- (instancetype)initWithText {
    self = [super init];
    
    if (self != nil) {
        
        NSLog(@"2");
    
    }
    return self;
}
TestObj *obj1 = [[TestObj alloc] initWithText]; 走2
TestObj *obj2 = [TestObj new];   走1

new调用只会走1, 不难看出如果

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

推荐阅读更多精彩内容