[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在调用的时候,传入的allocWithZonefalse,因此直接走到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个部分。

  1. 计算对象内存大小 (cls->instanceSize)
  2. 向系统请求内存空间 (calloc)
  3. 设置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源码探索

先看底层, 可看到本身调用了callAllocinit函数, 可以得出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所属库含义
sizeoffoundation成员变量所占空间
class_getInstanceSizeruntime所有成员变量所占空间
malloc_sizemalloc对象分配的空间

其中sizeofclass_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获取到的是对象内存对齐后的结果。

结构体内存对齐规则

  1. 数据成员对齐规则:结构(struct)的第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储)。
  2. 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct bb里有char,int ,double等元素,那b应该从8的整数倍开始存储)。
  3. 收尾:结构体的总大小,也就是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 1111WORD_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 000016 & ~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,  150000 1111, ~ 151111 0000

0001 0011  & 
1111 0000  =
0001 0000 = 16

例:x = 8
x + 15 & ~15
23 & ~15,  150000 1111, ~ 151111 0000

0001 0111  & 
1111 0000  =
0001 0000 = 16

例:x = 13
x + 15 & ~15
28 & ~15,  150000 1111, ~ 151111 0000

0001 1100  & 
1111 0000  =
0001 0000 = 16

例:x = 24
x + 15 & ~15
39 & ~15,  150000 1111, ~ 151111 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学习笔记]对象的本质探索的主要内容,如果未能解决你的问题,请参考以下文章

[OC学习笔记]objc_msgSend:方法快速查找

[OC学习笔记]类对象的结构

[OC学习笔记]对象消息运行期

[OC学习笔记]分类和关联对象源码解析

[OC学习笔记]分类和关联对象源码解析

[OC学习笔记]分类和关联对象源码解析