注册

OC alloc 底层探索

一、alloc对象的指针地址和内存

有如下代码:

//alloc后分配了内存,有了指针。
//init所指内存地址一样,init没有对指针进行操作。
HPObject *hp1 = [HPObject alloc];
HPObject *hp2 = [hp1 init];
HPObject *hp3 = [hp1 init];
NSLog(@"%@-%p",hp1,hp1);
NSLog(@"%@-%p",hp2,hp2);
NSLog(@"%@-%p",hp3,hp3);

输出:

<HPObject: 0x600000f84330>-0x600000f84330
<HPObject: 0x600000f84330>-0x600000f84330
<HPObject: 0x600000f84330>-0x600000f84330


说明alloc后进行了内存分配有了指针,而init后所指内存地址一致,所以init没有对指针进行操作。
修改NSLog内容如下:

NSLog(@"%@-%p &p:%p",hp1,hp1,&hp1);
NSLog(@"%@-%p &p:%p",hp2,hp2,&hp2);
NSLog(@"%@-%p &p:%p",hp3,hp3,&hp3);

输出:

<HPObject: 0x600000e7c2c0>-0x600000e7c2c0 &p:0x7ffeefbf40d8
<HPObject: 0x600000e7c2c0>-0x600000e7c2c0 &p:0x7ffeefbf40d0
<HPObject: 0x600000e7c2c0>-0x600000e7c2c0 &p:0x7ffeefbf40c8


这就说明hp1hp2hp3都指向堆空间的一块区域。而3个指针本身是在栈中连续开辟的空间,从高地址->低地址。
那么alloc是怎么开辟的内存空间呢?


二、底层探索思路


  1. 断点结合Step into instruction进入调用堆栈找到关键函数:

73a569020f4c079daefd515d4868e5a1.png

