本文源码来自于 objc4-756.2 版本;
一、SideTable
本文研究 sideTable 在 objc4 源码中的使用及其作用,从而解析 iOS 中引用计数器和弱引用的实现原理;
1. retain 操作
我们都知道,新版本的 objc 中引入了 Tagged Pointer,且 isa 采用 union 的方式进行构造,其中 isa 的结构体中有一个 extra_rc
和 has_sidetable_rc
,这两者共同记录引用计数器。
直接看看 objc_object::rootRetain()
方法,只看 extra_rc 超出之后 sidetable 相关的代码,删减之后如下:
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
if (carry) {
// Leave half of the retain counts inline and prepare to copy the other half to the side table.
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}
if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
sidetable_addExtraRC_nolock(RC_HALF);
}
那么关键方法就是 sidetable_addExtraRC_nolock()
:
bool
objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
assert(isa.nonpointer);
// 取出this对象所在的SideTable
SideTable& table = SideTables()[this];
// 取出SideTable中存储的refcnts,类型为Map
size_t& refcntStorage = table.refcnts[this];
// 记录原始的引用计数器
size_t oldRefcnt = refcntStorage;
// 容错处理
assert((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
assert((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);
if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;
uintptr_t carry;
size_t newRefcnt =
addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
if (carry) {
// SideTable溢出处理
refcntStorage =
SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
return true;
} else {
// SideTable未溢出
refcntStorage = newRefcnt;
return false;
}
}
这个函数的逻辑如下:
- 根据 this,也就是对象的地址从 SideTables 中取出一个 SideTable;
- 获取 SideTable 的 refcnts,这个成员变量是一个 Map;
- 存储旧的引用计数器;
- 进行 add 计算,并记录是否有溢出;
- 根据是否溢出计算并记录结果,最后返回;
那么,这里有几个点需要解开:
- 什么是 SideTables;
- 什么是 SideTable;
- 什么是 refcnts;
- add 的计算逻辑为什么需要位移?
- SideTable 中的溢出时如何处理的?
接下来,一一解决~~~
2. SideTables
直接来看 SideTables 的代码:
static StripedMap& SideTables() {
return *reinterpret_cast*>(SideTableBuf);
}
首先,这是个静态函数,返回 StripedMap
类型,但是 &
是什么意思呢?这个是 C++ 语法,表示返回引用类型,看个例子:
&
的用法还有些限制,比如不能返回栈中的引用,否则会栈变量消失后会出现 error,还有一些其他的限制,有兴趣可以深究,这里只需要知道 &
表示返回引用类型,也就是可以通过 & func()
来获取函数返回值的指针,其他的不再赘述;
接着,比较懵逼的是 *reinterpret_cast
,其实这个是 C++ 的强制类型转换语法,不用深究,有兴趣的可以自行百度。
所以,总结下这段代码:
-
SideTables()
使用 static 修饰,是一个静态函数; -
&
表示返回引用类型; -
reinterpret_cast
是一个强制类型转换符号; - 函数最终的结果就是返回 SideTableBuf;
那么 SideTableBuf 又是什么?
3. SideTableBuf
直接看代码:
// We cannot use a C++ static initializer to initialize SideTables because
// libc calls us before our C++ initializers run. We also don't want a global
// pointer to this struct because of the extra indirection.
// Do it the hard way.
alignas(StripedMap) static uint8_t
SideTableBuf[sizeof(StripedMap)];
首先看注释,说明了两点:
- SideTables 在 C++ 的 initializers 函数之前被调用,所以不能使用 C++ 初始化函数来初始化 SideTables,而 SideTables 本质就是 SideTableBuf;
- 不能使用全局指针来指向这个结构体,因为涉及到重定向问题;
其实还是比较懵逼为什么 SideTableBuf 要这么设计,原理有待考究~~~估计和初始化有关;
继续看 SideTableBuf,要点包括:
- alignas 表示对齐;
- StripedMap
的 size 为 4096(存疑,待验证); - uint8_t 实际上是 unsigned char 类型,即占 1 个字节;
由此可以得出:
- SideTableBuf 本质上是一个长度为 sizeof(StripedMap
) 的 char 类型的数组;
同时也可以这么理解:
- SideTableBuf 本质上就是一个大小为和
StripedMap
对象一致的内存块;
这也是为什么 SideTableBuf 可以用来表示 StripedMap
对象。本质上而言,SideTableBuf
就是指一个 StripedMap
对象;
那么接下来就是搞清楚 StripedMap
是个什么东西了......
4. StripedMap
先上代码,删减一些方法之后的代码为:
template
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
struct PaddedT {
T value alignas(CacheLineSize);
};
PaddedT array[StripeCount];
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
public:
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}
const T& operator[] (const void *p) const {
return const_cast>(this)[p];
}
...省略了对象方法...
}
上述代码的逻辑为:
- 根据是否为 iphone 定义了一个
StripeCount
,iphone 下为 8; - 源码中
CacheLineSize
为 64,使用 T 定义了一个结构体,而 T 就是SideTable
类型; - 生成了一个长度为 8 类型为
SideTable
的数组; -
indexForPointer()
逻辑为根据传入的指针,经过一定的算法,计算出一个存储该指针的位置,因为使用了取模运算,所以值的范围是 0 ~ (StripeCount-1),所以不会出现数组越界; - 后面的
operator
表示重写了运算符 [] 的逻辑,调用了indexForPointer()
方法,这样使用起来更像一个数组;
至此,SideTables 的含义已经很清楚了:
- SideTables 可以理解成一个类型为 StripedMap
静态全局对象,内部以数组的形式存储了 StripeCount 个 SideTable;
那么第一个问题已经解决,按照 sidetable_addExtraRC_nolock()
方法中的逻辑,先从 SideTables 数组中取出一个 SideTable,然后进行相关操作,所以现在就来看看 SideTable 是个啥~~~
5. SideTable
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
...省略对象方法...
}
可以看到,SideTable 有三个成员变量:
- spinlock_t:自旋锁,负责加锁相关逻辑;
- refcnts:存储引用计数器的 Map;
- weak_table:存储弱引用的表;
自旋锁暂不讨论,来看看 refcnts 的定义:
typedef objc::DenseMap,size_t,RefcountMapValuePurgeable> RefcountMap;
DenseMap
就是一个 hash Map,过于复杂,先不看。来看看基类 DenseMapBase
中的部分代码,如下,DenseMapBase
中重写了操作符 []:
ValueT &operator[](const KeyT &Key) {
return FindAndConstruct(Key).second;
}
大意是通过传入的 Key 寻找对应的 Value。而 Key 是 DisguisedPtr
类型,Value 是 size_t
类型。即使用 obj.address :refCount 的形式来记录引用计数器;
回到最初的 sidetable_addExtraRC_nolock
方法中:
size_t& refcntStorage = table.refcnts[this];
上述代码就是通过 this
,即 object 对象的地址,取出 refcnts
这个哈希表中存储的引用计数器;
refcnts 可以理解成一个 Map,使用 address:refcount 的形式存储了很多个对象的引用计数器;
6. 引用计数器原理总结
- iphone 中 SideTables() 本质是返回一个 SideTableBuf 对象,该对象存储 8 个 SideTable;
- 因为涉及到多线程和效率的问题,必定不可能只使用一个 SideTable 来存储对象相关的引用计数器和弱引用;
- Apple 通过对 object 的地址进行运算之后,对 SideTable 的个数进行取模运算,以此来决定将对象分配到哪个 SideTable 进行信息存储,因为有取模运算,不会出现数组溢出的情况;
总结:
- objc 中当对象需要使用到 sideTable 时,会被分配到 8/64 个全局 sideTables 中的某一个表中存储相关的引用计数器或者弱引用信息;
7. weak_table
继续看弱引用如何实现的,从上文中可以看出,8/64 个 SideTable 对象中不仅保存了引用计数器相关的 Map,还保存了一个 weak_table,来看看 weak_table_t 源码:
/**
* The global weak references table. Stores object ids as keys,
* and weak_entry_t structs as their values.
*/
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
注释也说明了 weak_table_t 是一个全局引用表,object 的地址作为 key,weak_entry_t
作为 Value。只不过这个全局引用表有 8 或者 64 个;
即:
- weak_table 中以 weak_entry_t 的形式存储对象的弱引用;
那么具体是怎么存储的呢?这个 weak_entry_t
是什么,又是怎么用的呢,弱引用的存储逻辑是怎样的?
上述成员变量是 weak_entries
,前面又带 *
,感觉很像是一个指向类型为 weak_entry_t
的数组,如果是这样,那也正好和注释的描述相符,大胆猜测一下:
-
weak_table_t
中使用数组的形式来存储weak_entry_t
对象,以此来表示该表中每个对象的弱引用情况;
接下来就是验证~~~
8. store_weak流程分析
要弄清楚 weak_table_t 和 weak_entry_t 的使用,就要从新增弱引用作为突破口,来看看 objc_storeWeak()
方法:
id
objc_storeWeak(id *location, id newObj)
{
return storeWeak
(location, (objc_object *)newObj);
}
storeWeak 的定义如下:
enum CrashIfDeallocating {
DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};
template
static id
storeWeak(id *location, objc_object *newObj)
{
...省略...
}
这里不要被这些 template 吓到,storeWeak
只不过又是一个模板函数,这是 C++ 中的语法,可以暂不深究,有兴趣的可以去学习学习。
这里只需要知道,haveOld
和 haveNew
是作为参数来使用的,从上面的storeWeak
调用代码以及后文对这两个参数的使用,也可以看出个大概,不必纠结~~~
精简 storeWeak()
函数的代码如下:
SideTable *oldTable;
SideTable *newTable;
// 根据参数判断是否存在旧表决定使用哪个表进行存储
if (haveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
...省略很多异常场景处理代码...
// 只看 new 的逻辑
if (haveNew) {
// 在weak_table中新增弱引用
// 如果失败则会返回 nil,成功则返回对象本身
newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table, (id)newObj, location, crashIfDeallocating);
if (newObj && !newObj->isTaggedPointer()) {
// 成功则设置weakly_referenced为1;
newObj->setWeaklyReferenced_nolock();
}
// 赋值
*location = (id)newObj;
}
其中,location 是作为入参传递进来的,是被 __weak
修饰的指针本身,而 newObj 就是这个弱指针所指向的对象,伪代码如下:
__weak NSObject * location = newObj;
现在梳理下 storeWeak()
的逻辑:
- 根据 haveOld/haveNew 调用 SideTables() 方法,获取到 8/64 个全局 SideTable 中的某一个;
- 调用
weak_register_no_lock()
方法将 newObj 添加到 SideTable 的 weak_table 中,如果失败则会返回 nil,成功则返回对象本身; - 调用
setWeaklyReferenced_nolock()
方法,设置 isa 的weakly_referenced
为 1; - 将 location 正式指向 newObj 进行赋值,但是注意此时并没有调用 retain 方法,所以引用计数器不会 + 1;
那么接下来看看 weak_register_no_lock()
方法,精简后如下:
id
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, bool crashIfDeallocating)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;
...省略异常场景处理代码...
// now remember it and where it is being stored
weak_entry_t *entry;
if ((entry = weak_entry_for_referent(weak_table, referent))) {
// 存在 weak_entry_t 对象则直接新增
append_referrer(entry, referrer);
} else {
// 不存在则证明是第一次被弱引用,新建一个weak_entry_t对象
weak_entry_t new_entry(referent, referrer);
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);
}
return referent_id;
}
精简之后的代码逻辑非常清晰:
- 存在
weak_entry_t
则证明该对象存在其他的弱引用,直接在原来的weak_entry_t
最后新增一个new_referrer
; - 不存在
weak_entry_t
则证明该对象是第一次被弱引用,新增一个weak_entry_t
后插入;
append_referrer()
等插入的函数就不赘述了,还涉及到内联和外联的操作和实现,有兴趣的可以自己看代码;
9. 总结
一张图总结吧:
二、SideTables 的初始化时机和流程
1. 初始化流程
SideTables 也就是 SideTableBuf, 是在 SideTableInit()
方法中初始化:
static void SideTableInit() {
new (SideTableBuf) StripedMap();
}
来看看 SideTableInit 的调用顺序,代码就不贴了:
map_images_nolock( )
的代码太多了,就不贴了,只看下 arr_init( )
的调用代码吧:
要想知道 SideTables 何时被初始化,那么关键就在于 map_images( )
何时被调用,而这个函数应该相当熟悉了吧:
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
如上,_objc_init( )
调用 dyld 的 Api 注册通知并绑定了三个函数:
- map_images:印射到内存中的回调;
- load_images:加载时的回调;
- unmap_image:从内存中移除时的回调;
_dyld_objc_notify_register
最终调用 registerObjCNotifiers
函数,dyld 中的源码如下:
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
// record functions to call
sNotifyObjCMapped = mapped;
sNotifyObjCInit = init;
sNotifyObjCUnmapped = unmapped;
// call 'mapped' function with all images mapped so far
try {
notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
}
catch (const char* msg) {
// ignore request to abort during registration
}
}
如上图代码,mapped 的回调最终被赋值给了 sNotifyObjCMapped
,而该函数的调用只存在于 notifyBatchPartial ( )
中,且 state 为 dyld_image_state_bound
;
上述代码的注释也说的很清楚,回调赋值之后就立马尝试了一次 notifyBatchPartial( )
的调用。
至此可以总结一下:
- map_images 函数在第一次注册 dyld 监听时被调用,会将所有具有 objc section 的 image 进行回传;
- 如果有新的 objc 相关的 image 被印射到内存,也会触发 map_images 的调用;
- SideTables 在第一次处理包含 objc section 的 image 时被初始化(只会被初始化一次,具体参见源码);
- arr_init 只调用一次,所以这个 SideTables 是整个生命周期只会生成一次,记录着所有对象的引用计数器和弱引用关系。这也是为什么注释中写道不能析构的原因;
其逻辑如下:
2. 补充
再次温习下 _dyld_objc_notify_register()
函数:
翻译注释中的几个要点:
- 该方法仅供 objc runtime 使用;
- Dyld 会以数组的形式将包含 objc 相关 section 的 image 进行回调,调用 mapped 方法;
- 上述的 image 都已经被自动增加了引用计数器,不需要再调用
dlopen()
方法来维持 image 不被 unmap; - 新的包含 objc section 的 image 被
dlopen( )
时也会调用 mapped 回调; - 当将要调用 C++ 初始化方法时,init 回调将会被调用;
之前梳理过 dyld 流程复习一下:
- dyld 自举
- 加载共享缓存
- 实例化主程序
- 加载插入的动态库
- 链接主程序(递归加载依赖库、递归刷新层级、递归rebase、递归bind、weakbind暂不绑定)
- 链接插入的动态库;
- weak bind;
- 调用主程序初始化方法(依赖库初始化方法调用、主程序初始化方法调用)
- 寻找并调用main函数;
因为 libSystem 是依赖库,调用 libSystem 的初始化方法时,前面加载了所有的依赖库,所以此时的回调将会回调所有的包含 objc section 的 image 到 mapped 函数;
这种逻辑正好也是相称的,objc 的初始化流程大概是:
- 因为
ImageLoaderMachO::doModInitFunctions
符号断点不是第一次就进入libSystem_initializer
,所以可以确认某些层级高于 libsystem.B.dylib 库的初始化函数调用,这些库应该是非 OBJC 库; - libsystem.B.dylib 库可以理解成一个包装库,相对于其他 objc 库而言,需要优先被初始化。此时函数
libSystem_initializer
被调用; -
_objc_init
被调用,使用dyld_objc_register
绑定了三个 image 相关的回调并触发map_image( )
函数被调用,进而完成了 SideTables 的初始化(与此同时map_image
也做了很多其他的初始化操作); - 至此,在 objc 相关库(如Foundation、UIKit等)的初始化方法被调用之前,objc 的环境就已经被配置完成;
- 其他依赖库的初始化方法调用,触发 load_image 进而触发 +load 方法,此时会使用到 SideTables 等 objc 全局相关的配置;
3. 验证
来个符号断点:
看看结果:
再来个 + load 的调用栈:
三、疑问
1. 为什么 weak 能够自动置为 nil;
这个问题应该说的更具体一点:
被 __weak
修饰的对象在被析构之后,弱指针为何会被置为 nil?而 assign 修饰的指针则仍然存储原来的内存地址;
那么,这里就应该从对象的析构开始研究:
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
如上,如果开启了指针优化、没有弱引用、没有关联对象、没有 c++ 析构函数、引用计数器未存储到 sidetable 中,则直接 free(this)
,否则进入object_dispose()
;
很显然,我们要寻找的逻辑肯定不符合上述的条件,继续用看看这个函数的代码:
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
objc_destructInstance
函数中做了很多处理,比如 c++ 析构函数的处理、关联对象的处理等,暂时不关心这些逻辑,只关心弱引用逻辑,顺着代码最终进入到这个函数:
inline void
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
很显然 isa.weakly_referenced == 1
,我们要的逻辑在 clearDeallocating_slow
中,最终进入到 weak_clear_no_lock
函数,在这里我们找到了答案:
即:弱引用标志为 1 的对象在析构时,会遍历 weak_table 中的 referrers 数组并将指针置为 nil。该数组正是存储了哪些指针对该对象进行了弱引用。
2. 数据结构
其实 SideTable 可以作为复习数据结构的一个很好的实践例子,后续有时间可以研究下 refMap、weak_table 等各种数据结构的具体实现,暂略~~~;