前言
Weak 是弱引证,它是 iOS 中用于润饰变量的一种润饰符,它有两个特点:
- 被
weak润饰符润饰的目标,引证计数不会 +1 - 被
weak润饰符润饰的目标,在抛弃时,会将nil赋值给该变量
所谓引证计数,是苹果用来管理内存的一种机制。当一个目标被强引证时,它的引证计数会 +1,有多个强引证,每个强引证都是导致引证计数 +1,当一个强引证被开释,引证计数 -1,当引证计数为 0 时,系统会调用 dealloc 函数来毁掉内存。
目前苹果选用的是自动引证计数,也便是咱们不需求去手动的去对目标进行 retain(对引证计数+1) 和 release(对引证计数-1)的操作,这些由编译器来完结。但其实只要编译器的话是无法完全担任的,还需求运行时库的协助。
而运行时库会依据开发者供给的目标的润饰符,来和编译器共同确定怎么去管理这个目标的内存。
iOS 中有多种润饰符:
咱们要点阐明,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;
}
它们的联系如图:
中心的实线箭头代表它们相互进行了强引证,强引证会导致引证计数 +1,在它们的效果域现已完毕之后,因为它们的互相引证,所以编译器无法开释它们的内存(引证计数为 0 才会开释),然后导致内存泄漏。在 viewDidLoad 办法履行完毕之后,并没有打印 Cat 和 Dog 的 dealloc 办法。
此刻 weak 就派上用场了,在上面的代码中运用的润饰符都是 strong,将其中一个,比方 Dog 中的 @property (nonatomic, strong) Cat *cat 改为 @property (nonatomic, weak) Cat *cat,此刻他们的引证联系如下:
虚线代表弱引证,它不会导致引证计数 +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,展示汇编代码,能够看到下面这段代码:
经过 objc_initWeak 函数初始化附有 __weak 润饰符的变量,在变量效果域完毕时经过 objc_destoryWeak 函数开释该变量。
翻开运行时库的源码,咱们能够找到 objc_initWeak 和 objc_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 办法,这个办法有点长,并且没有一些前置的知识点的话估计看了源码也是一脸懵,所以先简单说一下这个办法首要做了哪些工作。
- 这个办法中会运用两张表,
oldTable和newTable,分别代表旧的弱引证表和新的弱引证表。 - 如果
weak指针之前现已指向了一个弱引证,那么将旧的weak指针地址从旧的弱引证表移除 - 如果
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_entries:hash数组,用来寄存所有弱引证该目标的指针 -
num_entries:hash数组中的元素个数 -
mask:hash数组长度 -1,会参加hash计算 -
max_hash_displacement:可能会发生的hash抵触的最大次数,用于判断是否出现了逻辑过错(hash表中的抵触次数绝不会超过该值)
别的,在苹果的注释中能够看到,weak_table_t 是以目标的 id 作为 key,以 weak_entries 作为值的方式来寄存一个目标的弱引证的。
综上,咱们能够得出一个结构图:
(转自 iOS底层原理:weak的完成原理)
需求阐明的是,引证计数表并不是只要一张表,而是很多张表,统称为 SideTables,以链表的方式串联起来。
源码的完成
知道上面这个结构之后,其实怎么去存储 weak 指针和怎么再目标抛弃时将 weak 指针置为 nil 咱们也能大约能猜出来了。
- 当运用一个
weak指针指向某个目标时,咱们以这个目标的 id 为 key,以这个weak指针作为值,将其寄存弱引证表中。 - 如果这个
weak指针之前现已指向了其他目标,也便是现已寄存在了其他的弱引证表中,天然得先将它从之前的弱引证表中移除,因为它行将指向了新的目标 - 当被
weak指针指向的这个目标履行dealloc办法,也便是在析构时,只需求以这个目标的 id 为 key,取出对应的values遍历一下全部置为nil。当然,也要将这个key和values从弱引证表中移除去。
源码的解析就直接看这篇 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);
-
objc_loadWeakRetained函数取出附有__weak润饰符变量所引证的目标并retain -
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 次。
运用 __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 次。
dene~







