前言
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~