alloc 源码分析 & init & new

一. 案例

分析源码之前,先来看一段代码

@interface LCPerson : NSObject
@end
@implementation LCPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LCPerson *p1 = [LCPerson alloc];
        LCPerson *p2 = [p1 init];
        LCPerson *p3 = [p1 init];
        
        NSLog(@"%@-%p-%p", p1, p1, &p1);
        NSLog(@"%@-%p-%p", p2, p2, &p2);
        NSLog(@"%@-%p-%p", p3, p3, &p3);
    }
    return 0;
}

%p:p1 打印的是对象指向的内存地址
%p:&p1 打印的是指向内存地址的指针地址

输出结果如下

可以看出:3 个对象指向的是同一个内存空间,但是指向这个对象的3个指针是不一样的。指向对象的指针空间是由栈分配的,栈空间从高位到低位,依次降低。因为是64位,指针大小为8字节,所以依次递减0x8。

二. 准备工作

  • 下载源码 objc-781
  • 编译源码,参考 objc 源码编译
  • 创建新的 Target, 创建 LCPerson 类

三. alloc 源码初探

alloc流程图

在 main.m 中导入创建的 LCPerson 头文件,在 main 函数中添加如下代码

LCPerson *p = [LCPerson alloc];
  • 按住 option 键,鼠标点击 alloc,进入 alloc 方法的源码实现
+ (id)alloc {
    return _objc_rootAlloc(self);
}
  • 按住 option 键,鼠标点击 _objc_rootAlloc,继续下一步,进入 _objc_rootAlloc 的源码实现
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__
    // checkNil 为false,!cls 也为false ,不会返回nil
    if (slowpath(checkNil && !cls)) return nil;
    // 是否有自定义的 +allocWithZone 实现
    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));
}
  • 当遇到 if & else 判断时,不确定会走哪步时,我们可以通过断点调试,发现进入的是 _objc_rootAllocWithZone,进入源码的实现
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    //zone 参数不再使用 类创建实例内存空间
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}
  • 进入 _class_createInstanceFromZone 的源码实现
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;

    // 1. 计算需要开辟的内存大小
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        // 2. 申请内存
        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);
}

以上源码的实现核心分为三部分

1. 计算需要开辟的内存大小

size_t size = cls->instanceSize(extraBytes);
  • 跳转到 instanceSize 的源码实现
size_t instanceSize(size_t extraBytes) const {
    //编译器快速计算内存大小
    if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
        return cache.fastInstanceSize(extraBytes);
    }
    
    // 计算类中所有属性的大小 + 额外的字节数0
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    //如果size 小于 16,最小取16
    if (size < 16) size = 16;
    return size;
}
  • 通过断点调试,可以知道,会去执行 fastInstanceSize 方法,快速计算内存大小,跳进 fastInstanceSize 查看源码实现
size_t fastInstanceSize(size_t extra) const
{
    ASSERT(hasFastInstanceSize(extra));
    //Gcc的内建函数 __builtin_constant_p 用于判断一个值是否为编译时常数,如果参数EXP 的值是常数,函数返回 1,否则返回 0
    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
        //删除由setFastInstanceSize添加的FAST_CACHE_ALLOC_DELTA16 8个字节
        return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
    }
}
  • 通过断点调试,会走 else 方法,这里我们跳转到 align16 的实现源码
static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

这里是一个16字节对齐算法

2. 申请内存

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

通过上一步计算的内存大小,向内存申请大小为 size 的内存,此时 obj 成了指向内存地址的指针。这个时候我们打印下 obj 在 calloc 前后看看是个什么东西?

在平常的开发中,我们打印一个对象,类似于这样的,这里为什么不是呢?

alloc 的本质是开辟内存;obj 还没有与传入的 cls 关联

3. 关联

obj->initInstanceIsa(cls, hasCxxDtor);

通过上面步骤,内存已经开辟,类也已经传进来,接下来就需要将类与地址指针(isa)进行关联。通过断点调试,执行完 initInstanceIsa,打印 obj

总结

  • 通过 alloc 源码分析,alloc 的本质是开辟内存,开辟内存需要16字节对齐
  • 核心三步骤:计算内存大小->申请内存->关联 isa

