探究对象个人认为需要从以下几个方面来探究下
1、创建方式
2、占用空间大小
3、如何创建
要分析以上内容,需先准备好一份源码,这里以objc4-838.1
为例,另附一点汇编指令。
b bl
跳转指令,方法调用ldr
将内存内容加载入通用寄存器ret
函数的返回;
注释
日常开发中常见的创建对象方式如下:
FMUserInfo *user1 = [FMUserInfo alloc];
FMUserInfo *user4 = [user1 init];
FMUserInfo *user2 = [[FMUserInfo alloc] init];
FMUserInfo *user3 = [FMUserInfo new];
其运行结果如下:
init
在这里我们可以看到user1
和user2
共用的同一份内存地址。通过查看源码可以看到对象的init
方法底层调用了_objc_rootInit
方法,并把 [FMUserInfo alloc]
创建的对象作为参数传入,而_objc_rootInit
则直接返回了obj。
- (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;
}
既然alloc
和init
都能同样的创建一个同地址的对象,那为什么平时开发过程中我们经常用alloc init
而不单单使用init
呢?
- 在这里
alloc
的作用是给对象分配内存,并将地址返回,但是分配内存后这篇内存还没有正确的初始化,所以在很多类方法中我们都需要写如下代码;其作用就是调用init方法为类中属性进行初始化。
- (instancetype)init{
self = [super init];
if (self) {
}
return self;
}
new
在看由new创建的对象的时候,我们看源码发现,其调用了以下方法:
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
在这里我们可以看到new方法最终调用的是 callAlloc
方法后最后再调用init
方法
我们看下callAlloc
方法定义:
static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, bool allocWithZone=false){......}
联想到当实例化一个对象的时候,其调用:
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
这里就不难看出,new
方法与alloc init
方法的区别:
- 无论是
alloc init
方法还是new
方法,都是类初始化的方法,区别在于,一个是显示调用,一个是隐式调用。大家目的都是一样的。只是在底层代码执行时会有点不同.- new方法虽然能一步到位,但其只能调用init,而alloc init可以派生出initwithFrame、initwithString等各种便于调用的方法。
- 这里要补充的是,编译器对于new还做了一些优化。
当我们打开Always Show Disassembly进行调试时,会发现它会调用一个名为objc_opt_new
的方法,在该方法内部最终调用了callAlloc
方法。
// Calls [cls new]
id
objc_opt_new(Class cls)
{
#if __OBJC2__
if (fastpath(cls && !cls->ISA()->hasCustomCore())) {
return [callAlloc(cls, false/*checkNil*/) init];
}
#endif
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(new));
}
在objc_opt_new
方法中,值得注意的是fastpath
函数
#define fastpath(x) (__builtin_expect(bool(x), 1)) //x很可能为真
#define slowpath(x) (__builtin_expect(bool(x), 0)) //x很可能为假
__builtin_expect
这个指令是gcc引入的,作用是允许程序员将最有可能执行的分支告诉编译器。这个指令的写法为:__builtin_expect(EXP, N)。
意思是:EXP==N的概率很大。- 作用:通过这种方式,编译器在编译过程中,会将可能性更大的代码紧跟着起面的代码,从而减少指令跳转带来的性能上的下降。
fastpath
函数所起到的作用:如果我们把FMUserInfo *user3 = [FMUserInfo new];
拿到第一行执行就会发现,当加断电点运行到objc_opt_new
方法时,如果FMUserInfo
类型的对象之前从没有创建过,那么他就会走objc_msgSend
分支,如果之前曾创建过,那么就会走callAlloc
分支。(编译器优化,下文alloc中
也有相关调用)
alloc
通过实际运行以及查看源码我们可以看到整个alloc的调用函数过程如下:
alloc-> objc_alloc —> callAlloc —> objc_msgSend —> alloc —> _objc_rootAlloc —> callAlloc —> _objc_rootAllocWithZone —> _class_createInstanceFromZone
对于为什么callAlloc
方法执行了两次
这里LLVM对底层方法进行了拦截优化,
alloc
方法被hook
成上面说的objc_alloc
方法,这样做的目的就是标记一个receiver
,在标记完这个类为receiver
之后都会进入普通的消息发送判断(调用fixupMessageRef
,然后第二次进入的alloc
方法),这样做的目的其实就是间接符号的绑定。当然在fixupMessageRef
中似乎不止alloc
,还有其他的几个方法,例如release
,retain
等。
/***********************************************************************
* fixupMessageRef
* Repairs an old vtable dispatch call site.
* vtable dispatch itself is not supported.
**********************************************************************/
static void
fixupMessageRef(message_ref_t *msg)
{
msg->sel = sel_registerName((const char *)msg->sel);
if (msg->imp == &objc_msgSend_fixup) {
if (msg->sel == @selector(alloc)) {
msg->imp = (IMP)&objc_alloc;
} else if (msg->sel == @selector(allocWithZone:)) {
msg->imp = (IMP)&objc_allocWithZone;
} else if (msg->sel == @selector(retain)) {
msg->imp = (IMP)&objc_retain;
}
}
......
}
callAlloc
点击 callAlloc
后这样就进入到alloc的核心方法:
// 重磅提示 这里是核心方法
/**
注意:
slowpath和fastpath 这两个都是objc源码中定义的宏,其定义如下:
//x很可能为真, fastpath 可以简称为 真值判断
#define fastpath(x) (__builtin_expect(bool(x), 1))
//x很可能为假,slowpath 可以简称为 假值判断
#define slowpath(x) (__builtin_expect(bool(x), 0))
其中的__builtin_expect指令是由gcc引入的,
1、目的:编译器可以对代码进行优化,以减少指令跳转带来的性能下降。即性能优化
2、作用:允许程序员将最有可能执行的分支告诉编译器。
3、指令的写法为:__builtin_expect(EXP, N)。表示 EXP==N的概率很大。
4、fastpath定义中__builtin_expect((x),1)表示x的值为真的可能性更大;即 执行if 里面语句的机会更大
5、slowpath定义中的__builtin_expect((x),0)表示 x 的值为假的可能性更大。即执行else 里面语句的机会更大
6、在日常的开发中,也可以通过设置来优化编译器,达到性能优化的目的,设置的路径为:Build Setting --> Optimization Level --> Debug --> 将None 改为 fastest 或者 smallest
cls->ISA()->hasCustomAWZ()
其中fastpath中的 cls->ISA()->hasCustomAWZ() 表示判断一个类是否有自定义的 +allocWithZone 实现,这里通过断点调试,是没有自定义的实现,所以会执行到 if 里面的代码,即走到_objc_rootAllocWithZone。
*/
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));
}
点击_objc_rootAllocWithZone
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);
}
callAlloc核心操作
点击_class_createInstanceFromZone后,可以看到alloc底层的三步重要操作:
-
cls->instanceSize(extraBytes)
计算需要开辟的内存空间大小 -
obj = (id)calloc(1, size)
申请内存,返回地址指针 -
obj->initInstanceIsa(cls, hasCxxDtor)
将 类 与 isa 关联
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) {
//将类cls和obj指针进行关联
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);
}
字节对⻬
这里我们专门看下size = cls->instanceSize(extraBytes);
计算对象所需要的空间
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());
}
#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;
}
size_t fastInstanceSize(size_t extra) cons
{
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);
}
}
在instanceSize方法中我们看到有两个分支
-
cache.fastInstanceSize()->align16()
这里返回16字节; -
alignedInstanceSize()->word_align()
这里返回8字节,但是返回instanceSize
后又做了16字节处理;
那么实际分配的内存大小呢?我们看下obj = (id)calloc(1, size);
这个方法calloc函数的实现在libmalloc
中,并且其最终会到_nano_malloc_check_clear
函数中。其中计算对象开辟空间大小的关键代码为
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
}
#define SHIFT_NANO_QUANTUM 4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
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;
}
这里NANO_REGIME_QUANTA_SIZE=16
,SHIFT_NANO_QUANTUM=4
,将size+15
,右移4位,再左移4位,将低4位变为0,就是16字节对齐。以size为2为例,如下图所示:
由此,在为对象开辟内存空间时,是以16字节对齐的方式。
为什么要字节对⻬?
字节是内存的容量单位。但是,CPU在读取内存的时候,却不是以字节为单位来读取的,⽽是以 “块”为单位读取的,所以⼤家也经常听到⼀块内存,“块”的⼤⼩也就是内存存取的⼒度。如果不 对⻬的话,在我们频繁的存取内存的时候,CPU就需要花费⼤量的精⼒去分辨你要读取多少字节, 这就会造成CPU的效率低下,如果想要CPU能够⾼效读取数据,那就需要找⼀个规范,这个规范就 是字节对⻬。
为什么对象内部的成员变量是以8字节对⻬,系统实际分配的内存以16字节对⻬?
以空间换时间。苹果采取16字节对⻬,是因为OC的对象中,第⼀位叫isa指针,它是必然存在的, ⽽且它就占了8位字节,就算对象中没有其他的属性了,也⼀定有⼀个isa,那对象就⾄少要占⽤8 位字节。如果以8位字节对⻬的话,如果连续的两块内存都是没有属性的对象,那么它们的内存空 间就会完全的挨在⼀起,是容易混乱的。以16字节为⼀块,这就保证了CPU在读取的时候,按照块 读取就可以,效率更⾼,同时还不容易混乱。
总结
由上可总结类 alloc时调用流程图
扩展:class_instanceSize
和 malloc_size
及sizeof()
区别
1)class_getInstanceSize
为获取对象实际占用的空间(要求符合内存对齐原则)
2)malloc_size
为系统为OC对象分配的空间(要求≥class_getInstanceSize
,且能被16整除,iOS规定一个对象至少分配16Byte空间)
3)sizeof()
:sizeof()
实际上不是函数,()传入的是类型常量(int 等),在编译时确定缓冲区的长度,不能返回动态分配的内存空间大小 e.g. sizeof(int) 为 4