Links

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实现

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 先思考一个问题:

6
  • 显然第二种效率更高,但是牺牲了一定的空间。
    • 这就是典型的以空间,换时间,对于CPU来说处理速度非常重要

3.3 内存调试

  • 我们在第一个demo中下个断点
7
(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; 会是怎样呢?