四. init & new 源码探索

在平时的开发中,我们创建一个对象,通常会使用[[NSObject alloc] init] 或者 [NSObject new] 方法,他们有什么区别,源码是如何实现的呢?

1. init

类方法 init 源码实现

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

这里的 init 是一个构造方法,是通过工厂设计,主要是用于给用户提供构造方法入口,强转为所需要的类型

实例方法 init 源码实现

- (id)init {
    return _objc_rootInit(self);
}
  • 继续跳转,查看 _objc_rootInit 的源码实现
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 本身

2. new 源码探索

new 方法的源码实现

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

通过上述源码可以得知,new 函数直接调用了 callAlloc 函数,并且调用了 init 函数,可以这么说 new 等价于 alloc & init。

⚠️在我们平时开发中,有些开发者为了节省代码或习惯使用 new ,这里不建议使用 new,主要是因为开发过程中,我们或多或少的会去重写 init 方法做一些自定义的操作,用 new 初始化可能会无法走到我们自定义的部分。

五. 拓展

在 alloc 源码探索的过程中,有遇到一些知识点,这里记录下

slowpath & fastpath

这两个是 objc 源码中定义的宏,作用是允许程序员将最有可能执行的分支告诉编译器

#define fastpath(x) (__builtin_expect(bool(x), 1)) //x很可能为真
#define slowpath(x) (__builtin_expect(bool(x), 0)) //x很可能为假

__builtin_expect 指令是由 gcc 提供给程序员使用的,目的是将“分支转移”的信息提供给编译器,这样编译器可以对代码进行优化,以减少指令跳转带来的性能下降
__builtin_expect(EXP, N):表示 EXP == N 的概率较大
__builtin_expect((x),1)表示 x 的值为真的可能性更大
__builtin_expect((x),0)表示 x 的值为假的可能性更大

如在 alloc 源码流程中的

if (slowpath(checkNil && !cls)) return nil;

checkNil 为false,!cls 也为false ,所以slowpath 为 false,假值判断不会走到 if 里面,即不会返回 nil

内存字节对齐

每个特定平台上的编译器都有自己的默认“对齐系数”。可以通过预编译命令 #pragma pack(n),n=1,2,4,8,16 来指定对齐系数,其中的 n就是“对齐系数”,也是需要对齐的字节数。

内存字节对齐的规则

  • 数据成员对齐规则:struct 或 union 的数据成员,第一个数据成员放在 offset 为 0 的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员大小的子成员大小(只要该成员有子成员,如数组、结构体等)的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储)
  • 数据成员为结构体:如果结构体的数据成员还为结构体,则该数据成员的“自身长度”为其内部最大元素的大小(即 struct a 里面有 struct b,把里面有 char,int,double 等,那么‘b’的自身长度应该为8)
  • 结构体的整体对齐规则:结构体的总大小,即 sizeof 的结果,必须是其内部最大成员的整数倍,不足的要补齐

编译器为什么要进行内存字节对齐

  • 内存是由一个个字节组成的,CPU 在存取数据时,并不是以字节为单位存储,而是以“块”为单位存取数据。因为频繁存取字节未对齐的数据,会极大降低 CPU 的性能。“块”的大小称为内存存取粒度,由 CPU 的地址总线决定,例如 32 位就是以4 字节为“块”,64 位就是以 8 字节为“块”。CPU 每次存取都会产生一个固定的开销,减少存取次数可以有效提升程序的性能(以空间换时间)

  • 一个对象中,第一个属性 isa 占 8 字节,系统会预留 8 字节,即16字节对齐。如果不预留,相当于这个对象的 isa 和其他对象的 isa 紧挨着,容易造成访问混乱

内存对齐算法

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

将计算的内存的大小加上 15,然后结果与上 (15 的取反)例如计算后的内存大小为8,与 15 相加为 23

23 的二进制为 0000 0000 0001 0111
15 的二进制为 0000 0000 0000 1111
15 的取反(~)二进制为 1111 1111 1111 0000
23 与 15 的取反结果进行 与(&)运算 结果为 0000 0000 0001 0000(即 16 的整数倍)

编译好的objc-781源码objc-编译源码

你可能感兴趣的:(alloc 源码分析 & init & new)