循环引证原因

在方针图中常常会出现一种情况,便是几个方针都以某种办法相互引证,然后构成“环”(cycle),一起由于 iOS 中选用引证计数内存办理模型,所以这种情况通常会导致内存走漏,由于最终没有其他东西会引证环中的方针。这样的话,环里的方针就无法为外界所拜访,但方针之间尚有引证,这些引证使得他们都能持续存活下去,而不会为系统收回。例如:

class Teacher {
    var student: Student?
}
class Student {
    var teacher: Teacher?
}
let teacher = Teacher()
let student = Student()
teacher.student = student
student.teacher = teacher

从上面代码能够看出,teacherstudent 相互持有,构成保存环,假如 teacherstudent 方针无法被外界拜访到,整个保存环就走漏了。但假如还能被外界所拜访,外界还能手动破除“环”以防止循环引证,比如 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,其数据结构如下图所示:

iOS中的内存管理|weak

关于 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 中存储了方针的引证计数和弱引证指针,分别是 refcntsweak_tableweak_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_initWeakobjc_destroyWeak 相似,最终都指向 storeWeak 办法,仅仅传递参数不同。

objc_initWeak 调用为 storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating> (location, (objc_object*)newObj)

objc_destroyWeak 调用为 storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating> (location, nil)

HaveOldHaveNew 总是相反的,两者不会一起都有和一起都没有,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,这产生了循环引证。当页面退出时,vctimer 没有被外界方针引证 ,这会导致内存走漏。

它的方针联系图如下:

iOS中的内存管理|weak

下面给出一些常见的处理方案:

  • 提早调用 invalidate 办法

假如能提早知道什么时候 timer 不需求了,能够提早调用 invalidate 办法,例如上例中能够在回来按钮被点击时调用 invalidate,这就使得 RunLoop 不会持续持有 timertimer 因而失效,也不会强持有方针方针(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];

它的方针联系图如下:

iOS中的内存管理|weak

  • 运用 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循环引证分析与处理