01.alloc与字节对齐

一、探索alloc

看下面的例子,输出会是什么样的呢?

Ryukie *obj1 = [Ryukie alloc];
Ryukie *obj2 = [obj1 init];
Ryukie *obj3 = [obj1 init];

NSLog(@"Objc1:%p,他的指针:%p", obj1, &obj1);
NSLog(@"Objc2:%p,他的指针:%p", obj2, &obj2);
NSLog(@"Objc3:%p,他的指针:%p", obj3, &obj3);

输出如下:

2021-06-06 19:56:22.045515+0800 OCObject[10944:3914848] Objc1:0x28220bc60,他的指针:0x16d421bf8
2021-06-06 19:56:22.045611+0800 OCObject[10944:3914848] Objc2:0x28220bc60,他的指针:0x16d421bf0
2021-06-06 19:56:22.045659+0800 OCObject[10944:3914848] Objc3:0x28220bc60,他的指针:0x16d421be8
  • 不同的指针,指向了同样的内存空间

二、通过源码了解alloc实现

Source Browser

Apple Open Source

NSObject.mm这里就是NSobject的核心实现代码

2.1 可运行调试的源码

GitHub

将项目Clone下来,即可运行调试。更加直观的学习底层实现。

2.2 调用流程探究

a. 实验代码

我们在main中写下如下代码,分别在<-处添加断点

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
        RYModel *father = [RYModel alloc];<-

        RYModel *objc = [RYModel alloc];<-    
    }
    return 0;
}

b. alloc

现在是RYModel第一次调用alloc。字面实现如下:

+ (id)alloc {
    return _objc_rootAlloc(self);
}

按照代码下一步应该会调用_objc_rootAlloc,真的吗?

c. objc_alloc

我们发现断点走到了这里,正如注释里面所说:Calls [cls alloc].

// Calls [cls alloc].
id
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

我们推断,这里应该是alloc的实现IMP被修改了。继续下一步

d. callAlloc

断点直接来到了 objc_msgSend 处,又继续调用了 alloc

// Call [cls alloc] or [cls allocWithZone:nil], with appropriate
// shortcutting optimizations.
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));
}

e. _objc_rootAlloc

这次断点才来到了,_objc_rootAlloc

id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/); <-
}

f. _objc_rootAllocWithZone

又来到了callAlloc方法中,这次断点来到了_objc_rootAllocWithZone

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false) {
#if __OBJC2__
    ...
    if (fastpath(!cls->ISA()->hasCustomAWZ())) { // 这里会判断类对象的isa指针是否有自定义的AllocWithZone的实现
        return _objc_rootAllocWithZone(cls, nil); <-
    }
#endif
    ...
}

_objc_rootAllocWithZone的实现:

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

我们继续下一步

g. _class_createInstanceFromZone 创建实例

/***********************************************************************
* class_createInstance
* fixme
* Locking: none
*
* Note: this function has been carefully written so that the fastpath
* takes no branch.
**********************************************************************/
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);// 1. 计算所需大小
    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); // 2. 开辟空间
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor); // 3. 实例化isa
    } 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);
}

g.1 instanceSize 大小

  • 这里会去缓存中查找

  • CoreFoundation规定最小16个字节

    • isa指针的8字节

    • 还有8字节预留

  • // Class's ivar size rounded up to a pointer-size boundary.

    • 成员变量的大小向上取整(一个指针的大小8字节)

    • 即:成员变量的字节对齐

    • 即:不足8字节就按8字节

  • // May be unaligned depending on class's ivars.

    • 大小取决于成员变量的大小

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

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}

// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() const {
    ASSERT(isRealized());
    return data()->ro()->instanceSize;
}

g.2 calloc 开空间

这里分配的还空间内还有数据,因为内存只覆写,不删除。

if (zone) {
    obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);// OBJC2 zone已经不用了,不走这里
} else {
    obj = (id)calloc(1, size);
}

g.3 initInstanceIsa 初始化实例isa

这里会将刚创建的obj和isa进行绑定,完成这一步后,可以发现,obj就会先变成RYModel了。

inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

h. 非第一次alloc

  • 发现在d. callAlloc这一步中有所不同:

  • 没有走return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));

  • 而是直接走了_objc_rootAllocWithZone

这里是LLVM做了处理,对alloc的IMP做了调整,在类第一次调用alloc时进行了一些处理,然后再调用原有的alloc实现。

