1.准备工作

源码下载:objc4-781 源码编译:可参考教程 源码编译调试

2.源码分析

2.1 alloc源码

源码在手,天下我有!接着就很简单了,创建一个Person类,然后调用 alloc 方法,main.m 如下:

#import

#import "Person.h"

int main(int argc, const char * argv[]) {

@autoreleasepool {

// insert code here...

Person *objc = [Person alloc];

NSLog(@"Hello, World! %@", objc);

}

return 0;

}

此时,按住command 鼠标左键就可以跳入alloc方法内部了 【第一步】NSObject.mm中的 alloc 实现,可以看到alloc内部调用了一个叫 _objc_rootAlloc 的函数,并传入了当前Person类

+ (id)alloc {

return _objc_rootAlloc(self);

}

【第二步】_objc_rootAlloc内部又调用了 callAlloc() ,同样传入了Person类

id _objc_rootAlloc(Class cls)

{

return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);

}

【第三步】callAlloc函数内部

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() 函数 这里解释下为什么会走 _objc_rootAllocWithZone() ? 我们的 NSObject 类经历过改版,原本会调用allocWithZone:方法,改版后去掉了。 【第四步】接下来,objc_rootAllocWithZone() 将我们的流程带入到了objc-runtime-new.mm文件中

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);

}

【第五步】_objc_rootAllocWithZone() 内部进入到了alloc源码的核心操作 _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());

// Person类或者父类是否有C++构造方法或析够方法

bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();

bool hasCxxDtor = cls->hasCxxDtor();

//是否能alloc nonpointer isa

bool fast = cls->canAllocNonpointer();

size_t size;

//计算对象所需大学,这里extraBytes == 0,由前一函数传递

size = cls->instanceSize(extraBytes);

//将计算好的size通过指针地址传递出去,不影响我们理解主流程,可以忽略

if (outAllocatedSize) *outAllocatedSize = size;

id obj;

//判断是否有zone,在之前的版本是有zone的

if (zone) {

obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);

} else {

// alloc 开辟内存的地方

obj = (id)calloc(1, size);

}

//这里是内存开辟错误执行的流程,字面意思可以看到,系统调用了alloc失败句柄

if (slowpath(!obj)) {

if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {

return _objc_callBadAllocHandler(cls);

}

return nil;

}

if (!zone && fast) {

// 将类与 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);

}

//若没有C++构造函数,则将关联好类的isa返回

if (fastpath(!hasCxxCtor)) {

return obj;

}

//若有c++构造函数,则走c++构造流程

construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;

return object_cxxConstructFromClass(obj, cls, construct_flags);

}

这段代码我写了注释,通过源码的阅读,我们发现这里执行了三个重要的操作,分别是: - cls->instanceSize():计算需要开辟的内存空间大小。 - calloc():申请内存,返回地址指针。 - obj->initInstanceIsa():将类与内存以及指针进行关联。

接下来我们分别看一下这三个函数。 - 首先,第一个函数,size = cls->instanceSize(extraBytes);这里cls就是我们之前传进来的参数Person类。因此,此时我们已经来到了objc_class对象中(详情请参考runtime中class的数据结构)

size_t instanceSize(size_t extraBytes) const {

//判断能否快速查找需要开辟的内存空间大小(查找是否有缓存数据)

if (fastpath(cache.hasFastInstanceSize(extraBytes))) {

return cache.fastInstanceSize(extraBytes);

}

//无法快速查找,alignedInstanceSize()计算

size_t size = alignedInstanceSize() + extraBytes;

// CF requires all objects be at least 16 bytes.

// 内存大小最小为16,不足的会补齐

if (size < 16) size = 16;

return size;

}

根据断点判断,这里调用了cache.fastInstanceSize(),cache是objc_runtime_new.mm 中的一个结构体。

size_t fastInstanceSize(size_t extra) const

{

ASSERT(hasFastInstanceSize(extra));

//GCC的内建函数 __builtin_constant_p 用于判断一个值是否为编译时常数,如果参数EXP 的值是常数,函数返回 1,否则返回 0

if (__builtin_constant_p(extra) && extra == 0) {

return _flags & FAST_CACHE_ALLOC_MASK16;

} else {

//类所需的内存大小已经存在于_flags中了,与一个常数做&运算就是size的精准大小

size_t size = _flags & FAST_CACHE_ALLOC_MASK;

// remove the FAST_CACHE_ALLOC_DELTA16 that was added

// by setFastInstanceSize

//删除由setFastInstanceSize添加的FAST_CACHE_ALLOC_DELTA16 8个字节

//之后做16位字节对齐,也就是按16取整

return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);

}

}

