iOS alloc 源码探究

开篇

alloc 日常开发运用很多,但很少去探究它的底层原理,今天抽时间出来探究一下,记录下alloc 的开辟流程;

查看alloc 底层原理的几种方式
1、通过符号断点定位:

通过 symbolic breakpoint ,下一个 名为 alloc 的符号断点,但有个缺点,这个符号断点会比较多,因为系统会有很多地方调用alloc方法
1、通过符号断点定位.png

2、在[LGPerson alloc]处断点,走到断点时,按住 control 键,就可以跳入源码

2、按control键.png

3、在[LGPerson alloc]; 处断点,走到断点时,点击debug—> debug workflow,就会出现汇编方式的源码

编译优化.png

alloc 开辟流程

截屏2021-05-13 上午10.58.00.png

如图所示,qly1 通过alloc,开辟内存空间,qly2、qly3 使用 `init`并没有对内存进行任何修改,
而是也指向qly1 开辟的内存空间。
复制代码

通过源码追踪,我们发现 alloc 的开辟流程如下:
截屏2021-06-06 下午2.54.07.png

接下来,我们详细分析一下具体流程:

1、callAlloc
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__ //判断当前 objc 版本
    /*
        知识点⚠️⚠️⚠️⚠️⚠️⚠️:
     #define fastpath(x) (__builtin_expect(bool(x), 1))
     #define slowpath(x) (__builtin_expect(bool(x), 0))
     
     __builtin_expect(x,Y/N) 这是unix的代码,会在汇编层优化代码,减少代码的跳转次数,从而节省性能;
     fastpath(x):表示传入的 x逻辑 很可能不为0,
     slowpath(x):表示传入的 x逻辑 很可能为0
     
     if (slowpath(checkNil && !cls)) return nil;
     所以 这个判断可以这么理解 :
     callAlloc(cls, flase, true) 传入的 checkNil 为 false,cls 很可能是有值的,那么编译器就不用每次都 return nil
     (那这里的判断,能不能理解为,如果 checkNil == true && cls 有值, 编译器处于不执行优化模式,则调用 return nil)
     */
    if (slowpath(checkNil && !cls)) return nil;    
    if (fastpath(!cls->ISA()->hasCustomAWZ()))
    {/*知识点⚠️⚠️⚠️⚠️⚠️⚠️:
      if 判断,判断 cls 是否已经实现 allocWithZone(开辟内存空间) 方法,
        如果没有,则调用系统方法,开辟空间。_objc_rootAllocWithZone()
        如果已实现allocWithZone,则调用下面  if (allocWithZone)  判断
      */
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif
    
    // No shortcuts available.
    if (allocWithZone) {//判断该类是否实现 allocWithZone 方法,如果实现,则走if 里的逻辑,如果没有实现,则调用消息转发,走alloc 流程;

        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
复制代码
2、_objc_rootAllocWithZone 和 _class_createInstancesFromZone
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();// 是否能创建 nonpointer 类型的对象
    size_t size;

    // 计算需要开辟多少内存空间
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;// obj 在这里,有内存地址信息,这是一种 脏内存 的存在形式。
    if (zone) {//早期苹果是通过zong来申请内存空间的,但现在都默认传入nil,所以该判断目前已经基本废弃了。
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size); // 申请开辟空间
    }
    if (slowpath(!obj))
    {//判断 obj 是否开辟成功
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC)
        {//obj 内存没有开辟成功,回调 _objc_callBadAllocHandler方法
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }
    
    if (!zone && fast) {//将 QLYPerson对象与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);
}
复制代码
2.1 size = cls->instanceSize(extraBytes) 源码分析:
size_t fastInstanceSize(size_t extra) const
{
    ASSERT(hasFastInstanceSize(extra));

    if (__builtin_constant_p(extra) && extra == 0) {
        return _flags & FAST_CACHE_ALLOC_MASK16;
    } else {
      /*
         知识点⚠️⚠️⚠️:
         FAST_CACHE_ALLOC_MASK = 0x1ff8
         _flags &  0x1ff8 ,两者相与,最后得到size 为 8 字节;
      */
        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);
}

/*
 知识点⚠️⚠️⚠️:
 由上可知,x = 8;
 所以 :  x + size_t(15) =  8 + 15 = 23;

0000 0000 0001 0111 ---- 23 的二进制
0000 0000 0000 1111 ---- 15 的二进制
1111 1111 1111 0000 ---- ~size_t(15)(15 取反 的二进制)


因此 (x + size_t(15)) & ~size_t(15)得值是:23 的二进制,与15取反的二进制相结合,取&
 0000 0000 0001 0111 ---- 23 的二进制
 1111 1111 1111 0000 ---- 15 取~(取反)的二进制
 0000 0000 0001 0000 ----- 23 与 ~15,最后取&的值 == 16 的二进制
*/
复制代码
2.2 计算开辟空间大小总结:最后得出,alloc 开辟空间,需要16字节大小

苹果早期,内存开辟是8个字节,目前新版是16字节;但目前软件内,每一个对象都有对应的 isa 指针,一个isa所占用的内存大小就是8字节;

内存分配是连续的,一片一片的,如果 对象 开辟空间,依旧使用 8字节,就会造成每一个对象都是紧挨着,没有富余空间,万一访问异常,就会访问到其他的isa指针,从而造成野指针,内存访问错误等问题。

所以使用16字节,有预留空间;

选用16字节,也是因为一个对象无论怎么扩展,最少是8个字节,而选用8的倍数,更便于系统高效,且安全处理。不选用32或者更大的 8 的倍数,也是因为减少内存浪费。所以 16 字节相对来说,更合理。

2.3、开辟空间
id obj;
   if (zone) {//早期苹果是通过zong来申请内存空间的,但目前已经废弃了。
       obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
   } else {//申请内存空间,返回内存空间的指针地址,赋值给 obj
       obj = (id)calloc(1, size);
   }
复制代码
2.4、内存指针与对象关联
    if (!zone && fast) {//对象关联
        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);
    }
复制代码
2.4.1、对象关联内部解析
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) {
    //知识点:⚠️⚠️⚠️⚠️⚠️⚠️⚠️nonpointer 表示是否对ias指针进行优化, 0 代表没有优化,是纯 isa指针;1代表进行优化,包含地址,类的信息,对象的引用计数等;
        newisa.setClass(cls, this);// 不是 nonpointer 的对象,直接将类对象赋值给 isa
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());
#if SUPPORT_INDEXED_ISA      //判断平台
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
#   if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);   // 类对象赋值给 isa
#endif
        newisa.extra_rc = 1;
    }
    isa = newisa;
}
复制代码
Tagged Pointer

ASSERT(!isTaggedPointer());判断是否为 小对象类型,如果是,直接返回true。因为小对象类型,是没有 isa 的;
Tagged Pointer 小对象类型;为了改进内存管理和效率,苹果引进了 Tagged Pointer
由于NSNumber、NSDate、NSInteger等这类变量,本身所需要的内存大小常常不到8字节,所以为了改进内存占用,在64位系统下,苹果引进了 Tagged Pointer
Tagged Pointer 指针指向的不再是地址,而是真正的值。而 Tagged Pointer实际上,并不是一个对象;
Tagged Pointer是将对象拆分为2个部分,一部分保存数据,一部分作为特殊标记(如下图,Tagged Pointer 内存图)
截屏2021-05-13 下午6.50.12.png

关联后,结果如下图:
截屏2021-05-13 下午10.07.10.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享