1828 字
9 分钟
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,他的指针:0x16d421bf82021-06-06 19:56:22.045611+0800 OCObject[10944:3914848] Objc2:0x28220bc60,他的指针:0x16d421bf02021-06-06 19:56:22.045659+0800 OCObject[10944:3914848] Objc3:0x28220bc60,他的指针:0x16d421be8- 不同的指针,指向了同样的内存空间
二、通过源码了解alloc实现
NSObject.mm这里就是NSobject的核心实现代码
2.1 可运行调试的源码
将项目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].idobjc_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 idcallAlloc(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 idcallAlloc(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_INLINEid_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 voidobjc_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#endif3.2 先思考一个问题:

- 显然第二种效率更高,但是牺牲了一定的空间。
- 这就是典型的
以空间,换时间,对于CPU来说处理速度非常重要
- 这就是典型的
3.3 内存调试
- 我们在第一个demo中下个断点

(lldb) x obj10x282d86140: 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 Classisa_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 0x0000000ffffffff8ULLc.复原出isa
再次输出
(lldb) po 0x000061a10441570d & 0x0000000ffffffff8Ryukie成功输出了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;@enda.一个内存对齐的场景
- 首先我们格式化打印分析 x/4gx
- 这种方式更加直观,不用我们自己去按小端的方式反着看了。
4代表打印4个单位的,可以按需要调整。
(lldb) x/4gx obj10x28150e2e0: 0x000041a100c0577d 0x00000012000000010x28150e2f0: 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;会是怎样呢?