前言
关于 keywords 的区别,这里主要涉及到引用计数相关知识,对于 ARC 相关的介绍可以参考上一篇面试题:19·iOS 面试题·什么是 ARC ?(ARC 是为了解决什么问题诞生的?) 。
这篇文章我们简单了解下引用计数的实现机制,再分别对比这个几个 keywords 的区别,最后再说明 __block 和 __weak 的应用场景。
引用计数机制
在 Objective-C 中,使用引用计数机制来实现内存管理:每个对象都有与之对应的引用计数值,所以在底层中要维护对象的引用计数值。在 MRC 环境下,是通过调用 retain
、release
、autorelease
这些方法来控制对象的引用计数,而在 ARC 环境下,则是由编译器自动给我们添加这些方法。现在我们来看下这些方法的底层源码,以便更加深层次的了解引用计数机制。(以下代码摘抄自:iOS内存管理机制分析 )
Retain 方法
- (id)retain {
return ((id)self)->rootRetain();
}
inline id objc_object::rootRetain()
{
if (isTaggedPointer()) return (id)this;
return sidetable_retain();
}
id objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];//获取引用计数表
table.lock(); // 加锁
size_t& refcntStorage = table.refcnts[this]; // 根据对象的引用计数
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock(); // 解锁
return (id)this;
}
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
// 省略...
};
从上面源码可以知道 retain 方法的调用过程:retain -> rootRetain -> sidetable_retain,具体操作是在 sidetable_retain
方法中。sidetable_retain
主要是操作 SideTable 中的 RefcountMap(对象引用计数的 Map,这个引用计数的 Map 以对象的地址作为 Key,引用计数值作为 Value)。
到这里,我们可以知道内存中有一张表维护对象的引用计数值。
Release 操作
- (oneway void)release {
((id)self)->rootRelease();
}
inline bool objc_object::rootRelease()
{
if (isTaggedPointer()) return false;
return sidetable_release(true);
}
uintptr_t objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];
bool do_dealloc = false;
table.lock(); // 加锁
RefcountMap::iterator it = table.refcnts.find(this); // 先找到对象的地址
if (it == table.refcnts.end()) {
do_dealloc = true; //引用计数小于阈值,最后执行dealloc
table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (it->second < SIDE_TABLE_DEALLOCATING) {
// SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
do_dealloc = true;
it->second |= SIDE_TABLE_DEALLOCATING;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
it->second -= SIDE_TABLE_RC_ONE; //引用计数减去1
}
table.unlock(); // 解锁
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}
release 操作也是一样的,在 sidetable_release
中,通过操作 SideTable 的 RefcountMap 来将对象的引用计数值 -1,当对象的引用计数值小于阈值则调用 dealloc 方法销毁对象。
strong vs copy
首先 strong 和 copy 都会增加对象的引用计数(强引用),这里需要注意的是用 copy 修饰的变量,必须遵循 NSCopying 协议,不然在赋值的时候会 Crash。这里把 strong 跟 copy 放到一起进行比较,主要想考察在修饰变量的时候如何选择正确的修饰符。
在平时开发的时候,对于 NSString、NSArray、NSDictionary 类型的对象经常使用 copy 来修饰,这里主要是为了避免这种情况:将可变类型对象赋值给不可变类型对象之后,之后再去修改可变类型对象,会导致不可变类型对象也遭到修改。但是,我们往往用不可变类型修饰对象,都是希望不会被修改,这里用 copy 就不会出现这个问题。
对于更加详尽的解释:iOS 声明属性时,到底用 strong 还是用 copy,二者有何区别?,这里就不重复描述了。
PS:但是如果你能确定修改不会出现问题,对于这些对象也可以用 strong 来修饰。
assign vs weak
assign 和 weak 都是不会增加对象的引用计数(弱引用),它们之间的区别是:当对象被销毁时,weak 修饰的变量指针会置为 nil,但是 assign 修饰的变量还是会指向原来的地址(这里就会出现野指针)。
在我们平时开发一般都是用 assign 来修饰基本类型,weak 来修饰对象。
weak 底层实现
首先用 weak 是弱引用,不会增加对象的引用计数,并且在对象释放的时候,weak 修饰的指针会置为 nil,这样子就可以防止野指针。
概括 weak 底层实现:有一张 weak hash 表,维护了指向对象的 weak 指针。对象的地址作为 Key,Value 则是指向该对象的 weak 指针数组(因为可能会出现多个 weak 指针指向同一个对象)。这里看下相关的结构体:
struct SideTable {
// 保证原子操作的自旋锁
spinlock_t slock;
// 引用计数的 hash 表
RefcountMap refcnts;
// weak 引用全局 hash 表
weak_table_t weak_table
}
//RefcountMap 这个是引用计数值的表
//weak_table_t 这个是弱引用的表
struct weak_table_t {
// 保存了所有指向指定对象的 weak 指针
weak_entry_t *weak_entries;
// 存储空间
size_t num_entries;
// 参与判断引用计数辅助量
uintptr_t mask;
// hash key 最大偏移值
uintptr_t max_hash_displacement;
};
typedef objc_object ** weak_referrer_t;
struct weak_entry_t {
DisguisedPtrobjc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line : 1;
uintptr_t num_refs : PTR_MINUS_1;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line=0 is LSB of one of these (don't care which)
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
}
所以 weak 底层也是通过操作 weak_table_t 表来实现相关功能功能,核心步骤如下,具体底层代码可以参阅文末链接:
1、初始化时:runtime 会调用 objc_initWeak 函数,初始化一个新的 weak 指针指向对象的地址。
2、添加引用时:objc_initWeak 函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
3、释放时,调用 clearDeallocating 函数。clearDeallocating 函数首先根据对象地址获取所有 weak 指针地址的数组,然后遍历这个数组把其中的数据设为 nil,最后把这个 entry 从 weak 表中删除,最后清理对象的记录。
__block vs __weak
先来说一下 __weak,通过 __weak 来增加一个弱引用,这里常用来打破循环引用。
对于 __block,主要是为了解决 block 中匿名函数截获变量,产生的生命周期的问题。block 截获外部变量,默认是不可以取修改变量的,但是通过 __block 修饰的变量,在 block 内部可以修改,对于 block 更加详细的介绍以及 __ block 底层的实现可以参阅: 04·iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的?
__block MRC 和 ARC 的区别
__block 修饰在变量在 MRC 环境和 ARC 环境也是有区别的:
- MRC 环境下,block 截获外部用 __block 修饰的变量,不会增加对象的引用计数
- ARC 环境下,block 截获外部用 __block 修饰的变量,会增加对象的引用计数
这里我们可以得知,在 MRC 环境下,可以通过 __block 来打破循环引用,在 ARC 环境下,则需要用 __weak 来打破循环引用。
在 MRC 环境下,block 内部截获 __block 修饰的变量,为什么不会增加对象的引用计数?
将 block 从栈区拷贝到堆区,需要调用 block 的 copy 方法,copy 方法实际上调用的是 Block_object_assign 方法,我们这里看一下底层实现:(这里主要是通过 block 的 flag 来确定是否增加对象的引用计数。)
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void **dest = (const void **)destArg;
switch ((flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_OBJECT:
/*******
id object = ...;
[^{ object; } copy];
********/
_Block_retain_object(object); //object 引用计数+1
*dest = object; //赋值
break;
case BLOCK_FIELD_IS_BLOCK:
/*******
void (^object)(void) = ...;
[^{ object; } copy];
********/
*dest = _Block_copy(object);
break;
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
/*******
// copy the onstack __block container to the heap
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__block ... x;
__weak __block ... x;
[^{ x; } copy];
********/
*dest = _Block_byref_copy(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
/*******
// copy the actual field held in the __block container
// Note this is MRC unretained __block only.
// ARC retained __block is handled by the copy helper directly.
__block id object;
__block void (^object)(void);
[^{ object; } copy];
********/
/// 在mrc的情况下,你对对象添加__block, block是不会对这个对象引用计数+1
*dest = object;
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
/*******
// copy the actual field held in the __block container
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__weak __block id object;
__weak __block void (^object)(void);
[^{ object; } copy];
********/
*dest = object;
break;
default:
break;
}
}
以上。
参考文献
04·iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的?
19·iOS 面试题·什么是 ARC ?(ARC 是为了解决什么问题诞生的?)
网友评论