该文章属于刘小壮原创,转载请注明:刘小壮
概述
在项目中我们经常用到weak
指针,其可以保证在指向的对象释放后,weak
指针自动置为nil
,以防止崩溃,因为在OC
中向nil
发送消息是没有任何处理的。通过__weak
、property weak
等形式,都可以将指针修饰为weak
类型的。
weak
的实现原理其实很简单,概括来说就是,在内存中有一个名为weak_table_t
的哈希表,weak_table_t
中存储着App
所有的weak
对象及指针。当有对象被weak
指针修饰时,会将被修饰的对象及指针添加到weak_table_t
表中。当被weak
指针的作用域消失时,weak
指针会被销毁,随后会从哈希表中查找对应的weak
指针,并将指针置为nil
。
weak引用表
SideTables
在weak
的实现中很多地方都用到了SideTables
函数,此函数内部直接返回了一个哈希表,哈希表名为SideTablesMap
。此函数用来保存整个程序所有被weak
指针指向的对象,和应用程序是一对一的。每个对象对应其中的一个元素,例如两个weak
指针指向一个对象,则这个对象对应一个SideTable
对象,这个对象中保存这两个weak
指针。
按照比较新的runtime 779.1
版本,对于SideTables
的定义如下。SideTables
本质上是一个静态函数,其内部是通过SideTablesMap
实现的。
static StripedMap<SideTable>& SideTables() {
return SideTablesMap.get();
}
下面定义是一个嵌套模型,其中SideTablesMap
是一个objc
命名空间下的ExplicitInit
类,其内部实现的上面的get
函数。StripedMap
也是一个模型,传入的SideTable
就是全局的weak
表所在的结构体。
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;
ExplicitInit
ExplicitInit
是一个模板类,传入的type
就是上面定义的StripedMap
。在get
函数中有reinterpret_cast
关键字,这个关键字类似于强制类型转换,改变类型但存储的数据不变。
template <typename Type>
class ExplicitInit {
alignas(Type) uint8_t _storage[sizeof(Type)];
public:
template <typename... Ts>
void init(Ts &&... Args) {
new (_storage) Type(std::forward<Ts>(Args)...);
}
Type &get() {
return *reinterpret_cast<Type *>(_storage);
}
};
SideTable
SideTable
结构体是weak
实现的核心,结构体中定义了引用计数表和弱引用表,弱引用使用weak_table
字段。下面的slock
自旋锁,两个表都会使用同一个锁。
struct SideTable {
// 自旋锁,保证线程安全
spinlock_t slock;
// 引用计数表,在未开启isa指针优化,或isa指针存储满了才会用
RefcountMap refcnts;
// 弱引用表
weak_table_t weak_table;
};
weak_table_t
weak_table_t
是弱引用表,所有的弱引用都会被存储在这个表中。在下面结构体变量的定义中,weak_entries
是一个哈希表的结构,其中key
是堆区内存地址,通过key
可以获取weak_entries
链表中对应的weak_entry_t
,也就是指针数组。
struct weak_table_t {
// 弱引用数组,用来存储weak_entry_t对象,是一个链表结构
weak_entry_t *weak_entries;
// 弱引用数组大小,如果到阈值会自动扩容
size_t num_entries;
// 进行哈希运算的mask,大小是num_entries-1
uintptr_t mask;
// 最大冲突数,一般不会大于这个数
uintptr_t max_hash_displacement;
};
weak_entry_t
weak_entry_t
结构体内部的定义是一个union
联合体,联合体中包含两个结构体,此结构体用来存储指针地址。在存储时会判断,如果同一个对象的weak
指针数量少于4
,则使用inline_referrers
定长数组,否则使用referrers
动态长度数组。
有趣的是,因为是union
联合体,当weak
指针数量大于4
之后,不需要另外开辟空间,在当前空间直接覆盖inline_referrers
的存储区域,使用referrers
哈希表。
#define WEAK_INLINE_COUNT 4
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
// 弱引用该对象的,指针地址的哈希数组
weak_referrer_t *referrers;
// 是否使用动态长度数组
uintptr_t out_of_line_ness : 2;
// referrers数组中的元素
uintptr_t num_refs : PTR_MINUS_2;
// 参与哈希运算的值,是referrers数组分配的长度,会随着动态扩容而改变
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
};
weak_referrer_t
无论是inline_referrers
数组还是referrers
链表,都是一个weak_referrer_t
的定义,在这个typedef
中定义了一个指向objc_object
的指针,isa
指针都是被优化为objc_object
的,所以这个指针指向这个被优化的isa
。也可以简单理解为,weak_referrer_t
本质上存储着被weak
修饰的指针地址。
结合weak_entry_t
的定义,可以知道referrers
数组存储结构,就是直接将DisguisedPtr
转换后的整型负数,存储在referrers
数组中。
typedef DisguisedPtr<objc_object *> weak_referrer_t;
DisguisedPtr
DisguisedPtr
是一个模板工具类,主要起到将指针和整型相互转换的作用,目的是为了隐藏指针,起到一个伪装的作用。隐藏指针的作用在于,一方面是为了安全性,另一方面也防止leaks
这些检测工具的误判。
template <typename T>
class DisguisedPtr {
// 存储地址转换为整数的结果
uintptr_t value;
// 将地址转为整数,并取反
static uintptr_t disguise(T* ptr) {
return -(uintptr_t)ptr;
}
// 将转换为负数的地址,先取反随后转换为地址
static T* undisguise(uintptr_t val) {
return (T*)-val;
}
public:
// 构造函数
DisguisedPtr() { }
// 通过指针,初始化value
DisguisedPtr(T* ptr)
: value(disguise(ptr)) { }
DisguisedPtr(const DisguisedPtr<T>& ptr)
: value(ptr.value) { }
// 运算符重载,将指针转换为DisguisedPtr的过程,直接由“&”取地址符来实现
DisguisedPtr<T>& operator = (T* rhs) {
value = disguise(rhs);
return *this;
}
DisguisedPtr<T>& operator = (const DisguisedPtr<T>& rhs) {
value = rhs.value;
return *this;
}
operator T* () const {
return undisguise(value);
}
// 运算符重载,将地址转为指针
T* operator -> () const {
return undisguise(value);
}
T& operator * () const {
return *undisguise(value);
}
T& operator [] (size_t i) const {
return undisguise(value)[i];
}
};
创建weak指针
objc_initWeak
通过__weak
等形式创建的指针,编译器会将其转换为objc_initWeak
函数的调用,并将被指向对象及weak
指针传进去。但需要注意的是,此函数并不是线程并发安全的,所以需要注意多线程的使用。
id objc_initWeak(id *location, id newObj) {
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
在调用函数后会将haveNew
、newObj
等参数传入,供storeWeak
调用。
storeWeak
无论是创建weak
指针还是销毁weak
指针,其内部实现都是通过storeWeak
函数实现的。storeWeak
函数是一个C++
的模板函数,函数会传入五个参数。其中haveOld
和haveNew
是互斥的,haveNew
则表示新创建的weak
指针,haveOld
则表示将已有的weak
指针置为nil
,或将指针重定向。
由于storeWeak
函数中的代码量较大,所以只保留了核心代码,不重要的代码都删掉了,看的也清楚点。如果是通过objc_initWeak
调用进来,下面template
的三个bool
参数定义,分别如下。
-
haveOld
=false
-
haveNew
=true
-
crashIfDeallocating
=true
template <HaveOld haveOld, HaveNew haveNew,
CrashIfDeallocating crashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
{
if (!haveNew) assert(newObj == nil);
Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable;
SideTable *newTable;
retry:
// 判断location是否已经指向weak对象,有的话先把旧对象取出来
if (haveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
// 将新传入的对象,对应的weak表取出来,如果这个对象之前被weak指针指向过,则返回有值
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
// 如果有旧对象,但是不是location所指向的,也就是当前传入的对象。则表示从retry执行到这里,可能被其他线程改过,所以需要重新执行retry
if (haveOld && *location != oldObj) {
goto retry;
}
// 有新对象,则判断类是否初始化,没有初始化则先初始化,再重新执行retry
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
_class_initialize(_class_getNonMetaClass(cls, (id)newObj));
previouslyInitializedClass = cls;
goto retry;
}
}
// 如果location指针指向过其他对象,则执行weak_unregister_no_lock,将weak表和旧对象解绑
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// 将传入的新对象,以及新对象的指针地址,添加到weak表中
if (haveNew) {
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating);
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}
*location = (id)newObj;
}
return (id)newObj;
}
在storeWeak
函数中会进行判断,如果是haveNew
则表示创建一个weak
指针,这时会判断被指向的对象是否第一次传入,如果是则会创建新的内存空间,如果是第二次被weak
指向则取出之前已经创建的weak_table
。当释放weak
指针时也是如此,先取出SideTable
对象再从weak_table
中移除对应的weak
指针。
随后会进入一个判断,会判断弱引用对象所属的类对象是否未执行+initialized
方法,如果未执行则调用下面的代码初始化类对象。以保证在弱引用对象和+initialized
方法之间,不会产生死锁。
最后会进入具体的实现中,这些实现代码都在objc-weak.mm
中。移除weak
引用或指针重定向会通过weak_unregister_no_lock
函数实现,添加新的weak
引用则会通过weak_register_no_lock
函数实现。
weak_register_no_lock
weak_register_no_lock
函数是添加weak
引用的关键,在函数中会有是否可以进行weak
操作的判断,如果不符合条件则return
或抛出异常。需要注意的是,在NSObject.mm
文件中提供了allowsWeakReference
方法,可以通过此方法返回是否允许使用weak
引用,在下面的代码中也会向此方法发送消息来做确认。
weak_register_no_lock
函数传入的四个参数如下。
-
weak_table
,全局的weak
哈希表 -
referent_id
,weak
指针。 -
*referrer_id
,weak
指针地址。 -
crashIfDeallocating
,如果weak
指向的地址正在释放中,是否crash
。
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;
// 判断传入的对象是否为空,以及是否tagged pointer这些边界判断
if (!referent || referent->isTaggedPointer()) return referent_id;
bool deallocating;
if (!referent->ISA()->hasCustomRR()) {
deallocating = referent->rootIsDeallocating();
}
else {
// 有自定义的retain实现,执行自定义方法
BOOL (*allowsWeakReference)(objc_object *, SEL) =
(BOOL(*)(objc_object *, SEL))
object_getMethodImplementation((id)referent,
SEL_allowsWeakReference);
if ((IMP)allowsWeakReference == _objc_msgForward) {
return nil;
}
deallocating =
! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
}
if (deallocating) {
if (crashIfDeallocating) {
_objc_fatal("error");
} else {
return nil;
}
}
// 判断堆区内存是否有被weak引用过,如果有的话就加入到已有的weak_entry_t中
weak_entry_t *entry;
if ((entry = weak_entry_for_referent(weak_table, referent))) {
append_referrer(entry, referrer);
}
else {
// 如果没有,则创建一个weak_entry_t,并插入到全局的weak_table哈希表中
weak_entry_t new_entry(referent, referrer);
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);
}
return referent_id;
}
在函数中通过referent
获取到被weak
引用的对象,通过referrer
获取到指针对象,随后会将这两个变量包装成weak_entry_t
结构体,并调用对应的函数添加到哈希表中。
执行添加操作的有两个关键函数,当对象第一次有weak
指针指向时,会调用weak_entry_insert
将weak_entry_t
插入到哈希表中,后面的weak
指向都会调用append_referrer
函数向表中添加referrer
。
weak_entry_for_referent
weak_entry_for_referent
函数用来查找referent
,weak
指向的堆区地址,也就是objc_object
对象的地址。随后通过hash_pointer
函数找到referent
所在的index
,也就是哈希表的key
。在while
循环中遍历weak_entries
表,从index
开始查找referent
所在的位置,一般很快就能找到,并返回。
static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
ASSERT(referent);
weak_entry_t *weak_entries = weak_table->weak_entries;
if (!weak_entries) return nil;
// 通过哈希算法获取到referent的index,也就是所在的下标
size_t begin = hash_pointer(referent) & weak_table->mask;
size_t index = begin;
size_t hash_displacement = 0;
// 循环时从index所在的下标开始,这样查找速度会很快
while (weak_table->weak_entries[index].referent != referent) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_table->weak_entries);
hash_displacement++;
if (hash_displacement > weak_table->max_hash_displacement) {
return nil;
}
}
return &weak_table->weak_entries[index];
}
append_referrer
此函数是对weak_entry_t
进行插入,向数组后面拼接指向指针的地址。内部会根据哈希表大小,决定用动态数组还是定长数组,并且会根据使用情况,对动态数组进行扩容。并最终插入到weak_entry_t
的末尾。
static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
// 如果还未开启动态长度数组,则进入这里
if (! entry->out_of_line()) {
// 先尝试定长数组,如果有空位置就插入
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i] == nil) {
entry->inline_referrers[i] = new_referrer;
return;
}
}
// 如果定长数组存满了,就创建动态数组,并将定长数组的对象插入进来
weak_referrer_t *new_referrers = (weak_referrer_t *)
calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
new_referrers[i] = entry->inline_referrers[i];
}
entry->referrers = new_referrers;
entry->num_refs = WEAK_INLINE_COUNT;
entry->out_of_line_ness = REFERRERS_OUT_OF_LINE;
entry->mask = WEAK_INLINE_COUNT-1;
entry->max_hash_displacement = 0;
}
ASSERT(entry->out_of_line());
// 判断weak_entry_t已经使用的大小,是否超过了已经开辟的3/4,如果超过则进行动态扩容
if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
return grow_refs_and_insert(entry, new_referrer);
}
// 通过哈希算法查找当前的index
size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
size_t index = begin;
size_t hash_displacement = 0;
// 通过while循环,找到referrers数组的最新一个为空的位置
while (entry->referrers[index] != nil) {
hash_displacement++;
// 由于扩容是成倍扩容的,所以mask的值一定是,0x111, 0x1111, 0x11111
index = (index+1) & entry->mask;
if (index == begin) bad_weak_table(entry);
}
if (hash_displacement > entry->max_hash_displacement) {
entry->max_hash_displacement = hash_displacement;
}
// 将new_referrer插入到新的空位置,并将count加一
weak_referrer_t &ref = entry->referrers[index];
ref = new_referrer;
entry->num_refs++;
}
weak_entry_insert
在weak_table_t
中定义了weak_entries
结构体变量,这是一个数组结构,弱引用对象都保存在这里。当插入一个weak_entry_t
时会遍历数组,并找到合适的位置将其插入。
static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
{
weak_entry_t *weak_entries = weak_table->weak_entries;
assert(weak_entries != nil);
size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (weak_entries[index].referent != nil) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_entries);
hash_displacement++;
}
weak_entries[index] = *new_entry;
weak_table->num_entries++;
if (hash_displacement > weak_table->max_hash_displacement) {
weak_table->max_hash_displacement = hash_displacement;
}
}
指针重定向
有时候会把weak
指针指向一个新的对象,这时候会涉及到指针重定向。新建weak
指针会传入newObj
并调用对应的函数,weak
指针作用域销毁会传入oldObj
并调用对应的函数,而指针重定向则这两个参数都有值,haveNew
和haveOld
也都是有值的。
指针重定向会在一次storeWeak
函数调用中处理,并按顺序先调用weak_unregister_no_lock
函数,先将weak
指针从哈希表中移除,再调用weak_register_no_lock
函数添加到新的SideTable
中。
dealloc
rootDealloc
当对象释放时,会调用dealloc
方法,并调用到rootDealloc
来实现释放逻辑。在释放时会判断,如果没有弱引用等逻辑,可以直接调用free
函数释放,否则调用object_dispose
函数处理释放逻辑。
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return;
// 如果符合下面条件,则直接调用free函数释放内存
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);
}
}
objc_destructInstance
object_dispose
函数中调用的objc_destructInstance
函数,在objc_destructInstance
函数中,执行了一些释放和收尾的工作。而weak
的释放操作,就在clearDeallocating
中完成的。
void *objc_destructInstance(id obj)
{
if (obj) {
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
clearDeallocating
在clearDeallocating
函数中,会判断是否开启指针优化,如果未开启则执行sidetable
的释放逻辑。如果开启了指针优化,并且有weak
指针,或引用计数的逻辑,则执行clearDeallocating_slow
函数。
inline void
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
clearDeallocating_slow();
}
assert(!sidetable_present());
}
在clearDeallocating_slow
函数内部,实际上通过weak_clear_no_lock
函数对weak
指针释放逻辑进行的实现。
weak_clear_no_lock
weak
最核心的功能,即对象释放时,将weak
指针指向nil
。这个逻辑就在weak_clear_no_lock
函数中实现的。
void
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id;
// 根据被释放的对象referent_id,从weak_table中找到weak_entry_t结构体
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
return;
}
weak_referrer_t *referrers;
size_t count;
// 判断用的是定长数组,还是动态数组,并将数组赋值给referrers指针
if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
// 遍历weak数组,并将指向weak堆区地址的指针,都置为nil
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
if (*referrer == referent) {
*referrer = nil;
}
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
// 遍历结束后,从weak_table哈希表中,将weak_entry_t移除
weak_entry_remove(weak_table, entry);
}
释放优化
objc_destroyWeak
当weak
作为一个局部变量出现时,编译器会对weak
进行优化。创建对象后,会通过objc_initWeak
的方式将weak
指针添加到哈希表中。在作用域结束时,会通过objc_destroyWeak
将weak
直接释放掉。
当weak
指针作用域消失时,系统会调用objc_destroyWeak
函数来处理,并在函数内部调用storeWeak
函数。随后storeWeak
函数中会传入haveOld
,表示是执行移除weak
的操作。
void objc_destroyWeak(id *location)
{
(void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
(location, nil);
}
weak_unregister_no_lock
weak_unregister_no_lock
函数是在storeWeak
函数中调用的,当weak
指针第一次指向或重新指向时,都会调用storeWeak
函数。如果重新指向,会调用weak_unregister_no_lock
先将之前的移除掉。
函数会通过referent
获取到被weak
引用的对象,通过referrer
获取到指针对象。随后会通过weak_entry_for_referent
函数查找被指向的对象是否有weak_entry_t
,如果找到则进入if
语句中。
在if
语句中会调用remove_referrer
函数,将被weak
指针从weak_entry_t
中移除,并将weak
指针置nil
,这步就是最关键的一步了。随后会判断,如果被指向的对象,所有weak
指针都没有了,则将对象从weak_table
的哈希表中移除。
void
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;
weak_entry_t *entry;
if (!referent) return;
// 从weak_table中获取weak_entry_t对象,如果有的话则进入if语句
if ((entry = weak_entry_for_referent(weak_table, referent))) {
// 从weak_entry_t中删除referrer引用
remove_referrer(entry, referrer);
// 判断删除后,weak_entry_t是否还有弱引用指针指向这块内存地址
bool empty = true;
if (entry->out_of_line() && entry->num_refs != 0) {
empty = false;
}
else {
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i]) {
empty = false;
break;
}
}
}
// 如果为空,则将weak_entry_t从全局weak_table哈希表中删除
if (empty) {
weak_entry_remove(weak_table, entry);
}
}
}
remove_referrer
这是weak
实现的关键函数,和向weak_entry_t
中添加一样,只是这个过程是逆向的。会先遍历inline_referrers
数组中有没有指针对象,如果有则将指针置为nil
,否则就查找referrers
数组。
static void remove_referrer(weak_entry_t *entry, objc_object **old_referrer)
{
if (!entry->out_of_line()) {
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i] == old_referrer) {
entry->inline_referrers[i] = nil;
return;
}
}
}
size_t begin = w_hash_pointer(old_referrer) & (entry->mask);
size_t index = begin;
while (entry->referrers[index] != old_referrer) {
index = (index+1) & entry->mask;
if (index == begin) bad_weak_table(entry);
}
entry->referrers[index] = nil;
entry->num_refs--;
}