当创建一个对象时,我们常用的是
[[Person alloc] init]
或者[Person new]
Person *p1 = [Person alloc];
Person *p2 = [p1 init];
Person *p3 = [p1 init];
// 其中p1、p2、p3所指向的地址都是一样的
// 但是&p1、&p2、&p3是三个不一样的对象,只是指向了同一个内存空间
由上,我们知道了当调用alloc
的时候,就创建了内存对象。
但alloc
如何实现的呢?而init
是何作用呢?
与之对应的[Person new]
又是怎么实现的?
通过三种方式了解所调用的内部函数libobjc.A.dylib
的objc_alloc:
- 直接下断点,通过control + in进入汇编内容
- 通过「Symbolic Breakpoint」,
alloc
符号断点到对应的NSObject
创建方法 - 通过「Debug Workflow - Always Show Disassembly」汇编指令进行跟踪
源码了解
既然了解大致的对象创建,但是要真正的了解alloc
原理、流程还相应的源码。
通过源码的调用顺序,大概整理了一下alloc
的执行流程。
alloc流程
虽然梳理了流程,但是是否真的就如所梳理的流程那也执行呢?
为了能够进行代码断点跟踪,就需要进行源码的编译运行。
Cooci【iOS_objc4-756.2 最新源码编译调试】
通过源码项目分析,我们可以清晰的跟踪函数调用顺序。
Person *p = [Person alloc];
调用到的是
+ (id)alloc {
return _objc_rootAlloc(self);
}
// 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*/);
}
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate
// shortcutting optimizations.
static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, bool allocWithZone=false) {
if (slowpath(checkNil && !cls)) return nil;
#if __OBJC2__
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
// No alloc/allocWithZone implementation. Go straight to the allocator.
// fixme store hasCustomAWZ in the non-meta class and
// add it to canAllocFast's summary
if (fastpath(cls->canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal.
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
// Has ctor or raw isa or something. Use the slower path.
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
#endif
// No shortcuts available.
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}
对于callAlloc
方法中的代码,先了解一下slowpath
和fastpath
// x 很可能不为 0,希望编译器进行优化
#define fastpath(x) (__builtin_expect(bool(x), 1))
// x 很可能为 0,希望编译器进行优化
#define slowpath(x) (__builtin_expect(bool(x), 0))
// (checkNil && !cls)大概率是false,因此不用事先加载[return nil]这一代码
if (slowpath(checkNil && !cls)) return nil;
接下来的,继续代码的执行
// 如果该类实现了allocWithZone方法,那么就不会走if里的逻辑,直接跳过
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
// No alloc/allocWithZone implementation. Go straight to the allocator.
// fixme store hasCustomAWZ in the non-meta class and
// add it to canAllocFast's summary
// 是否快速创建判断,内部调用了bits.canAllocFast()默认为false
if (fastpath(cls->canAllocFast())) {
} else {
}
}
创建实例的方法class_createInstance
函数
正常流程下来,如果没有重写allocWithZone
且不是快捷alloc
会到id obj = class_createInstance(cls, 0);
这句代码中,即下面代码。
id class_createInstance(Class cls, size_t extraBytes) {
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
static __attribute__((always_inline))
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil) {
if (!cls) return nil;
assert(cls->isRealized());
// Read class's info bits all at once for performance
//类构造函数和析构函数
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
// (!instancesRequireRawIsa()); 实例需不要原始Isa
bool fast = cls->canAllocNonpointer();
//根据内存对齐原则,最少分配16字节的内存
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) { //zone == nil && fast == YES
obj = (id)calloc(1, size); //根据size创建内存空间
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor); // isa初始化(关联isa)
}
else {
if (zone) { // 有额外的字节要求
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else { // 没有额外的字节要求
obj = (id)calloc(1, size);
}
if (!obj) return nil;
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
如果重写了allocWithZone
且allocWithZone=true
,则执行[cls allocWithZone:nil];
,最后还是来到class_createInstance()
。
// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone) {
id obj;
#if __OBJC2__
// allocWithZone under __OBJC2__ ignores the zone parameter
(void)zone;
obj = class_createInstance(cls, 0);
#else
if (!zone) {
obj = class_createInstance(cls, 0);
}
else {
obj = class_createInstanceFromZone(cls, 0, zone);
}
#endif
if (slowpath(!obj)) obj = callBadAllocHandler(cls);
return obj;
}
至此,一个完整的alloc
流程基本已经走完,返回了对应的obj
。
那回到上面的问题中,init
又是做了什么?new
又是怎么实现创建的呢?
// Replaced by CF (throws an NSException)
+ (id)init {
return (id)self;
}
- (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;
}
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
通过代码我们很清楚的就能知道,
init
没有任何实现,只是简单的返回本身而已,由此可以推断是预留给iOSer做工厂模式。
new
只是换一种方式去调用alloc
而已。
alloc()
创建对象的过程梳理到最后就是calloc()
创建内存空间,initIsa()
进行isa
的Class关联。
那么我们就要转移视线,去了解一下calloc()
和initIsa()
都做了什么?
calloc申请内存
创建对象,我们需要开辟多少的内存空间?
其中指针,对象,占用8字节,结构体则是按照结构中的详细内容进行内存计算。
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
#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
// 进行与或非的操作,进行字节对齐
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;
}
// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() {
assert(isRealized());
return data()->ro->instanceSize; // 遍历其中的属性,变量等实例的空间大小
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.(不小于16byte)
if (size < 16) size = 16;
return size;
}
内存空间分布
每一个属性会根据自身大小占用相应的内存空间,并且一次排列,不足位数的进行补齐
,但是为何同样的结构体内容,却因为内容顺序不同,占用不同的内存空间?
那就让我们继续~
内存对齐规则
- 数据成员对齐规则:结构体(struct)或联合体(union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的其实位置要从该成员大小或者成员的子成员大小(只要该成员又子成员,比如数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储)。
- 结构体作为成员:一个结构体中又另外的结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(struct a中存在struct b,b中又char,int,double元素,则struct b应该从8的整数倍开始存储)。
- 收尾工作:结构体的总大小,也就是sizeof的结果,必须是内部最大成员的整数倍,不足的要补齐。
为了验证对象的属性的内存对齐规则,对上面的Person
类新增加几个属性。
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@property (nonatomic, strong) NSString *hobby;
@property (nonatomic) char char1;
@property (nonatomic) char char2;
@end
从实例中的内存开辟情况可以发现,在申请内存的同时,还做了相应的顺序调整,使得申请的内存大小得到了优化。「二进制重排」
instanceSize 和 malloc_size
从上面的输入,发现同一个对象,为何输出的size却是不一样的?
为此,我们需要先根据内存对齐的原则,进行内存空间大小的一个计算
@interface Person : NSObject
@property (nonatomic, copy) NSString *name; //8byte
@property (nonatomic, assign) int age; //4byte
@property (nonatomic, assign) long height; //8byte
@property (nonatomic, strong) NSString *hobby; //8byte
@property (nonatomic) char char1; //1byte
@property (nonatomic) char char2; //1byte
@end
通过计算 8(name) + 8(height) + 8(hobby) + (4(age) + 1(char1) + 1(char2) + 2(补齐)) = 32byte + 8byte(isa)
= 40byte
通过函数名称可以推测:
instanceSize
是对象所需要申请的内存大小,
malloc_size
是系统实际开辟的内存大小,并且它们是不一致的。
为了证实这一推测,就需要去了解一下calloc
是如何创建内存空间的。
void *
calloc(size_t num_items, size_t size) {
void *retval;
retval = malloc_zone_calloc(default_zone, num_items, size);
if (retval == NULL) {
errno = ENOMEM;
}
return retval;
}
void *
malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size) {
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
void *ptr;
if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
internal_check();
}
// 返回的指针,其中应该创建好了内存空间大小
ptr = zone->calloc(zone, num_items, size);
if (malloc_logger) {
malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
(uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
}
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
return ptr;
}
其中ptr = zone->calloc(zone, num_items, size);
调用的属性函数。因此我们可以打印一下其中的实现
static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size) {
zone = runtime_default_zone();
return zone->calloc(zone, num_items, size); //依旧是一个属性函数
}
static void *
nano_malloc(nanozone_t *nanozone, size_t size) {
if (size <= NANO_MAX_SIZE) {
void *p = _nano_malloc_check_clear(nanozone, size, 0);
if (p) {
return p;
} else {
/* FALLTHROUGH to helper zone */
}
}
malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
return zone->malloc(zone, size);
}
static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested) {
MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);
void *ptr;
size_t slot_key;
size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
mag_index_t mag_index = nano_mag_index(nanozone);
nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
if (ptr) {
// 略过(主要是Error处理)
} else {
ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
}
if (cleared_requested && ptr) {
memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
}
return ptr;
}
在_nano_malloc_check_clear
中,发现有一个slot_bytes
变量,代替了外层传入的size
,因此我们需要了解一下segregated_size_to_fit
做了什么?
#define SHIFT_NANO_QUANTUM 4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM)
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey) {
size_t k, slot_bytes;
if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
slot_bytes = k << SHIFT_NANO_QUANTUM; // multiply by power of two quanta size
*pKey = k - 1; // Zero-based!
return slot_bytes;
}
追踪到segregated_size_to_fit
,发现其中做的操作,便是对size_t size
进行了16字节对齐
操作,因此当需要是size_t size
为40byte时,calloc
最终创建的空间大小便是48byte(以整个对象为整体的进行字节对齐)。
因此就能说明为什么instanceSize
所需要的内存大小是40byte,而系统malloc_size
创建出来的实际大小却是48byte。
calloc()
创建完内存空间后,接下来就是isa
的关联了!
isa
首先先了解一下isa
是什么?
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
通过源码,发现是一个union
联合体。
包含了两个初始化,一个Class cls
,一个uintptr_t bits
,和一个结构体
,其中属性cls
和bits
是两个互斥的,仅需对其一进行赋值。
当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体(union),利用union可以用相同的存储空间存储不同型别的数据类型,从而节省内存空间。
而为了解决union数据读写麻烦,所以有了位域的概念,也就是isa_t
联合体中的结构体。
下面就是isa_t
里面定义的位域-ISA_BITFIELD
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
通过宏定义,x86_64和arm64下的位域定义存在差异,不过都是占用了64bit。
含义:(以arm64为模版)
nonpointer:
1bit
表示是否对isa开启指针优化。0代表纯isa指针,1代表除了地址外,还包含了类的一些信息、对象的引用计数等。has_assoc:
1bit
关联对象标志位,0没有,1存在。has_cxx_dtor:
1bit
该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象。shiftcls:
33bit
存储类指针的值。开启指针优化的情况下,arm64架构中有33bit来存储类指针。magic:
6bit
用于调试器判断当前对象是真的对象还是一段没有初始化的空间。weakly_referenced:
1bit
标志对象释放被指向或者曾指向一个ARC的弱变量,没有弱引用的对象可以更快释放。deallocating:
1bit
标志对象释放正在释放内存。has_sidetable_rc:
1bit
当对象应用计数大于10时,则需要借用该变量存储进位。extra_rc:
19bit
表示该对象的引用计数值,实际上是引用计数减一。例如:如果引用计数为10,那么extra_rc为9。如果引用计数大于10,则需要使用has_sidetable_rc
isa的初始化
inline void
objc_object::initIsa(Class cls) {
initIsa(cls, false, false);
}
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.cls = cls;
} else { //对isa联合体位域进行初始化赋值
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;
}
}
既然创建了对象,isa和对象的关联在哪?
进行对象内存的打印,我们要知道对象的
第一个属性
必然是isa
,那就需要验证一下。
结合代码和联合体位域,我们知道isa.shiftcls
是存储类指针的值。
其中shiftcls
在arm64
中包含了33bit
,在__x86_64__
中包含了44bit
。
通过位移过滤的方式,就能获取到对应到shiftcls
信息,而在代码中,我们也能发现存在相应的函数替我们进行isa_mask
/***********************************************************************
* object_getClass.
* Locking: None. If you add locking, tell gdb (rdar://7516456).
**********************************************************************/
Class object_getClass(id obj) {
if (obj) return obj->getIsa();
else return Nil;
}
inline Class
objc_object::getIsa() {
if (!isTaggedPointer()) return ISA();
uintptr_t ptr = (uintptr_t)this;
if (isExtTaggedPointer()) {
uintptr_t slot =
(ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
return objc_tag_ext_classes[slot];
} else {
uintptr_t slot =
(ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
return objc_tag_classes[slot];
}
}
inline Class
objc_object::ISA() {
assert(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
return (Class)(isa.bits & ISA_MASK);
#endif
}
通过
object_getClass
函数,到最后的(Class)(isa.bits & ISA_MASK);
在不同架构中,对应的位域是不同的!
isa的指向关系
至此,我们通过对象内存中找到了类信息的存在,那我们就能通过关联,去查找isa的指向关系。
ISA
的指向:
- instance对象的isa指向了class对象
- class对象的isa指向了meta-class对象
- meta-class对象的isa指向了基类的meta-class对象
类
的继承关系:
- Subclass继承于SuperClass,meta_subClass继承于meta_superClass
- SuperClass继承于RootClass,meta_superClass继承于meta_rootClass
- meta_rootClass继承于RootClass
- RootClass指向nil(NSObject的父类是nil)
最后
对象alloc的创建,需要有calloc去开辟内存空间,需要isa进行类的关联。