[OC学习笔记]对象的本质探索
Posted Billy Miracle
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[OC学习笔记]对象的本质探索相关的知识,希望对你有一定的参考价值。
对象的本质
Objective-C 代码的底层都是通过 C/C++ 实现,所以 Objective-C 面向对象是基于 C/C++ 数据结构实现。
对于下面的OC代码:
@interface OSTestObject : NSObject
- (void)print;
@end
@implementation OSTestObject
- (void)print
@end
@interface OSTestObject1 : OSTestObject
@public int _count2;
@end
@implementation OSTestObject1
@end
int main(int argc, const char * argv[])
@autoreleasepool
OSTestObject1 *obj1 = [[OSTestObject1 alloc] init];
return 0;
我们使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
将其转换成为C++代码(即上面的第一步)。
下面摘取一部分重要的代码:
struct NSObject_IMPL
Class isa;
;
struct OSTestObject_IMPL
struct NSObject_IMPL NSObject_IVARS;
;
// - (void)print;
/* @end */
// @implementation OSTestObject
static void _I_OSTestObject_print(OSTestObject * self, SEL _cmd)
// @end
struct OSTestObject1_IMPL
struct OSTestObject_IMPL OSTestObject_IVARS;
int _count2;
;
/* @end */
// @implementation OSTestObject1
// @end
int main(int argc, const char * argv[])
/* @autoreleasepool */ __AtAutoreleasePool __autoreleasepool;
OSTestObject1 *obj1 = ((OSTestObject1 *(*)(id, SEL))(void *)objc_msgSend)((id)((OSTestObject1 *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("OSTestObject1"), sel_registerName("alloc")), sel_registerName("init"));
return 0;
结论:
OC对象的本质其实是结构体,对象里面存储了对象的成员变量信息。
对象创建的流程
alloc的流程
NSObject *object = [NSObject alloc];
其实,在这里打断点调试,系统并非直接走alloc
方法:
而是进入了一个叫objc_alloc
的函数:
id
objc_alloc(Class cls)
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
但是,我们还是先看一下alloc
:
+ (id)alloc
return _objc_rootAlloc(self);
再看:
id
_objc_rootAlloc(Class cls)
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
我们顺着这个一直点下去,和前面提到的那个方法一样,同样是调用了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));
因为这个类是第一次创建对象,类还没有初始化(懒加载),因此无法判断该类是否实现了allocWithZone方法,因而判断不成立,所以直接跳到下面allocWithZone
的判断,但是callAlloc
在调用的时候,传入的allocWithZone
是false
,因此直接走到return
,调用 [cls alloc]
。
+ (id)alloc
return _objc_rootAlloc(self);
继续往下看:
id
_objc_rootAlloc(Class cls)
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
注意,allocWithZone
参数变成true
了。
接下来调用allocWithZone
:
+ (id)allocWithZone:(struct _NSZone *)zone
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
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);
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);
我们知道一个对象的init
其实根本是调用了c语言里面的calloc
函数。但其实在这个操作之前还需要知道该对象所需要的内存大小。所以在源码中继续看_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;
//1.
size = cls->instanceSize(extraBytes);
//1.end
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
//2.zone为空进入else
if (zone)
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
else
obj = (id)calloc(1, size);
//2.end
if (slowpath(!obj))
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC)
return _objc_callBadAllocHandler(cls);
return nil;
//3. zone为nil, fast经判断后是true
if (!zone && fast)
// 将cls类与obj的指针(即: 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);
//3.end
if (fastpath(!hasCxxCtor))
return obj;
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
这里就是对象创建的关键代码。可以大致总结为3个部分。
- 计算对象内存大小 (cls->instanceSize)
- 向系统请求内存空间 (calloc)
- 设置isa指针,关联到类对象,初始化对象信息 (obj->initInstanceIsa)
我们可以看一下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.
// 最小16
if (size < 16) size = 16;
return size;
这个方法和后面要介绍的内存对齐存在关系。
init源码探索
先看下init
源码, 有两种:类方法init
,实例方法init
。
类方法init
:
+ (id)init
return (id)self;
实例方法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;
也可以看到返回的是传入的self 本身。
new源码探索
先看底层, 可看到本身调用了callAlloc
、init
函数, 可以得出new
等价于[XXX alloc ] init]
。
+ (id)new
return [callAlloc(self, false/*checkNil*/) init];
new与init的爱恨情仇
我们重写一下一个新类的init
方法:
- (instancetype)init
self = [super init];
if (self != nil)
NSLog(@"1");
return self;
- (instancetype)initWithName
self = [super init];
if (self != nil)
NSLog(@"2");
return self;
主函数:
Person *per = [[Person alloc] initWithName];
Person *per2 = [Person new];
输出:
2
1
new
调用只会走1。
结论:
- 子类没有重写
init
方法,new
调用父类init
方法 - 子类重写
init
方法,new
调用子类init
方法 - 子类重写
init
方法并自定义init
方法new
的话不会调用,所以alloc + init
更为灵活一些,扩展性好
对象的大小
在OC中有3个关于对象大小的API:
API | 所属库 | 含义 |
---|---|---|
sizeof | foundation | 成员变量所占空间 |
class_getInstanceSize | runtime | 所有成员变量所占空间 |
malloc_size | malloc | 对象分配的空间 |
其中sizeof
和class_getInstanceSize
的区别是,sizeof
为符号,在编译的过程就确定了值,而class_getInstanceSize
为函数,在运行的过程中才知道结果。
示例代码:
NSLog(@"结构体所占内存为%zd",sizeof(struct objcet_test));
NSObject *objc1 = [[NSObject alloc] init];
NSLog(@"NSobject的成员变量所占空间为%zd",class_getInstanceSize([NSObject class]));
NSLog(@"objc1所占内存空间为%zd", malloc_size((__bridge const void *)objc1));
运行结果:
内存对齐
造成上面情况的根本原因是内存对齐。可以大致理解为这样存储虽然数据空间会增大,但访问数据的效率会更高,是一种牺牲空间换取时间的操作。
在OC中是经过了2次内存对齐,一种是结构体的内存对齐,一种是对象的内存对齐。
其中class_getInstanceSize
获取到的是结构体内存对齐后的结果。
而malloc_size
获取到的是对象内存对齐后的结果。
结构体内存对齐规则
- 数据成员对齐规则:结构(struct)的第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储)。
- 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。(
struct a
里存有struct b
,b
里有char,int ,double等元素,那b
应该从8的整数倍开始存储)。 - 收尾:结构体的总大小,也就是
sizeof
的结果必须是其内部最大成员的整数倍,不足的要补齐。
对象内存对齐
oc对象内存对齐可以粗暴的理解为所需的内存必须是16的倍数。这是苹果系统分配内存处理的。为了进一步优化内存读取效率,内存在使用的时候是有bucket
这样一个概念的。苹果会把内存分为多个bucket
,其中bucket
最小的单位是16个字节。回到前面提到的instanceSize()
方法,先看里面的alignedInstanceSize
方法(虽然打断点时,会发现走的是fastInstanceSize
快速计算内存空间方法):
// Class's ivar size rounded up to a pointer-size boundary.
// 类的 ivar 大小向上舍入到指针大小边界。
// 将未内存对齐的类的属性的总大小传入 word_align() 方法中进行对齐计算。
uint32_t alignedInstanceSize() const
return word_align(unalignedInstanceSize());
// May be unaligned depending on class's ivars.
// 可能未对齐,具体取决于类的 ivar。
uint32_t unalignedInstanceSize() const
ASSERT(isRealized());
return data()->ro()->instanceSize;
static inline uint32_t word_align(uint32_t x)
return (x + WORD_MASK) & ~WORD_MASK;
解析:
我们知道NSObject对象有一个属性,那就是isa,是一个指针类型,所占空间大小为8字节,如果创建一个NSObject,我们看看这个方法如何计算的。看一下宏定义:
#define WORD_MASK 7UL
在64位下,WORD_MASK
为7,当传入的x为8的时候,那么x + WORD_MASK
为15,其二进制为:0000 1111
,WORD_MASK
的二进制为:0000 0111
, 那么~WORD_MASK
的二进制为:1111 1000
。那么15 & ~7
的计算为:
0000 1111
& 1111 1000
= 0000 1000
0000 1000
的十进制结果为8,即当传入x值为8的时候,经过计算后得到的结果为8字节。
那么假设传入x=9
,我们再计算一遍。当传入的x为9的时候,那么x + WORD_MASK
为16,其二进制为:0001 0000
,16 & ~7
的计算为:
0001 0000
& 1111 1000
= 0001 0000
0001 0000
的十进制结果为16,由此可知,类的属性总空间大小为9,经过对齐后需要的空间为16。
由上面的分析可以,对象申请内存空间的大小是8字节对齐计算的,我们得到的内存对齐后的数值就是对象创建的时候,向内存申请的空间大小,那么计算机真的是按照这个数值开辟的空间吗?
接下来看一下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);
这里留意下__builtin_constant_p
也是编译器gcc的内建函数 用于判断一个值是否为编译时常数,如果传入参数的值是常数,函数返回 1,否则返回 0。接着会走else
一直执行到align16
。
看一下size
的计算:
#define FAST_CACHE_ALLOC_MASK 0x1ff8
#define FAST_CACHE_ALLOC_MASK16 0x1ff0
#define FAST_CACHE_ALLOC_DELTA16 0x0008
#if __LP64__
uint16_t _flags;
#endif
_flags
是个16位无符号整数。接下来跳转到:
static inline size_t align16(size_t x)
return (x + size_t(15)) & ~size_t(15);
我们可以看到, 这是个16字节对齐方法。
例:x = 4
x + 15 & ~15
19 & ~15, 15为 0000 1111, ~ 15为 1111 0000
0001 0011 &
1111 0000 =
0001 0000 = 16
例:x = 8
x + 15 & ~15
23 & ~15, 15为 0000 1111, ~ 15为 1111 0000
0001 0111 &
1111 0000 =
0001 0000 = 16
例:x = 13
x + 15 & ~15
28 & ~15, 15为 0000 1111, ~ 15为 1111 0000
0001 1100 &
1111 0000 =
0001 0000 = 16
例:x = 24
x + 15 & ~15
39 & ~15, 15为 0000 1111, ~ 15为 1111 0000
0010 1001 &
1111 0000 =
0010 0000 = 32
内存的大小是以16的倍数增加的。
系统开辟空间
通过前面的探索,我们也看到了,系统一定会通过calloc()
方法开辟空间,系统在calloc()
方法中,对于开辟多大的空间,有自己的算法。
void *
calloc(size_t num_items, size_t size)
return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
其中default_zone
是一个默认的zone
, 目的是引导创建真正zone
流程。接下来看_malloc_zone_calloc
。
static void *
_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
malloc_zone_options_t mzo)
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
void *ptr;
if (malloc_check_start)
internal_check();
ptr = zone->calloc(zone, num_items, size);
if (os_unlikely(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);
if (os_unlikely(ptr == NULL))
malloc_set_errno_fast(mzo, ENOMEM);
return ptr;
其中这个ptr = zone->calloc(zone, num_items, size);
方法是我们要找的关键。想要Jump to,结果是:
void *(* MALLOC_ZONE_FN_PTR(cal以上是关于[OC学习笔记]对象的本质探索的主要内容,如果未能解决你的问题,请参考以下文章