循环引证原因
在方针图中常常会出现一种情况,便是几个方针都以某种办法相互引证,然后构成“环”(cycle),一起由于 iOS 中选用引证计数内存办理模型,所以这种情况通常会导致内存走漏,由于最终没有其他东西会引证环中的方针。这样的话,环里的方针就无法为外界所拜访,但方针之间尚有引证,这些引证使得他们都能持续存活下去,而不会为系统收回。例如:
class Teacher {
var student: Student?
}
class Student {
var teacher: Teacher?
}
let teacher = Teacher()
let student = Student()
teacher.student = student
student.teacher = teacher
从上面代码能够看出,teacher
与 student
相互持有,构成保存环,假如 teacher
和 student
方针无法被外界拜访到,整个保存环就走漏了。但假如还能被外界所拜访,外界还能手动破除“环”以防止循环引证,比如 student.teacher = nil
。
在垃圾收回机制中,若选用 Root Tracing 算法,就能检测到这些保存环,然后收回掉这些方针。它经过一系列名为 “GCRoots” 的方针作为起始点,从这个节点向下搜索,搜索走过的路径称为 ReferenceChain
,当一个方针与 GCRoots
没有任何 ReferenceChain
相连时,就证明这个方针不可用,能够被收回。
在 iOS 中,供给了 weak
来帮助我们处理循环引证这种内存办理问题,运用 weak
润饰的方针不会持有方针,因而不会使方针的引证计数加 1,一起弱引证指向的方针被抛弃时,弱引证指针会指向 nil
。在上述例子中,例如将 Student
类中的 teacher
加上 weak
所有权润饰符,就能够防止强引证“环”的出现。
完成 weak 的源码
在相似这样运用 __weak id weakObj = object;
弱引证所有权润饰符时,编译器会将其转换成相似 objc_initWeak(&weakObj, 0);
这样的代码。同样的,还有毁掉 weak
指针,objc_destroyWeak
。
以下参阅的源码为 objc4-838。
SideTable 的数据结构
SideTable
中存储了方针的引证计数以及所相关的弱引证指针,它是 SideTables()
这样一个全局哈希表的 value
,其数据结构如下图所示:
关于 SideTables()
,它是 SideTablesMap
的封装函数,其实践类型为 StripedMap<SideTable>
,它是经过 SideTablesMap.get()
获取,而实践的 SideTablesMap
则又被 ExplicitInit
所封装;为什么需求 ExplicitInit
呢?在官方注释能够发现答案:由于 C++ 的静态初始化器在 Runtime 初始化之后,而在 Runtime 初始化时需求用到这个方针,因而需求显式初始化。StripedMap
是一个哈希表,经过方针的地址偏移与 StripeCount
巨细映射到属于方针的 SideTable
的下标,这样能够将方针放在不同的 SideTable
存储,防止一起拜访提升拜访功率。
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;
static StripedMap<SideTable>& SideTables() {
return SideTablesMap.get();
}
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
SideTable
中存储了方针的引证计数和弱引证指针,分别是 refcnts
和 weak_table
,weak_table
本身是一个简易的哈希表,它的 weak_entries
是存储详细方针弱引证指针的数组,size_entries
标明数组的巨细,mask
用于哈希映射,max_hash_displacement
则表明数组中方针的最大哈希抵触次数,选用线性勘探法处理哈希抵触。其详细 key-value
映射逻辑如下:
static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
weak_entry_t *weak_entries = weak_table->weak_entries;
if (!weak_entries) return nil;
size_t begin = hash_pointer(referent) & weak_table->mask;
size_t index = begin;
size_t hash_displacement = 0;
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];
}
在详细的 entry
中,有两种办法存储弱引证指针,弱引证指针总个数 <= 4 选用数组,大于 4 选用哈希表。在选用哈希表的完成中,out_of_line_ness
表明是否超出运用数组的巨细规模(4),num_refs
表明弱引证指针个数,mask
用于哈希映射,max_hash_displacement
表明数组中方针最大哈希抵触的个数。因而其 key-value
的映射逻辑也与 weak_table
的相似:
size_t begin = w_hash_pointer(old_referrer) & (entry->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (entry->referrers[index] != old_referrer) {
index = (index+1) & entry->mask;
if (index == begin) bad_weak_table(entry);
hash_displacement++;
if (hash_displacement > entry->max_hash_displacement) {
objc_weak_error();
return;
}
}
storeWeak
objc_initWeak
与 objc_destroyWeak
相似,最终都指向 storeWeak
办法,仅仅传递参数不同。
objc_initWeak
调用为 storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating> (location, (objc_object*)newObj)
;
objc_destroyWeak
调用为 storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating> (location, nil)
;
HaveOld
与 HaveNew
总是相反的,两者不会一起都有和一起都没有,DontHaveOld, DoHaveNew
表明初始化,DoHaveOld, DontHaveNew
表明毁掉;CrashIfDeallocating
表明假如方针正处于毁掉阶段是否产生 Crash,因而在方针的 dealloc
中不要试图运用 weak
润饰 self
;最终的参数则是弱引证指针和方针地址。
// 简化后代码
template <HaveOld haveOld, HaveNew haveNew,
enum CrashIfDeallocating crashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
{
id oldObj;
SideTable *oldTable;
SideTable *newTable;
retry:
if (haveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
// 依据 table 地址,按巨细进行加锁,防止死锁
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
if (haveOld && *location != oldObj) {
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}
// 清除弱引证指针
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
if (haveNew) {
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
// 设置 isa 指针的 WeaklyReference 位域
if (!_objc_isTaggedPointerOrNil(newObj)) {
newObj->setWeaklyReferenced_nolock();
}
*location = (id)newObj;
}
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
// 假如方针完成了 _setWeaklyReferenced 办法,会调用通知
callSetWeaklyReferenced((id)newObj);
return (id)newObj;
}
在上述代码中,首先会获取到相应的 SideTable
,在进行加锁时按 SideTable
的地址巨细顺序进行桎梏,这能够防止死锁,之后进行 SideTable
的整理或者增加,最终设置 isa 指针的 weakly_referenced
位域。
在进行整理和增加时,会按必定逻辑进行紧缩和扩容:
在清除时,weak_table
可能会紧缩,巨细大于 1024 且容量不满 1/16,紧缩 weak_table
到本来的 1/8:
static void weak_compact_maybe(weak_table_t *weak_table)
{
size_t old_size = TABLE_SIZE(weak_table);
// 巨细大于 1024 且容量不满 1/16,紧缩 weak_table
if (old_size >= 1024 && old_size / 16 >= weak_table->num_entries) {
weak_resize(weak_table, old_size / 8);
}
}
在增加时,假如 entry
的巨细超越 4,会转换成哈希表,假如容量占满 3/4,会进行扩容到本来的两倍:
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;
}
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_referrer_t &ref = entry->referrers[index];
ref = new_referrer;
entry->num_refs++;
}
假如 weak_table
容量占满 3/4,会进行扩容到本来的两倍:
static void weak_grow_maybe(weak_table_t *weak_table)
{
size_t old_size = TABLE_SIZE(weak_table);
if (weak_table->num_entries >= old_size * 3 / 4) {
weak_resize(weak_table, old_size ? old_size*2 : 64);
}
}
weak_clear_no_lock
在方针被毁掉时,会进行方针的所有弱引证指针的整理,它由 dealloc
调用:
// 简化后代码
void
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id;
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
weak_referrer_t *referrers;
size_t count;
if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
if (*referrer == referent) {
*referrer = nil;
}
}
}
weak_entry_remove(weak_table, entry);
}
NSTimer 的循环引证问题
在运用 NSTimer
时,可能会产生循环引证问题。例如,我们常常这样运用:
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerTriggered:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)timerTriggered:(NSTimer *)timer {
NSLog(@"timerTriggered");
}
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s", __func__);
}
@end
上述代码会产生循环引证,这是由于 timer
会保存其方针方针,比及自身“失效”时再开释此方针。调用 invalidate
办法可令计时器失效;履行完相关任务后,一次性定时器也会失效。假如是重复性定时器,那有必要自己调用 invalidate
办法,才能令其停止。在 vc
中强持有一份 timer
,一起由于这是一个重复性定时器,NSTimer
一向不会失效,也会一向强持有 vc
,这产生了循环引证。当页面退出时,vc
和 timer
没有被外界方针引证 ,这会导致内存走漏。
它的方针联系图如下:
下面给出一些常见的处理方案:
- 提早调用
invalidate
办法
假如能提早知道什么时候 timer
不需求了,能够提早调用 invalidate
办法,例如上例中能够在回来按钮被点击时调用 invalidate
,这就使得 RunLoop
不会持续持有 timer
,timer
因而失效,也不会强持有方针方针(vc
),使得“环”被破除。但这种办法存在很大局限性,需求清晰知道什么时候必定能够调用 invalidate
办法。
- 运用
block
办法运用timer
在 iOS10 以上,新增了选用 block
办法运用 timer
,这防止了以 target-action 办法强持有方针方针,只需处理对 block
的循环引证即可:
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
__strong typeof(weakSelf) strongSelf = weakSelf;
NSLog(@"timerTriggered");
// ...
}];
- 选用
NSProxy
或者中心方针进行音讯转发
使方针方针从 vc
转换成其他方针,如 NSProxy
,在 NSProxy
内部将音讯转发到 vc
,也可使得“环”被打破,例如:
@implementation LBWeakProxy
+ (instancetype)proxyWithTarget:(id)target {
LBWeakProxy *proxy = [LBWeakProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
if ([self.target respondsToSelector:sel]) {
NSMethodSignature *signature = [self.target methodSignatureForSelector:sel];
return signature;
}
return [super methodSignatureForSelector:sel];
}
-(void)forwardInvocation:(NSInvocation *)invocation {
SEL aSelector = invocation.selector;
if ([self.target respondsToSelector:aSelector]) {
invocation.target = self.target;
[invocation invoke];
} else {
[super forwardInvocation:invocation];
}
}
@end
LBWeakProxy *proxy = [LBWeakProxy proxyWithTarget:self];
self.timer = [NSTimer timerWithTimeInterval:1 target:proxy selector:@selector(timerTriggered:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
它的方针联系图如下:
- 运用
GCD
代替NSTimer
能够利用 GCD
完成一个 timer
,例如开源库 MSWeakTimer,它不会保存方针方针,这样只需求在 dealloc
中开释掉 timer 就好。一起 GCD 的 timer 也不会有 RunLoop 的 Mode 切换、子线程创建 timer 的相关问题。
NSTimer Special Considerations
You must send this message from the thread on which the timer was installed. If you send this message from another thread, the input source associated with the timer may not be removed from its run loop, which could prevent the thread from exiting properly.
本文同步发布于公众号(nihao的编程随记)和个人博客nihao,欢迎 follow 以获得更佳观看体验。
[1] Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有用办法
[2] Purpose of class ExplicitInit in objc runtime source code
[3] 如何在iOS中处理循环引证问题
[4] 警觉 NSTimer 引起的循环引证
[5] NSTimer循环引证分析与处理