前言
weak
弱引用的相关内容在开发中常遇到,那么这篇文章我们主要探索weak
的底层操作是什么样子的,开始吧!
准备工作
1. weak基本用法
weak
是弱引用,用weak
来修饰、描述所引用对象的计数器并不会增加,而且weak
会在引用对象被释放的时候自动置为nil
,这也就避免了野指针
访问坏内存而引起奔溃的情况,另外weak
也可以解决循环引用
。
面试拓展:为什么修饰代理使用weak
而不是用assign
?
assign
可用来修饰基本数据类型,也可修饰OC
的对象,但如果用 assign
修饰对象类型指向的是一个强指针
,当指向的这个指针释放之后,它仍指向这块内存,必须要手动给置为nil
,否则会产生野指针
,如果还通过此指针操作那块内存,会导致EXC_BAD_ACCESS
错误,调用了已经被释放的内存空间;而weak
只能用来修饰OC
对象,而且相比assign
比较安全,如果指向的对象消失了,那么它会自动置为nil
,不会导致野指针
。
2. weak的实现原理
Runtime
维护了一个weak
表,用于存储指向某个对象的所有weak
指针。weak
表其实是一个hash
(哈希
)表,Key
是所指对象的地址,Value
是weak
指针的地址 (这个地址的值是所指对象的地址)数组
。
weak
的实现原理可以概括一下三步:
-
初始化时
:runtime
会调用objc_initWeak
函数,初始化一个新的weak
指针指向对象的地址
。 -
添加引用时
:objc_initWeak
函数会调用objc_storeWeak()
函数,objc_storeWeak()
的作用是更新指针指向
,创建对应的弱引用表
。 -
释放时
:调用clearDeallocating
函数。clearDeallocating
函数首先根据对象地址获取所有weak
指针地址的数组
,然后遍历这个数组
把其中的数据设为nil
,最后把这个entry
从weak
表中删除,最后清理对象的记录。
3. weak底层原理探索
以下案例就是使用一个临时变量obc1
来存储创建出来的变量,代码如下:
NSObject * objc1 = [[NSObject alloc] init];
id __weak objc2 = objc1;
注意:
一般情况下,我们不会直接用__weak
来修饰一个刚创建出来的临时变量,因为__weak
修饰这个变量,运行时一创建出来就会释放
,而如果使用一个临时变量objc1
话,创建出来后会放到自动释放池
中,延缓这个变量的生命周期,变量的生命周期会跟着自动释放池自动保持
,所以这样能够保证不会一创建出来就会释放
。
然后我们运行代码,打开汇编断点查看底层调用了什么方法,如下:
进入断点查看,如下:
进入汇编断点
通过汇编断点可以看出底层是调用了
objc_initWeak
方法,这是入口。
4. 初始化源码分析
进入objc_initWeak
的内部添加断点,进行lldb
调试。
-
objc_initWeak
分析
在libobjc.dylib
源码中成功定位到了初始化过程,同时objc_initWeak
传入了两个参数,locatio
n即弱引用的地址
(存储在栈中
),newObjc
也就是创建的对象(存储在堆中
)。见下图:
objc_initWeak内部断点
根据以上的源码,可以分析得出: - 首先会判断对象
是否为空
,如果为空直接返回nil
。 - 如果不为空,则会调用
stroeWeak
方法进行存储
。 -
location
是弱引用的地址
; -
newObjc
是一个对象,在底层对象的实现就是objc_object
。
注意:object必须是一个没有被注册为__weak对象的有效指针。
4.1数据结构分析
在分析方法之前进行数据结构的分析是非常必要的!系统维护了一个SideTables
,那么SideTable
散列表为什么有多张?一张表不安全
,太多了性能不好
。真机下8
张表,其他环境下64
张,散列表也是一张hash
表。而SideTables
是一个hash
表,综合了链表和数组
的特点。拉链法
,同一个hash可以放在同一个表中
。
1. 散列表SideTable
struct SideTable {
//自旋锁
spinlock_t slock;
//引用计数
RefcountMap refcnts;
//对象的弱引用表
weak_table_t weak_table;
.....
//后面还有一些对锁的操作
}
SideTable
是个结构体
,其属性包括:自旋锁
、引用计数表
和弱引用依赖表
。
2. 弱引用表weak_table
struct weak_table_t {
//保存了所有指向指定对象的 weak 指针
weak_entry_t *weak_entries;
//存储空间
size_t num_entries;
//哈希算法辅助值(掩码)
uintptr_t mask;
//最大偏移量
uintptr_t max_hash_displacement;
};
weak
表是一个弱引用表
,实现为一个weak_table_t
结构体,存储了某个对象相关的所有的弱引用信息,这是一个hash
表。使用不定类型对象的地址作为key
,用weak_entry_t
类型结构体对象作为value
。其中的weak_entries
成员,即为弱引用表入口
。
3. weak_entry_t
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
//最开始会存放到inline_referrers, 放满之后存放到上面的 referrers
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
......
//还有一些结构体提供的方法
}
弱引用实体,有两个属性,一个对象
,另一个属性是一个联合体
,其中包含弱引用的数组
。
数据关系图:
4.2 objc_storeWeak分析
结束完所需类型的数据结构分析后,我们就开始进入核心方法的流程分析了,即分析objc_storeWeak
方法,该方法主要分为三个功能,如下:
-
判断应用是否存在值
功能1
-
-
如果有旧值,将旧值清除
功能2
-
- 有新增,重新初始化
功能3
根据上面的源码可以知道,objc_storeWeak
主要做了以下的三点: - 对当前引用的数据处理判断,判断该引用是
否存在旧值以及是否指向了新值
; - 如果引用当前有指向的值,即存在旧值,则需要将
旧值清除
; - 同时如果引用指向了新的对象,即存在新值,则需要进行对应对象
弱引用的初始化
工作。
我们从弱引用指向一个新值
开始探索,整理其核心代码如下:
if (haveNew) {
//根据对象回去对应的散列表
newTable = &SideTables()[newObj];
}
if (haveNew) {
// 从散列表中获取弱引用表,并传入
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected
// Set is-weakly-referenced bit in refcount table.
if (!newObj->isTaggedPointerOrNil()) {
newObj->setWeaklyReferenced_nolock();
}
// Do not set *location anywhere else. That would introduce a race.
*location = (id)newObj;
}
进入weak_register_no_lock
流程。代码如下:
//对象对应的全局弱引用表,对象指针,弱引用指针
id
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)
{
//对象
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;
//根据弱引用对象从weak_table中找出weak_entry_t
if ((entry = weak_entry_for_referent(weak_table, referent))) {
//将弱引用指针加入entry
append_referrer(entry, referrer);
}
else {
//通过弱引用指针与对象创建new_entry
weak_entry_t new_entry(referent, referrer);
//weak_table扩容
weak_grow_maybe(weak_table);
//将new_entry插入weak_table
weak_entry_insert(weak_table, &new_entry);
}
// Do not set *referrer. objc_storeWeak() requires that the
// value not change.
//返回对象
return referent_id;
}
- 在
weak_table
中找到对象对应的weak_entry_t
。 - 找到对应的
weak_entry_t
调用append_referrer
将弱引用指针加入weak_entry_t
(因为释放过程中有置空操作
,所以找空位nil加入
,这个过程中可能会进行扩容
)。 - 找不到则根据对象和弱引用指针创建
weak_entry_t
。- 调用
weak_grow_maybe
尝试扩容。 - 调用
weak_entry_insert
将创建的weak_entry_t
加入weak_table
。
- 调用
方法所传入的前三个参数分别为:弱引用表
、对象
、弱引用
。该方法会判断对象是否析构
,如果有就不会处理,直接返回。如果没有,则会通过weak_entry_for_referent
方法获取对象对应的weak_entry_t
见下图:
通过
hash
函数获取其表中的下标
,通过循环弱引用表
,找对应的下标
,获取对应的weak_entry_t
。获取weak_entry_t
后,去存储新的弱引用对象
,回到方法weak_register_no_lock
,在调用weak_entry_for_referent
之后,会通过append_referrer(entry, referrer);
方法进行弱引用的存储
。见下图:append_referrer
总结:
提供维护了一个
SideTables
,其中多张散列表SideTable
,每一张SideTable
表中有自旋锁
、引用计数表
、弱引用表weak_table_t
。弱引用表中存储一些实体weak_entry_t
,实体中包括了对象
和弱引用数组
。
如果弱引用存在旧值,具体操作是什么样子的呢?以下是核心代码:
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
进入weak_unregister_no_lock
方法,如下:
在此过程中,会获取弱应用对象对应的
实体weak_entry
,并调用remove_referrer
方法,从实体中移除对应的弱引用
。见下图:remove_referrer
此过程会
从对象的引用列表移除该的引用
,并将弱引用个数减1
。
5. 弱引用的清除流程
当一个对象调用dealloc
方法时,其弱引用处理流程见下面代码:
- (void)dealloc {
_objc_rootDealloc(self);
}
void _objc_rootDealloc(id obj)
{
ASSERT(obj);
obj->rootDealloc();
}
inline void objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
#if ISA_HAS_CXX_DTOR_BIT
!isa.has_cxx_dtor &&
#else
!isa.getClass(false)->hasCxxDtor() &&
#endif
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
id object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
obj->clearDeallocating();
}
return obj;
}
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());
}
最终会调用clearDeallocating
方法对弱引用进行处理。针对有弱引用的对象,会调用clearDeallocating_slow();
方法,最终的弱引用清除的处理流程在weak_clear_no_lock
中。见下图:
在此过程中,首先
在弱引用表中获取对象对应的实体
,开启循环
,将数组中的弱引用全部设为nil
,最后将实体从弱引用表中移除
。
网友评论