准备工作
先来看个例子
可以看到,三个对象指向同一个地址空间,内容和指针地址是相同的,但是对象的内存地址是不同的。why?这正是本篇博客需要去探索的。
探索
- 进入
alloc
的源码实现
+ (id)alloc {
return _objc_rootAlloc(self);
}
alloc
源码实现我们看不出什么,只知道调用了_objc_rootAlloc
,我们接着再看看_objc_rootAlloc
的源码实现
-
_objc_rootAlloc
的源码实现
// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
_objc_rootAlloc
的源码实现我们也看不出什么,我们继续往下探索
-
callAlloc
的源码实现
// 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));
}
这个方法的实现里面就有点东西了,光看源代码,我们不知道会进入哪个return,我们都打个断点看看:
这儿我躺了2个坑:
1、断点无效
2、
callAlloc
方法里面会走多次不同的return,我们要现在程序最开始那儿打断点,然后一步步往下才行:我们需要这样一步步来,不然我们不知道在
callAlloc
方法里面会具体进入那个return,因为系统也会调用callAlloc
。
- 进入
_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);
}
我们继续往下探索
- 进入
_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
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;
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;
}
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
、calloc
、initInstanceIsa
。
_class_createInstanceFromZone
详解
计算所需内存大小:instanceSize
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;
}
通过断点调试,发现进入的是cache.fastInstanceSize(extraBytes);
我们进入fastInstanceSize
实现
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);
}
}
断点调试,发现进入的是align16
方法
我们看看align16
的实现
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
此处涉及到一个知识点:16字节对齐
字节对齐原则
-
数据成员对齐规则
:struct 或者 union 的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如数据、结构体等)的整数倍开始(例如int在32位机中是4字节,则要从4的整数倍地址开始存储) -
数据成员为结构体
:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(例如:struct a里面存有struct b,b里面有char、int、double等元素,则b应该从8的整数倍开始存储) -
结构体的整体对齐规则
:结构体的总大小,即sizeof的结果,必须是其内部做大成员的整数倍,不足的要补齐
为什么需要16字节对齐
- 通常内存是由一个个字节组成的,cpu在存取数据时,并不是以字节为单位存储,而是以块为单位存取,块的大小为内存存取力度。频繁存取字节未对齐的数据,会极大降低cpu的性能,所以可以通过减少存取次数来降低cpu的开销
- 16字节对齐,是由于在一个对象中,第一个属性isa占8字节,当然一个对象肯定还有其他属性,当无属性时,会预留8字节,即16字节对齐,如果不预留,相当于这个对象的isa和其他对象的isa紧挨着,容易造成访问混乱
- 16字节对齐后,可以加快CPU读取速度,同时使访问更安全,不会产生访问混乱的情况
字节对齐总结
- 在字节对齐算法中,对齐的主要是
对象
,而对象的本质则是一个struct objc_object
的结构体 - 结构体在内存中是
连续存放
的,所以可以利用这点对结构体进行强转。 - 苹果早期是
8
字节对齐,现在是16
字节对齐
从图中可以看出,
align16
方法最终得出的结果为16的整数倍
申请内存,返回地址指针:calloc
通过instanceSize
方法获取到了需要申请内存的大小,接下来就可以开辟内存了。
obj = (id)calloc(1, size);
我们打印一下obj
(lldb) po obj
0x0000000100692830
发现是一个地址,但是我们平常打印一个对象时,输出的是<Person: 0x 01111111f >
这种形式,这里为什么只是输出了个地址呢?这是因为地址还没有与cls
关联。
类与isa关联:initInstanceIsa
通过断点调试,发现调用的代码是:obj->initInstanceIsa(cls, hasCxxDtor);
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
if (!nonpointer) {
isa = isa_t((uintptr_t)cls);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
isa_t newisa(0);
#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
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
// 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;
}
}
从上面可以看出initIsa
方法初始化了一个isa
指针,并将isa指针指向申请的内存地址,最后将指针
与cls
类进行关联
。
我们在执行完initInstanceIsa
方法后,打印一下obj
就可以得到一个对象了:
(lldb) po obj
<Person: 0x100692830>
alloc整体流程
init 源码探索
我们来看下init
- (id)init {
return _objc_rootInit(self);
}
id
_objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.
// Many classes do not properly chain -init calls.
return obj;
}
我们可以看到,在init
的源码实现中,最终返回了自身,没有做任何操作
new 源码探索
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
从源码可以看出,其等价于[alloc init]
。但是一般开发中并不建议使用new
,主要是因为有时会重写init方法做一些自定义的操作,用new初始化可能会无法走到自定义的部分。
结语
现在回到本文最开始的那个例子
- 因为p2和p3只是init了,根据之前的源码探索,可以知道,p1、p2、p3都是同一个对象,其指针地址都是相同的
- 在iOS中对象是连续存储的,我们可以发现这三个对象的地址差值都是8,这是因为有一个默认的isa,其大小刚好为8