align16() 具体实现,优化过的取整运算,之后就得到了我们要分配的内存大小

static inline size_t align16(size_t x) {

return (x + size_t(15)) & ~size_t(15);

}

然后再来看看无法快速查找的流程 alignedInstanceSize(),这里,系统进行了两步操作,分别是计算了Person类占用大小,然后进行了字节对齐算法。

uint32_t alignedInstanceSize() const {

//第一步,计算对象要占用的内存大小

//第二步,对内存大小进行字节对齐算法,这里是8字节对齐

return word_align(unalignedInstanceSize());

}

unalignedInstanceSize() , 其实这个函数比较简单,就是调用了逐级查找,最终找到了 instanceSize ,另一方面也说明了instanceSize在类的初始化过程中已经计算好了。

uint32_t unalignedInstanceSize() const {

ASSERT(isRealized());

return data()->ro()->instanceSize;

}

其次,是 calloc() 内存空间的申请,这个函数在_malloc中。这个函数做的工作是分配一块大小为size的内存,并赋值给obj,因此 obj是指向内存地址的指针。

obj = (id)calloc(1, size);

最后是obj->initInstanceIsa(),这个函数负责将类与内存以及指针进行关联,具体如何关联,我们点进去看一下,这里,来到了objc_object.h 当中,这也表示着,我们上一步得到的 obj 是一个objc_object类型的对象。

inline void

objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)

{

ASSERT(!cls->instancesRequireRawIsa());

ASSERT(hasCxxDtor == cls->hasCxxDtor());

initIsa(cls, true, hasCxxDtor);

}

initInstanceIsa()函数内部又调用了initIsa(cls, true, hasCxxDtor);

inline void

objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)

{

ASSERT(!isTaggedPointer());

//判断是否是nonpointer类型对象,根据断点判断,我们的对象是nonpointer类型,因此走了else分支

if (!nonpointer) {

//利用cls初始化了一个isa_t的联合体,里面的代码并没有开源

isa = isa_t((uintptr_t)cls);

} else {

ASSERT(!DisableNonpointerIsa);

ASSERT(!cls->instancesRequireRawIsa());

//主流程

//新声明了一个isa_t类型的变量newisa,之后进行赋值

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;

//重点:shiftcls存放了类对象的指针,这里是赋值操作,从newisa的第3~35位,为类对象指针地址

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

isa = newisa;

}

}

这里是对isa的赋值操作,其实类对象的地址存在了isa的3~35位,详情请看nonpointerisa介绍。通过断点调试来印证上面的说法,在执行完initInstanceIsa后,在通过po obj可以得出一个对象指针

总结: 通过对 alloc 源码分析,我们了解到,alloc 开辟内存的核心步骤有3步,分别是 计算size -> 开辟内存 -> 关联对象

2.2 init源码

alloc源码分析完后,我们来看一下 init 源码。出乎意料的发现,init方法不只有实例方法,还有一个类方法 + (id)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 ,所以,这里的 init 不起任何作用,这里单纯的是一种工厂设计模式。系统开发了一个 init 的初始化函数,供我们在对象初始化是执行一些操作。

2.3 new源码

一般在开发中,初始化除了init,还可以使用new,两者本质上并没有什么区别,以下是objc中new的源码实现,通过源码可以得知,new函数中直接调用了callAlloc函数(即alloc中分析的函数),且调用了init函数,所以可以得出new 其实就等价于 [[Person alloc] init]的结论。

+ (id)new {

return [callAlloc(self, false/*checkNil*/) init];

}

3.总结

本篇文章从源码角度探索了 alloc/ init/ new三个方法的内部实现 - alloc: 主要负责了计算要开辟的内存大小,开辟内存,以及关联类。 - init:并没有执行任何操作,只是系统为我们提供的一个初始化的入口 - new: 等同于 [[Class alloc] init] 操作,可理解我alloc init的一个简写。

原文https://zhuanlan.zhihu.com/p/338173849

★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。

见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

参考阅读

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: