前言

Weak 是弱引证,它是 iOS 中用于润饰变量的一种润饰符,它有两个特点:

  • weak 润饰符润饰的目标,引证计数不会 +1
  • weak 润饰符润饰的目标,在抛弃时,会将 nil 赋值给该变量

所谓引证计数,是苹果用来管理内存的一种机制。当一个目标被强引证时,它的引证计数会 +1,有多个强引证,每个强引证都是导致引证计数 +1,当一个强引证被开释,引证计数 -1,当引证计数为 0 时,系统会调用 dealloc 函数来毁掉内存。

目前苹果选用的是自动引证计数,也便是咱们不需求去手动的去对目标进行 retain(对引证计数+1) 和 release(对引证计数-1)的操作,这些由编译器来完结。但其实只要编译器的话是无法完全担任的,还需求运行时库的协助。

而运行时库会依据开发者供给的目标的润饰符,来和编译器共同确定怎么去管理这个目标的内存。

iOS 中有多种润饰符:

Tip4 - 关于 Weak 应该知道的

咱们要点阐明,weak 的效果以及它的完成原理。

weak 处理循环引证

weak,其实它便是用来处理循环引证的,下面是一个循环引证的比方:

@interface Dog: NSObject
@property (nonatomic, strong) Cat *cat;
@end
@implementation Dog
- (void)dealloc {
  NSLog(@"dog dealloc");
}
@end
@interface Cat: NSObject
@property (nonatomic, strong) Dog *dog;
@end
@implementation Cat
- (void)dealloc {
  NSLog(@"cat dealloc");
}
@end

在调用时:

- (void)viewDidLoad {
  [super viewDidLoad];
  Cat *cat = [[Cat alloc] init];
  Dog *dog = [[Dog alloc] init];
  cat.dog = dog;
  dog.cat = cat;
}

它们的联系如图:

Tip4 - 关于 Weak 应该知道的

中心的实线箭头代表它们相互进行了强引证,强引证会导致引证计数 +1,在它们的效果域现已完毕之后,因为它们的互相引证,所以编译器无法开释它们的内存(引证计数为 0 才会开释),然后导致内存泄漏。在 viewDidLoad 办法履行完毕之后,并没有打印 CatDogdealloc 办法。

此刻 weak 就派上用场了,在上面的代码中运用的润饰符都是 strong,将其中一个,比方 Dog 中的 @property (nonatomic, strong) Cat *cat 改为 @property (nonatomic, weak) Cat *cat,此刻他们的引证联系如下:

Tip4 - 关于 Weak 应该知道的

虚线代表弱引证,它不会导致引证计数 +1,所以在它们的效果域完毕,也便是 viewDidLoad 办法履行完毕之后,编译器会发现 Cat 的引证计数是 0,便是开释 Cat,当 Cat 被开释之后,Cat 所持有的 dog 的引证就没有了,dog 的引证计数也会变为 0,编译器就也会开释掉 dog 的内存,这样就处理了循环引证的问题。

控制台的打印成果为:

cat dealloc
dog dealloc

weak 原理

除了在特点中运用 weak 的办法,要弱引证一个目标,咱们还能够这样运用:

NSObject *obj = [[NSObject alloc] init];
id __weak obj1 = obj;

id __weak obj1 = obj; 处给个断点,翻开 Xcode -> Debug -> Debug Workflow -> Always show Disassembly,展示汇编代码,能够看到下面这段代码:

Tip4 - 关于 Weak 应该知道的

经过 objc_initWeak 函数初始化附有 __weak 润饰符的变量,在变量效果域完毕时经过 objc_destoryWeak 函数开释该变量。

翻开运行时库的源码,咱们能够找到 objc_initWeakobjc_destoryWeak 的完成:

id objc_initWeak(id *location, id newObj)
{
  if (!newObj) {
    *location = nil;
    return nil;
  }
  return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
    (location, (objc_object*)newObj);
}
void objc_destroyWeak(id *location)
{
  (void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
    (location, nil);
}

能够看到两个办法的内部都是调用了 storeWeak,所以上述源代码大致等于:

id obj1;
storeWeak(obj1, obj);
storeWeak(obj1, nil);

那么要点便是 storeWeak 办法,这个办法有点长,并且没有一些前置的知识点的话估计看了源码也是一脸懵,所以先简单说一下这个办法首要做了哪些工作。

  1. 这个办法中会运用两张表,oldTablenewTable,分别代表旧的弱引证表和新的弱引证表。
  2. 如果 weak 指针之前现已指向了一个弱引证,那么将旧的 weak 指针地址从旧的弱引证表移除
  3. 如果 weak 指针需求指向一个新的引证,将weak 指针添加到新的弱引证表中

所以为了看懂这个源码,咱们得先知道什么是弱引证表。

弱引证表

弱引证表和引证计数表休戚相关,它们都是散列表。散列表便是哈希表,咱们用的字典 NSDictionary 也是这样的结构。

引证计数表在源码中对应的,是一个名为 SideTable 的结构体:

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
    ...
}

SideTable 首要有三个成员:

  • slock:自旋锁,一个功率很高的锁,用于操作 SideTable 时进行上锁和解锁操作。
  • refcnts:这是用来存储引证计数的哈希表,便是咱们目标的引证计数是寄存在里的。
  • weak_table:便是咱们所说的弱引证表所对应的结构体。

继续跳进 weak_table_t 中:

/**
* The global weak references table. Stores object ids as keys,
* and weak_*entry_t structs as their values.*
*/
struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t  num_entries;
  uintptr_t mask;
  uintptr_t max_hash_displacement;
}
  • weak_entrieshash 数组,用来寄存所有弱引证该目标的指针
  • num_entrieshash 数组中的元素个数
  • maskhash 数组长度 -1,会参加 hash 计算
  • max_hash_displacement:可能会发生的 hash 抵触的最大次数,用于判断是否出现了逻辑过错(hash 表中的抵触次数绝不会超过该值)

别的,在苹果的注释中能够看到,weak_table_t 是以目标的 id 作为 key,以 weak_entries 作为值的方式来寄存一个目标的弱引证的。

综上,咱们能够得出一个结构图:

Tip4 - 关于 Weak 应该知道的

(转自 iOS底层原理:weak的完成原理)

需求阐明的是,引证计数表并不是只要一张表,而是很多张表,统称为 SideTables,以链表的方式串联起来。

源码的完成

知道上面这个结构之后,其实怎么去存储 weak 指针和怎么再目标抛弃时将 weak 指针置为 nil 咱们也能大约能猜出来了。

  • 当运用一个 weak 指针指向某个目标时,咱们以这个目标的 id 为 key,以这个 weak 指针作为值,将其寄存弱引证表中。
  • 如果这个 weak 指针之前现已指向了其他目标,也便是现已寄存在了其他的弱引证表中,天然得先将它从之前的弱引证表中移除,因为它行将指向了新的目标
  • 当被 weak 指针指向的这个目标履行 dealloc 办法,也便是在析构时,只需求以这个目标的 id 为 key,取出对应的 values 遍历一下全部置为 nil。当然,也要将这个 keyvalues 从弱引证表中移除去。

源码的解析就直接看这篇 iOS底层原理:weak的完成原理 吧,现已讲的很详细了就不再写一遍了。

总结

weak 被发明出来,便是首要来处理循环引证的问题的,它以指向的目标的地址为 key,将自身寄存在弱引证表中,弱引证表是引证计数表中的一个成员。当咱们运用 weak 去指向一个目标时,运行时库会将咱们将 weak 指针给保存起来,在所指向的目标被开释时,运行时库也会将保存起来的 weak 指针置为 nil,保证安全

别的,附有 __weak 润饰符变量所引证的目标是会被注册到 autoreleasepool 中的,比方一段代码:

{
    id __weak obj1 = obj;
    NSLog(@"%@", obj1);
}

该源代码可转换为如下方式:

// 编译器的模仿代码
id obj1;
objc_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(%@, tmp);
objc_destroyWeak(&obj1);
  1. objc_loadWeakRetained 函数取出附有 __weak 润饰符变量所引证的目标并 retain
  2. objc_autorelease 函数将目标注册到 autoreleasepool

__weak 所引证的目标像这样被注册到了 autoreleasepool 中,因此在 @autoreleasepool 块完毕之前都能够放心运用。可是,如果很多的运用附有 __weak 润饰符的变量,注册到 autoreleasepool 的目标也会很多的增加,因此在运用 __weak 时,最好先暂时赋值给 __strong 润饰符润饰的变量之后再运用。

比方,以下代码运用了 5 次附有 __weak 润饰符的变量 o。

{
    NSObject *obj = [[NSObject alloc] init];
    id __weak o = obj;
    NSLog(@"1 %@", o);
    NSLog(@"2 %@", o);
    NSLog(@"3 %@", o);
    NSLog(@"4 %@", o);
    NSLog(@"5 %@", o);
}

相应的,变量 o 所赋值的目标也就注册到 autoreleasepool 中 5 次。

Tip4 - 关于 Weak 应该知道的

运用 __strong 能够避免此类问题:

{
    NSObject *obj = [[NSObject alloc] init];
    id __weak o = obj;
    id tmp = o;
    NSLog(@"1 %@", tmp);
    NSLog(@"2 %@", tmp);
    NSLog(@"3 %@", tmp);
    NSLog(@"4 %@", tmp);
    NSLog(@"5 %@", tmp);
}

tmp = o; 时目标进登录到 autoreleasepool 中 1 次。

Tip4 - 关于 Weak 应该知道的

dene~