开篇
alloc 日常开发运用很多,但很少去探究它的底层原理,今天抽时间出来探究一下,记录下alloc 的开辟流程;
查看alloc 底层原理的几种方式
1、通过符号断点定位:
通过 symbolic breakpoint ,下一个 名为 alloc 的符号断点,但有个缺点,这个符号断点会比较多,因为系统会有很多地方调用alloc方法
2、在[LGPerson alloc]处断点,走到断点时,按住 control 键,就可以跳入源码
3、在[LGPerson alloc]; 处断点,走到断点时,点击debug—> debug workflow,就会出现汇编方式的源码
alloc 开辟流程
如图所示,qly1 通过alloc,开辟内存空间,qly2、qly3 使用 `init`并没有对内存进行任何修改,
而是也指向qly1 开辟的内存空间。
复制代码
通过源码追踪,我们发现 alloc 的开辟流程如下:
接下来,我们详细分析一下具体流程:
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 内存图)
关联后,结果如下图: