1. weak对象的实现函数
首先,看个例子(取自《Objective-C高级编程 iOS与OS X多线程和内存管理》的第一部分):
id __weak weakObj = [[NSObject alloc] init];
其模拟代码为
id obj;
// 创建临时对象tmp
id tmp = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(tmp, @selector(init));
// 使用tmp创建weak对象obj
objc_initWeak(&obj, tmp);
// 作用域结束,tmp释放
objc_release(tmp);
// 释放weak对象obj
objc_destroyWeak(&obj);
通过代码可以看到,weak指针(weak对象)的创建及释放,实际上是通过objc_initWeak() 和 objc_destroyWeak() 函数实现的**。现在,就需要查看一下iOS的运行时系统是如何实现weak的功能的。
注:源代码采用objc.723版本
objc_initWeak的实现:
id
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
objc_destroyWeak的实现:
void
objc_destroyWeak(id *location)
{
(void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
(location, nil);
}
补充一下,修改weak对象的函数objc_storeWeak的实现:
id
objc_storeWeak(id *location, id newObj)
{
return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object *)newObj);
}
可以看出,weak的相关功能,都是通过storeWeak函数进行实现的。通过不同的参数及条件设置实现weak对象的创建、修改和删除的**。
2. storeWeak的实现
还是不废话,直接贴出实现源码(删除了部分无关代码):
/**
更新weak变量。
HaveOld为true时,即weak变量本身已经指向一个即将被清空的对象。这个值可以是nil。
HaveNew为true时,即有个新对象需要赋值给weak指针。这个值可以为nil。
CrashIfDeallocating为true时,如果newObj正在释放、或者newObj所属的类不支持弱引用,则进程就要中断(崩溃);如果CrashIfDeallocating为false,则意味着将nil存储到变量中。
@param location __weak指针地址
@param newObj 真正指向的对象
*/
template <HaveOld haveOld, HaveNew haveNew,
CrashIfDeallocating crashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
{
assert(haveOld || haveNew); // 至少一个为true
if (!haveNew) assert(newObj == nil); // 无新值时,newObj必须为nil
id oldObj;
// SideTable即为内存管理的数据结构。内部包含:引用计数表、弱引用表、自旋锁及相关方法。
SideTable *oldTable; // 旧表(SideTable结构体指针)
SideTable *newTable; // 新表(SideTable结构体指针)
// Acquire locks for old and new values.
// Order by lock address to prevent lock ordering problems.
if (haveOld) {
// 若存在旧值
// 从weak地址中取出旧对象
oldObj = *location;
// 使用旧对象获取到旧表实例
oldTable = &SideTables()[oldObj];
} else {
// 若不存在旧值
// 旧表直接置为nil
oldTable = nil;
}
if (haveNew) {
// 若存在新值
// 使用新值初始化得到新表实例
newTable = &SideTables()[newObj];
} else {
// 若不存在新值
// 新表直接置为nil
newTable = nil;
}
// 根据haveOld和haveNew状态,给两张SideTable表进行对应加锁(自旋锁)
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
...
// Clean up old value, if any.
if (haveOld) {
// 若存在旧值
// 在旧表中的weak表中清除相关信息(旧对象和weak指针的关联)
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// Assign new value, if any.
if (haveNew) {
// 若存在新值
// 在新表的weak表中存储相关信息(使用newObj的地址作为key,location地址,即weak指针作为value)
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected
// 如果被打断(如此时正在释放对象),则newObj就为nil了【注:crash的已经在上一步中进行处理了,这里是不crash的情况】
// Set is-weakly-referenced bit in refcount table.
if (newObj && !newObj->isTaggedPointer()) {
// 若已经绑定成功,且newObj不是taggedPointer
// 将newObj的对应位标记为weak
newObj->setWeaklyReferenced_nolock();
}
// Do not set *location anywhere else. That would introduce a race.
// 确保weak指针指向newObj
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
// 若没有新值,原存储信息不变
}
// 解锁相关表
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
// 返回newObj对象
return (id)newObj;
}
在以上代码中可以看到,我们所有的操作(注册weak、移除weak等)都是发生在SideTable实例中的。且在操作时需要在有锁的条件下执行。我们看一下SideTable到底是何方神圣:
struct SideTable {
/** 自旋锁 */
spinlock_t slock;
/** 引用计数表(散列表) */
RefcountMap refcnts;
/** weak表(内部使用数组或二级表实现) */
weak_table_t weak_table;
...
}
可以看到,SideTable即为内存管理的精髓。其内部包含的就是引用计数表和weak弱引用表。
SideTable是全局对象(不支持析构,会crash),是由分离锁进行管理的StripedMap对象:
// 获取oldTable对象(根据oldObj对象)
oldTable = &SideTables()[oldObj];
其中,SideTables函数的实现为:
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
在StripedMap模板类中,我们可以找到获取SideTable的相关实现:
template<typename T>
class StripedMap {
enum { CacheLineSize = 64 };
#if TARGET_OS_EMBEDDED
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<uintptr_t>(p);
// hash算法(64位系统下得到的是0~63)
return ((addr >> 4) ^ (addr >> 9)) % Stripe Count;
}
public:
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}
...
}
其中,public方法中的实现即说明了一切:通过对象地址得到的hash值,取出数组中保存的值(这里即为SideTable对象)。且通过hash算法还可知道,一个StripedMap对象在64位系统下包含64个子单元。可以理解为一个分离锁管理着64个对象的SideTable对象。
由于本篇学习的是weak的实现,故我们暂时只讨论weak_table_t这个结构(这个还凑合,引用计数还根本不懂...)。
3. weak弱引用表
首先看一下weak_table_t的数据结构:
/**
* 全局弱引用表。
* weak_entry_t内部使用原始对象作为key,
* weak指针的地址的数组作为value
*/
struct weak_table_t {
/** 弱引用键值数据数组的起始地址 */
weak_entry_t *weak_entries;
/** weak_entry_t数组的元素个数 */
size_t num_entries;
/** weak_entry_t数组的大小 */
uintptr_t mask;
/** hash查找最大偏移量 */
uintptr_t max_hash_displacement;
};
weak_table_t内部保存着由week_entry_t实例组成的数组。
内部的weak_entry_t即为真正的键值对:key为引用的对象,value为weak指针组成的数组。
// 定义的结构体内部对于weak指针个数的储存上线
#define WEAK_INLINE_COUNT 4
// out_of_line_ness成员占用了inline_referrers数组index为1空间中的低2位。
// inline_referrers[1]是一个指针对齐的DisguisedPtr的对象。
// 一个指针对齐的DisguisedPtr的最低两位永远是0b00。
// 因此,在out_of_line_ness == 0b10,就被用于标记out-of-line动态数组的状态。
#define REFERRERS_OUT_OF_LINE 2
struct weak_entry_t {
/** 指向的对象 */
DisguisedPtr<objc_object> referent;
union {
struct {
/** 存储weak指针的动态数组的起始地址 */
weak_referrer_t *referrers;
/** 标识是否使用了动态数组:占2位 */
uintptr_t out_of_line_ness : 2;
/** 已存的weak指针个数:占62位 */
uintptr_t num_refs : PTR_MINUS_2;
/** 动态数组的总大小 */
uintptr_t mask;
/** hash查找最大偏移量 */
uintptr_t max_hash_displacement;
}; // 五个成员,共32字节
struct {
// 结构体中自带的可以存储4个weak指针的静态数组
// 注:out_of_line_ness域使用了索引为1的位置的最低2位
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
}; // 32字节
};
/** 判定是否使用了动态数组存储weak指针 */
bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}
weak_entry_t& operator=(const weak_entry_t& other) {
memcpy(this, &other, sizeof(other));
return *this;
}
/** 创建新的weak键值对实例(以前没有) */
weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
: referent(newReferent)
{
// 使用静态数组存储weak指针
inline_referrers[0] = newReferrer;
// 将数组的其他位置数据置为nil
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}
};
weak_entry_t结构体固定为40字节大小。
其中,为了快速访问weak指针,weak_entry_t中直接设置了一个包含4个元素的静态数组。当指向对象的weak指针超过上限时,就使用动态数组进行存储(将静态数组中的weak指针copy到动态数组中,之后再存入新的weak指针)。
weak_entry_t提供给外部用于识别自身正在使用哪种存储数组的函数。
有了以上这些知识,我们就可以解释SideTable中的weak表是如何注册weak对象并如何清除weak对象的过程了。
4. 在weak引用表中注册weak对象
/**
* 如果不存在,注册一个新的(对象,弱指针)键值对(entry对象)。
*
* @param weak_table 全局弱引用表
* @param referent 弱指针要指向的对象
* @param referrer 弱指针的地址
*/
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;
// weak指针
objc_object **referrer = (objc_object **)referrer_id;
// 原对象为nil,或者原对象是taggedPointer,直接返回原对象
if (!referent || referent->isTaggedPointer()) return referent_id;
// ensure that the referenced object is viable
bool deallocating;
if (!referent->ISA()->hasCustomRR()) {
// 若原始对象的类,没有实现内存管理的相关方法
// 直接以SideTable的释放状态作为标识
deallocating = referent->rootIsDeallocating();
}
else {
// 若原始对象的类,实现了内存管理的相关方法
BOOL (*allowsWeakReference)(objc_object *, SEL) =
(BOOL(*)(objc_object *, SEL))
object_getMethodImplementation((id)referent,
SEL_allowsWeakReference);
if ((IMP)allowsWeakReference == _objc_msgForward) {
// 若allowsWeakReference的实现为消息转发函数,则证明调研对象不支持weak,直接返回nil
return nil;
}
// 若allowsWeakReference的实现正常,执行此IMP,返回值取反作为释放的标识(即:自定义为不支持weak,则认为正在释放)
deallocating =
! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
}
if (deallocating) {
// 根据释放标识,进行处理
if (crashIfDeallocating) {
// crash
_objc_fatal("Cannot form weak reference to instance (%p) of "
"class %s. It is possible that this object was "
"over-released, or is in the process of deallocation.",
(void*)referent, object_getClassName((id)referent));
} else {
// 返回nil
return nil;
}
}
// now remember it and where it is being stored
weak_entry_t *entry;
if ((entry = weak_entry_for_referent(weak_table, referent))) {
// 在weak表中,使用原始对象获取生成入口信息,若存在(证明原始对象子表中已经存储了其他weak指针了)
// 将weak指针插入到子表中
append_referrer(entry, referrer);
}
else {
// 没有entry对象
// 使用原始对象创建新的entry对象(绑定好key和value)
weak_entry_t new_entry(referent, referrer);
// 检查是否需要扩充weak表(扩充后,原始数据均插入了新weak表中)
weak_grow_maybe(weak_table);
// 将新的入口信息插入到weak表中
weak_entry_insert(weak_table, &new_entry);
}
// 这里不要修改referrer的指向(也就是weak指针的指向不能变)
// 返回原对象
return referent_id;
}
其中,通过原始对象,在weak表中查找对应entry的过程如下:
/**
* 根据给定的对象,返回弱引用表中对应的entry。
* 如果没有对应的entry,返回NULL。
* 执行了一次循环查找。
*
* @param weak_table
* @param referent The object. Must not be nil.
*
* @return The table of weak referrers to this object.
*/
static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
assert(referent);
// 从表中取出weak_entry_t指针(weak数据表的起始地址)
weak_entry_t *weak_entries = weak_table->weak_entries;
if (!weak_entries) return nil;
// 根据对象地址的hash与weak表的mask与运算得到遍历的起始index
size_t begin = hash_pointer(referent) & weak_table->mask;
size_t index = begin;
size_t hash_displacement = 0;
// 依次查看weak_entry_t对象中的referent是否为指向的对象
while (weak_table->weak_entries[index].referent != referent) {
// 不是,索引后移
index = (index+1) & weak_table->mask;
// 索引又变为begin了,直接crash(hash冲突且没有空余位置了)
if (index == begin) bad_weak_table(weak_table->weak_entries);
hash_displacement++;
if (hash_displacement > weak_table->max_hash_displacement) {
// 超出最大偏移范围,返回
return nil;
}
}
// 此时,通过index取到weak_entry_t对象,返回地址
return &weak_table->weak_entries[index];
}
注:
在查找遍历索引begin时,系统使用的方式是通过对象的hash值和weak_table的mask值进行按位与运算。
其中mask是num_entries-1,即如果此弱引用表的weak_entries个数是4,mask即为3,也就是0b11。
对象地址hash之后,通过mask进行按位与运算得到的,就只有mask值对应的范围,即0b000b11,故index取值范围是03。系统巧妙地将对象地址转化为索引,在数组中查找对应位置的数据。
后面使用while循环的原因是,由于hash值可能会重复,得到的index位置可能已经存在其他数据,故对index进行偏移处理,待新位置的元素符合要求,再进行操作。其中hash_displacement就是标记偏移量的。如果超过最大偏移量max_hash_displacement,则数组中没有符合要求的位置索引。
向已经存在的weak_entry_t中插入新的weak指针,操作如下:
static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
if (! entry->out_of_line()) {
// 若使用的内部数组存储weak指针,则直接遍历,将空的赋值为weak指针地址
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i] == nil) {
entry->inline_referrers[i] = new_referrer;
return;
}
}
// 内部数组已满,此时需要使用动态数组存储(先初始化4个地址空间)
weak_referrer_t *new_referrers = (weak_referrer_t *)
calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));
// 将内部数组的四个weak指针copy到动态数组中
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());
// 占用空间超过 3/4,扩容并插入新weak指针
if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
return grow_refs_and_insert(entry, new_referrer);
}
// 空间正常,可以插入
size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
size_t index = begin;
size_t hash_displacement = 0;
// 遍历找到待插入的索引位置
while (entry->referrers[index] != nil) {
hash_displacement++;
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;
}
// 取出数组对应位置的地址,赋值weak指针
weak_referrer_t &ref = entry->referrers[index];
ref = new_referrer;
// 计数+1
entry->num_refs++;
}
而对于weak_table_t弱引用表中,没有引用对象指向的weak_entry_t对象时,直接创建新entry,将weak指针写入后,直接将此entry加入到弱引用表中。此过程与查询entry的过程非常相似:
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;
}
}
5. 在weak弱引用表中移除weak对象
相对的,当指向的对象将要释放时,运行时系统会将注册到weak表中的原始对象相关的所有weak指针信息移除。也就实现了weak自动置为nil的功能。
void
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id)
{
// 指向的对象
objc_object *referent = (objc_object *)referent_id;
// weak指针的地址
objc_object **referrer = (objc_object **)referrer_id;
weak_entry_t *entry;
// 指向对象不存在,直接返回(用指向对象的地址作为key进行保存)
if (!referent) return;
if ((entry = weak_entry_for_referent(weak_table, referent))) {
// 使用指向的对象在weak表中查找,获取entry,如果存在
// 在entry中移除weak指针的地址存储
remove_referrer(entry, referrer);
// 查看入口信息子表中是否还有其他weak指针存储
bool empty = true;
if (entry->out_of_line() && entry->num_refs != 0) {
// 使用了动态数组存储weak指针,且num_refs不为0,证明还有
empty = false;
}
else {
// 内部数组存储,只要数组不为空,就还有
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i]) {
empty = false;
break;
}
}
}
if (empty) {
// 若子表中没有其他的weak指针地址存储了
// 在weak表中移除entry信息
weak_entry_remove(weak_table, entry);
}
}
// 这里不要清除weak指针的地址,objc_storeWeak函数不允许他人更改
}
整体过程与注册weak指针的流程非常相似。查询到weak_entry_t对象后,移除内部的weak指针,最后清除此entry对象即可。
移除entry中指定的weak指针的过程如下(与插入weak指针非常相似):
static void remove_referrer(weak_entry_t *entry, objc_object **old_referrer)
{
if (! entry->out_of_line()) {
// 没有使用外部动态数组,即weak直接存储在内部静态数组中
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
// 查找到weak指针所在的对象
if (entry->inline_referrers[i] == old_referrer) {
// 置为nil,完成清除(weak指针自动置nil的功能)
entry->inline_referrers[i] = nil;
return;
}
}
// 未找到,出现内部错误
_objc_inform("Attempted to unregister unknown __weak variable "
"at %p. This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
old_referrer);
objc_weak_error();
return;
}
// weak指针存储超过了4个,使用了外部的动态数组
// 得到遍历的起始索引
size_t begin = w_hash_pointer(old_referrer) & (entry->mask);
size_t index = begin;
size_t hash_displacement = 0;
// 在动态数组中依次查看,直到找到weak指针存储的位置
while (entry->referrers[index] != old_referrer) {
index = (index+1) & entry->mask;
if (index == begin) bad_weak_table(entry); // 出错crash
hash_displacement++;
if (hash_displacement > entry->max_hash_displacement) {
_objc_inform("Attempted to unregister unknown __weak variable "
"at %p. This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
old_referrer);
objc_weak_error();
return;
}
}
// 此时index所在位置即为weak指针存储的位置,置为nil清除
entry->referrers[index] = nil;
// 将索引个数-1
entry->num_refs--;
}
在weak_table_t中移除weak_entry_t对象就比较简单了:
static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry)
{
// 使用了动态数组存储weak指针,则直接从起始地址进行释放即完成清除(动态数组通过calloc申请的连续地址空间)
if (entry->out_of_line()) free(entry->referrers);
// 将entry的整个存储空间清零
bzero(entry, sizeof(*entry));
// 整体weak表的num_entries计数 - 1
weak_table->num_entries--;
// 检查缩小weak表的容量
weak_compact_maybe(weak_table);
}
这里,weak_compact_maybe函数会检查weak表中entry的存储占用率,根据实际情况释放相应空间。
不仅如此,weak_grow_maybe函数也如此,会根据占用率动态扩大存储空间。其内部实现都是通过weak_resize函数实现的。
#define TABLE_SIZE(entry) (entry->mask ? entry->mask + 1 : 0)
static void weak_resize(weak_table_t *weak_table, size_t new_size)
{
size_t old_size = TABLE_SIZE(weak_table);
weak_entry_t *old_entries = weak_table->weak_entries;
// 分配内存,new_size * sizeof(weak_entry_t) 个字节(new_size * 40)
weak_entry_t *new_entries = (weak_entry_t *)
calloc(new_size, sizeof(weak_entry_t));
weak_table->mask = new_size - 1; // 记录下尺寸
weak_table->weak_entries = new_entries; // 数组起始地址
weak_table->max_hash_displacement = 0;
weak_table->num_entries = 0; // restored by weak_entry_insert below
// 将原始数据copy到新weak表中
if (old_entries) {
weak_entry_t *entry;
weak_entry_t *end = old_entries + old_size;
for (entry = old_entries; entry < end; entry++) {
if (entry->referent) {
// 将原始数据插入到weak表中
weak_entry_insert(weak_table, entry);
}
}
free(old_entries);
}
}
这里,我们就知道了,weak_table_t中的weak_entries成员实际上是weak_entry_t组成的数组(calloc创建的连续内存空间)。
6. 总结
至此,我们基本了解了创建和释放一个weak弱指针对象所需要做的主要工作。也明白了weak指针在原对象释放后自动置为nil的实现方式。
此外,我们还了解了:
- 苹果对于将hash值转化为数组索引index的转换方式及冲突后的索引偏移的操作。
- 对于weak_entry_t数据结构中,存储weak指针的两种方式(快速量少用静态数组,量大使用动态数组的可伸缩方式)的分段式存储思想。
网友评论