找到了最中调用的是libobjc.A.dylibobjc_alloc:`。

下断点后通过汇编查看调用流程Debug->Debug workflow->Always Show Disassembly通过已知符号断点确定未知符号。

直接alloc下符号断点跟踪:

4a2bd9524666c25c786d1f60609f241f.png

三、alloc源码分析

通过上面的分析已经能确定allocobjc框架中,正好苹果开源了这块代码,源码:objc源码地址:Source Browser
最好是自己能编译一份能跑通的源码(也可以直接github上找别人编译好的)。当然也可以根据源码下符号断点跟踪调试。由于objc4-824目前下载不了,这里以objc4-824.2为例进行调试。

HPObject定义如下:


@interface HPObject : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;

@end

3.1 alloc

直接搜索alloc函数的定义发现在NSObject.mm 2543,通过断点调试类。
调用alloc会首先调用objc_alloc:

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


callAlloc会走到调用alloc分支。

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

alloc直接调用了_objc_rootAlloc

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

  • _objc_rootAlloc传递参数checkNilfalseallocWithZonetrue直接调用了callAlloc
  • 在调用objc_alloc的时候传递的checkNiltrueallocWithZonefalse

这里没什么好说的只是方法的一些封装,具体实现要看callAlloc



3.2 callAlloc

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
//表示值为假的可能性更大。即执行else里面语句的机会更大
if (slowpath(checkNil && !cls)) return nil;
//hasCustomAWZ方法判断是否实现自定义的allocWithZone方法,如果没有实现就调用系统默认的allocWithZone方法。
//表示值为真的可能性更大;即执行if里面语句的机会更大
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));
}

slowpath:表示值为假的可能性更大。即执行else里面语句的机会更大。
fastpath:表示值为真的可能性更大;即执行if里面语句的机会更大。
OBJC2:是因为有两个版本。Legacy版本(早期版本,对应Objective-C 1.0) 和 Modern版本(现行版本Objective-C 2.0)。

  • 在首次调用的时候会走alloc分支进入到alloc逻辑。
  • hasCustomAWZ意思是hasCustomAllocWithZone有没有自定义实现AllocWithZone。没有实现就走(这里进行了取反)_objc_rootAllocWithZone,实现了走allocWithZone:
  • 第二次调用直接走callAlloc的其它分支不会调用到alloc

⚠️:自己实现一个类的allocWithZone alloc分支就每次都被调用了


3.3 _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);
}

_objc_rootAllocWithZone直接调用了_class_createInstanceFromZone

3.4 allocWithZone

// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}


_objc_rootAllocWithZone直接调用了_objc_rootAllocWithZone,与上面的3.3中的逻辑汇合了。

3.5 _class_createInstanceFromZone

最终会调用_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
//判断当前class或者superclass是否有.cxx_construct构造方法的实现
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
//判断当前class或者superclass是否有.cxx_destruct析构方法的实现
bool hasCxxDtor = cls->hasCxxDtor();
//标记类是否支持优化的isa
bool fast = cls->canAllocNonpointer();
size_t size;
//通过内存对齐得到实例大小,extraBytes是由对象所拥有的实例变量决定的。
size = cls->instanceSize(extraBytes);
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);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
//初始化实例isa指针
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);
}

if (fastpath(!hasCxxCtor)) {
return obj;
}

construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
调用instanceSize计算空间大小。根据zone是否有值调用malloc_zone_calloccalloc进行内存分配。
calloc之前分配的obj是一块脏内存,执行calloc后才会真正分配内存。执行前后内存地址发生了变化。

根据!zone && fast分别调用initInstanceIsainitIsa进行isa实例化。
  • 执行完initInstanceIsa后再次打印就有类型了。
  • 根据是否有hasCxxCtor分别返回obj和调用object_cxxConstructFromClass

3.6 instanceSize 申请内存

在这个函数中调用了instanceSize计算实例大小:


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;
}
  • 没有缓存的话会调用alignedInstanceSize,如果最终的size < 16会返回16
  • 有缓存则调用fastInstanceSize
  • 正常情况下缓存是在_read_images的时候生成的。所以这里一般会走fastInstanceSize分支。

3.6.1 alignedInstanceSize



#ifdef __LP64__
# define WORD_MASK 7UL
#else
# define WORD_MASK 3UL

uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}

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

uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}

xunalignedInstanceSize获取。读取的是data()->ro()->instanceSize实例变量的大小。由ivars决定。这里为8,因为默认有个isaisaClass ,Classobjc_class struct *类型。

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
  • 字节对齐算法为:(x + WORD_MASK) & ~WORD_MASKWORD_MASK 64位下为732位下为3
    那么对于HPObject对象计算方法如下:
    根据公式可得1:(8 + 7) & ~7 等价于 (8 + 7) >>3 << 3
    根据1可得2:15 & ~7
    转换为二进制:0000 1111 & ~0000 0111 = 0000 1111 & 1111 1000
    计算可得:00001000 = 8
    所以alignedInstanceSize计算就是以8字节对齐取8的倍数(算法中是往下取,对于内存分配来讲是往上取)。

那么为什么以8字节对齐,最后最小分配16呢?
分配16是为了做容错处理。以8字节对齐(选择8字节是因为8字节类型是最常用最多的)是以空间换取时间,提高CPU读取速度。当然这过程中会做一定的优化。

3.6.2 fastInstanceSize

bool hasFastInstanceSize(size_t extra) const
{
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
}
return _flags & FAST_CACHE_ALLOC_MASK;
}

size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));

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
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}

  • fastInstanceSize中会调用align16,实现如下(16字节对齐):
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}

void setInstanceSize(uint32_t newSize) {
ASSERT(isRealized());
ASSERT(data()->flags & RW_REALIZING);
auto ro = data()->ro();
if (newSize != ro->instanceSize) {
ASSERT(data()->flags & RW_COPIED_RO);
*const_cast<uint32_t *>(&ro->instanceSize) = newSize;
}
cache.setFastInstanceSize(newSize);
}

size变化只会走会更新在缓存中。那么调用setInstanceSize的地方如下:

  • realizeClassWithoutSwift:类加载的时候计算。这里包括懒加载和非懒加载。这里会调用方法,根据类的实例变量进行size计算。这里是在_read_images的时候调用。
  • class_addIvar:动态添加属性的时候会重新计算实例大小。
  • objc_initializeClassPair_internal:动态添加类相关的初始化。

instanceSize对于HPObject而言分配内存大小应该为8(isa) + 8(name)+4(age)= 20根据内存对齐应该分配24字节。


3.7 initInstanceIsa

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

initIsa(cls, true, hasCxxDtor);
}

initInstanceIsa最终会调用initIsainitIsa最后会对isa进行绑定:

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) {
newisa.setClass(cls, this);
} 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);
#endif
newisa.extra_rc = 1;
}

// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
  • isa_t是一个union
  • nonpointer表示是否进行指针优化。不优化直接走setClass逻辑,优化走else逻辑。


作者:HotPotCat
链接:https://www.jianshu.com/p/884275c811d5



0 个评论

要回复文章请先登录注册