在写 『iOS(Objective-C) 内存管理&Block』 一文时,我并没有发现 NSObject 的代码已经被开源了,所以分析的主要是 GNUStep 的源码,对 Apple 的部分只是通过猜测。
实质上,NSObject 的实现内容已经开源在 objc4-706 中。于是我便开始学习 objc4 中的内容。
下面就和大家扒一扒 Apple 的 NSObject 内存管理的一些内容。
SideTable
找到 NSObject.mm,首先来一些非常重要的信息,以便后面的理解。
objc4-706 NSObject.mm SideTable:
1 | struct SideTable { |
SideTable 结构体重定了几个非常重要的变量。
1 | // The order of these bits is important. |
以上定义的是几个重要偏移量。引用计数 retainCount 是保存在一个无符号整形中,也就是有 8 个字节。其结构可以用下图表示:
SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
(表示对象所在内存的第 1 位),标识该对象是否有过 weak 对象;SIDE_TABLE_DEALLOCATING (1UL<<1)
(表示对象所在内存的第 2 位),标识该对象是否正在 dealloc(析构)。SIDE_TABLE_RC_ONE (1UL<<2)
(表示对象所在内存的第 3 位),存放引用计数数值(其实第三位之后都用来存放引用计数数值)。
retainCount
找到 retainCount 的实现,一层一层向下看。
objc4 NSObject.mm retainCount:
1 | - (NSUInteger)retainCount { |
objc4 objc-object.h rootRetainCount:
1 | inline uintptr_t |
objc4 NSObject.mm sidetable_retainCount:
1 | uintptr_t |
it->second 指向的就是存放引用计数相关的那个 8 位的无符号整型。
上面介绍过 Sidetable 中的几个重要偏移量,通过位移 SIDE_TABLE_RC_SHIFT 可以获取真实的引用计数。
所以,sidetable_retainCount()
中的主要内容就是遍历引用计数表,查找对象获取引用计数 +1 并将结果返回。
retain
找到 retain 的实现。
objc4 NSObject.mm retain:
1 | - (id)retain { |
objc4 objc-objc.h rootRetain:
1 | // Base retain implementation, ignoring overrides. |
objc4 NSObject.mm sidetable_retain:
1 | id |
refcntStorage += SIDE_TABLE_RC_ONE
让人费解,实际是怎么回事呢?我们通过距离说明:
如果 obj 的引用计数数值为 1(二进制 00000100,因为第一位,第二位用来标识其他内容),现在如果进行 retain,需要对引用计数数值增加 1,那么需要由 00000100 => 00001000。所以实际上,从整型的角度,是 retainCount + 4
,而不是我们理解的 +1。
SIDE_TABLE_RC_ONE 定义是的 1UL<<2,也就是4,所以这里 refcntStorage += SIDE_TABLE_RC_ONE;
。
release
objc4-706 NSObject.mm release:
1 | - (oneway void)release { |
objc4-706 objc-object.h rootRelease:
1 | inline bool |
objc4-706 NSObject.mm sidetable_release:
1 | // rdar://20206767 |
看后面几个判断。
- 如果对象记录在引用计数表的最后一个:
do_dealloc
设置为 true,引用计数数值设置为 SIDE_TABLE_DEALLOCATING(二进制 00000010)。 - 如果 8 位的引用计数小于 SIDE_TABLE_DEALLOCATING(二进制 00000010),也就如果是 00000001 或 00000000:
do_dealloc
设置为 true,并添加 deallocating 标识位。(但至于有什么用不太理解,希望哪位大神指点一下)。 - 如果已经
8 位引用计数 & SIDE_TABLE_RC_PINNED
,即对象不在 deallocating,且没有被弱引用,且 8 位没有溢出:8 位引用计数减少 4,即真实引用计数数值 -1。 - 最后,如果
do_dealloc
和performDealloc
(传入时就已经为 true)都为 ture,执行 SEL_dealloc 释放对象。 - 方法返回 do_dealloc。
如果你只想知道 ARC 引用计数相关,那么只需要看上面的代码就可以了。alloc 和 dealloc 主要是对对象的一些内存分配。
alloc
查看 alloc 相关代码。
objc4-706 NSObject.mm alloc:
1 | + (id)alloc { |
objc4-706 NSObject.mm _objc_rootAlloc:
1 | id |
objc4-706 NSObject.mm callAlloc:
1 | // Call [cls alloc] or [cls allocWithZone:nil], with appropriate |
进入方法后先进行 if (slowpath(checkNil && !cls)) return nil;
判断。
objc4-706 objc-os.h slowpath:
1 |
__builtin_expect(exp, n)
方法表示 exp 很有可能为 0,返回值为 exp。你可以将 fastpath(x)
理解成真值判断,slowpath(x)
理解成假值判断。
所以,根据传入值,checkNil
为 false,checkNil && !cls
也为 false。那么这里不会返回 nil。继续向下阅读。
其后是一个 Objective-C 2.0 的条件编译指令。当然我们现在用的都属于 Objctive-C 2.0,会执行其中代码。首先进行一个判断 if (fastpath(!cls->ISA()->hasCustomAWZ()))... else ...
,这是判断一个类是否有自定义的 +allocWithZone
实现。
如果没有自定义的 +allocWithZone
实现。进行下一步,又是一个判断:if (fastpath(cls->canAllocFast()))... else ...
,这里只有对象不存在、没有 isa 等情况才会为真值。所以之间看 else 内容。
else 代码块中调用了 id obj = class_createInstance(cls, 0);
。查看内容时注意查看 objc-runtime-new.h
中的内容而不是 objc-runtime-old.mm
中的内容(你可以注意到 objc-runtime-new.h
顶部的 Coptyright 是 Copyright (c) 2005-2007 Apple Inc. All Rights Reserved.)
在 objc-runtime-new.mm
中 canAllocFast()
定义如下:
objc4-706 objc-runtime-new.h canAllocFast:
1 |
|
再看 FAST_ALLOC 定义,观察下图:
发现,#elif 1
直接拦截了下面的 define,所以 #if FAST_ALLOC
不起作用(这里我也不是很确定,哪位大神指点一下)。所以,canAllocFast()
返回 false,fastpath(cls->canAllocFast())
判断为假。
执行
1 | // Has ctor or raw isa or something. Use the slower path. |
objc4 objc-runtime-new.mm class_createInstance:
1 | id |
objc4 objc-runtime-new.mm _class_createInstanceFromZone:
1 | id |
传入的 extraBytes
为0,zone
为 nil,那么主要执行的语句是 bytes = calloc(1, size);
和 return objc_constructInstance(cls, bytes);
。bytes 是对象所需内存空间。
FYI:
calloc(size_t __count, size_t __size)
是 C 语言中的方法,用来在内存的动态存储区中分配 n 个长度为size的连续空间,函数返回一个指向分配起始地址的指针。如果分配不成功,返回NULL。
1 | objc_constructInstance(Class cls, void *bytes) |
objc_constructInstance
方法中,将 bytes
(指向分对象的指针)定义为 obj
,并将 obj
的 isa 赋值为传入的 cls。最后返回 obj。
FYI:
hasCxxCtor() 是判断当前 class 或者 superclass 是否有 .cxx_construct 构造方法的实现。
hasCxxDtor() 是判断判断当前 class 或者 superclass 是否有 .cxx_destruct 析构方法的实现。
参考:Objc 对象的今生今世
dealloc
找到 dealloc 的实现。
objc4 NSObject.mm dealloc:
1 | - (void)dealloc { |
objc4 NSObject.mm _objc_rootDealloc:
1 | void |
objc4 NSObject.mm _objc_rootDealloc:
1 | inline void |
objc4 objc-runtime-new.mm object_dispose:
1 | id |
objc4 objc-runtime-new.mm objc_destructInstance:
1 | /*********************************************************************** |
objc4 objc-object.h clearDeallocating:
1 | inline void |
objc4 NSObject.mm sidetable_clearDeallocating:
1 | void |
所以,objc_destructInstance(obj);
中进行了销毁实例但不释放内存,调用了 C++ 的析构函数(如果对象有),处理先关的对象(如果有),最后调用 obj->clearDeallocating();
清除 weak 引用、清除多余的 retain count。objc_destructInstance(obj);
之后是 free(obj);
释放 obj 占用的内存空间。
#Other
(在写这边文章的时候,我怎么感觉我最大的感触是,C++ 不懂。。)
写本篇博文时参考的所有资料:
有什么问题都可以在博文后面留言,或者微博上私信我。
博主是 iOS 妹子一枚。
希望大家一起进步。
我的微博:Lotty周小鱼