i. 流程图

图片较大较长,建议下载到本地

三、字节对齐

3.1 算法

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}

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

#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

3.2 先思考一个问题:

  • 显然第二种效率更高,但是牺牲了一定的空间。

    • 这就是典型的以空间,换时间,对于CPU来说处理速度非常重要

3.3 内存调试

  • 我们在第一个demo中下个断点

(lldb) x obj1
0x282d86140: 0d 57 41 04 a1 61 00 00 28 00 41 04 01 00 00 00  .WA..a..(.A.....
0x282d86150: 42 00 00 00 00 00 00 00 40 61 d8 82 02 00 00 00  B.......@a......
(lldb) p 0x000061a10441570d // 小端、反着看
(long) $3 = 107344189019917
  • (long) $1 = 80361110149439809这里其实是isa指针,但是打印出来确实long类型?

3.4 isa 与 MASK

a.获取isa指针

// Get the class pointer out of an isa. When ptrauth is supported,
// this operation is optionally authenticated. Many code paths don't
// need the authentication, so it can be skipped in those cases for
// better performance.
//
// Note: this method does not support retrieving indexed isas. When
// indexed isas are in use, it can only be used to retrieve the class
// of a raw isa.
#if SUPPORT_INDEXED_ISA || (ISA_SIGNING_AUTH_MODE != ISA_SIGNING_AUTH)
#define MAYBE_UNUSED_AUTHENTICATED_PARAM __attribute__((unused))
#else
#define MAYBE_UNUSED_AUTHENTICATED_PARAM UNUSED_WITHOUT_PTRAUTH
#endif

inline Class
isa_t::getClass(MAYBE_UNUSED_AUTHENTICATED_PARAM bool authenticated) {
#if SUPPORT_INDEXED_ISA
    return cls;
#else

    uintptr_t clsbits = bits;

#   if __has_feature(ptrauth_calls)
#       if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
    // Most callers aren't security critical, so skip the
    // authentication unless they ask for it. Message sending and
    // cache filling are protected by the auth code in msgSend.
    if (authenticated) {
        // Mask off all bits besides the class pointer and signature.
        clsbits &= ISA_MASK;
        if (clsbits == 0)
            return Nil;
        clsbits = (uintptr_t)ptrauth_auth_data((void *)clsbits, ISA_SIGNING_KEY, ptrauth_blend_discriminator(this, ISA_SIGNING_DISCRIMINATOR));
    } else {
        // If not authenticating, strip using the precomputed class mask.
        clsbits &= objc_debug_isa_class_mask;
    }
#       else
    // If not authenticating, strip using the precomputed class mask.
    clsbits &= objc_debug_isa_class_mask;
#       endif

#   else
    clsbits &= ISA_MASK;
#   endif

    return (Class)clsbits;
#endif
}

这里要拿到isa需要&一个mask,在isa.h中可以找到对应架构的定义

b.获取MASk

下面是ARM64的,其他架构的可以自行在头文件中查找,很简单的

#     define ISA_MASK        0x0000000ffffffff8ULL

c.复原出isa

再次输出

(lldb) po 0x000061a10441570d & 0x0000000ffffffff8
Ryukie

成功输出了Class: Ryukie

3.5 查看真实的内存排列方式

@interface Ryukie : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) BOOL isboy;
@property (nonatomic, copy) NSString *desc;
@end

a.一个内存对齐的场景

  • 首先我们格式化打印分析 x/4gx

  • 这种方式更加直观,不用我们自己去按小端的方式反着看了。

    • 4代表打印4个单位的,可以按需要调整。

(lldb) x/4gx obj1
0x28150e2e0: 0x000041a100c0577d 0x0000001200000001
0x28150e2f0: 0x0000000100c00008 0x0000000100c00028
  • 这里发现 po 0x0000001200000001 结果很怪异 77309411329

  • 这里我们还剩 age & isboy 两个成员变量没有输出

  • 将这里的16位分开看下

    • po 0x00000012

      • 18

    • po 0x00000001

      • 1

      • 这里Bool只占一个字节但是拉伸为了8个字节

  • 这里就是比较明显的内存对齐的一个场景

b.思考

如果我们将上面的@property (nonatomic, assign) int age;改为@property (nonatomic, assign) NSInteger age; 会是怎样呢?

Last updated