循环引用的理解
首先说一下循环引用,为什么没用 __weak
修饰就直接用 self.
属性,有时候不会造成循环引用,有时候会造成循环引用呢。
循环引用是指两个或者多个对象循环持有造成的无法释放(即引用计数减不到0)。
例如:类 Person
有个属性 block
, 在 block
实现后, 此时 self
持有 block
,如果在 block
中,直接使用 self
,block
将持有 self
,造成循环引用, 如果 block
本身不是 self
的属性,则 self
不持有 block
,即使在 block
中直接使用 self
也不会造成循环引用,但是为了避免多个对象的循环引用,所以 block
中最好还是用 __weak
,防止这种情况出现。代理用 weak
与此同理。
__weak、__block、__strong的作用
- __weak:弱引用变量修饰词,引用计数不会 +1。本身可以避免循环引用的问题的,但是其会导致外部对象释放了之后,Block 内部也访问不到这个对象的问题,我们可以通过在 Block 内部声明一个
__strong
的变量来指向weakObj
,使外部对象既能在 Block 内部保持住,又能避免循环引用的问题。 - __block:Block内部修改外部变量修饰词,使外部变量可以在 Block 内部进行修改。本身无法避免循环引用的问题,但是我们可以通过在 Block 内部手动把
blockObj
赋值为nil
的方式来避免循环引用的问题。另外一点就是__block
修饰的变量在 Block 内外都是唯一的,要注意这个特性可能带来的隐患。
但是__block
有一点:这只是限制在ARC环境下。在非ARC下,__block
是可以避免引用循环的。 - __strong:强引用变量修饰词,引用计数会+1。常用于 Block 内部对
blockObj
的引用修饰,如上面👆__weak
的说明。
代码示例
示例1(__weak的使用)
- (void)methond_1
{
NSString *string = @"1";
__weak NSString *weakStr = string;
void (^ block)() = ^ {
// 此处 weakStr 不能被修改,会报红
//weakStr = @"2";
};
block();
NSLog(@"string = %@ pointer = %p pointer_content = %p", string, &string, string);
NSLog(@"weakStr = %@ pointer = %p pointer_content = %p", weakStr, &weakStr, weakStr);
// &string 得到的是变量 string 本身的存储地址,而 number 得到的是存储的内容 @"1" 的地址。
// log:
// string = 1 pointer = 0x7fff587269f8 pointer_content = 0x1075d6ee0
// weakStr = 1 pointer = 0x7fff587269f0 pointer_content = 0x1075d6ee0
}
示例2(__block的使用)
- (void)methond_2
{
// __block:使外部变量可以在 Block 内部进行修改.
NSNumber *number = @1;
__block NSNumber *blockNum = number;
void (^ block)() = ^ {
blockNum = @2;
};
block();
NSLog(@"number = %@ pointer = %p pointer_content = %p", number, &number, number);
NSLog(@"blockNum = %@ pointer = %p pointer_content = %p", blockNum, &blockNum, blockNum);
// log:
// number = 1 pointer = 0x7fff5e35dad0 pointer_content = 0xb000000000000012
// blockNum = 2 pointer = 0x618000051008 pointer_content = 0xb000000000000022
// 可见 Block 会拷贝原来对象, __block 修饰的对象可被 Block 内外同时修改.
}
示例3(在堆区的变量与在栈区的变量对比)
- (void)methond_3
{
// model 变量是在堆区
BaseModel *model = [[BaseModel alloc] init];
__weak BaseModel *weakModel = model;
__weak __block TestVC *blockSelf = self;
self.blockModel = ^ {
// 如果 blockSelf 不用 __block 修饰,则在此处不能修改 testString 值,如果不用 __weak 修饰,则会引起循环引
blockSelf.testString = @"此时 model = nil,model 已被释放,所以 weakModel = nil";
};
model = nil;
self.blockModel();
NSLog(@"model = %@ pointer = %p pointer_content = %p", model, &model, model);
NSLog(@"weakMoedl = %@ pointer = %p pointer_content = %p", weakModel, &weakModel, weakModel);
// log:
// model = (null) pointer = 0x7fff595a9ad0 pointer_content = 0x0
// weakMoedl = (null) pointer = 0x7fff595a9ac8 pointer_content = 0x0
// number 变量是在栈区, 值@1是在常量区
NSNumber *number = @1;
__weak NSNumber *blockNum = number;
number = nil;
NSLog(@"number = %@ pointer = %p pointer_content = %p", number, &number, number);
NSLog(@"blockNum = %@ pointer = %p pointer_content = %p", blockNum, &blockNum, blockNum);
// log:
// number = (null) pointer = 0x7fff5a31cad0 pointer_content = 0x0
// blockNum = 1 pointer = 0x7fff5a31cac8 pointer_content = 0xb000000000000012
// string 变量是在栈区,值@"string"是在常量区
NSString *string = @"string";
__weak NSString *weakString = string;
string = nil;
NSLog(@"string = %@ pointer = %p pointer_content = %p", string, &string, string);
NSLog(@"weakString = %@ pointer = %p pointer_content = %p", weakString, &weakString, weakString);
// log:
// string = (null) pointer = 0x7fff5f627ad0 pointer_content = 0x0
// weakString = string pointer = 0x7fff5f627ac8 pointer_content = 0x1006d4e00
// 字符串常量是存在常量区的,栈内存并不会动态释放,而是当当前线程执行完毕后,释放当前线程的栈内存。所有的常量都存在常量区,
// 所以上面的例子中即使使用__ weak 修饰, 但是 @1 和 @"string" 这2个常量并没有被释放, 所以 weak 的地址指向依然存在值.
}
示例4(__weak与__block作用的对比)
- (void)methond_4
{
BaseModel *model = [[BaseModel alloc] init];
__weak BaseModel *weakModel = model;
void (^ block)() = ^ {
// weakModel 弱引用, 此时 model = nil ,所以 strongModel = weakModel = nil
__strong BaseModel *strongModel = weakModel;
NSLog(@"strongModel = %@ pointer = %p pointer_content = %p", strongModel, &strongModel, strongModel);
};
model = nil;
block();
NSLog(@"model = %@ pointer = %p pointer_content = %p", model, &model, model);
NSLog(@"weakMoedl = %@ pointer = %p pointer_content = %p", weakModel, &weakModel, weakModel);
// 在 model 置为 nil 之前, block 的 __ strong 并没有执行, 所以当时 model 对象被当前的区块持有, 当 model 置为 nil 时, 该对象已经被释放, 所以 __strong 的时候, weakModel 地址的内存已经被释放, strongModel 指向 nil, 所以 model 对象引用计数并没有加 1.
// log:
// strongModel = (null) pointer = 0x7fff5e6669a8 pointer_content = 0x0
// model = (null) pointer = 0x7fff5e666ad0 pointer_content = 0x0
// weakMoedl = (null) pointer = 0x7fff5e666ac8 pointer_content = 0x0
BaseModel *model_2 = [[BaseModel alloc] init];
__block BaseModel *blockModel = model_2;
void (^ blockModel_2)() = ^ {
// weakModel 只是被 __block 修饰,并不是弱引用,所以 model = nil 并不影响 weakModel 的值,所以 strongModel = weakModel != nil
__strong BaseModel *strongModel = blockModel;
NSLog(@"strongModel = %@ pointer = %p pointer_content = %p", strongModel, &strongModel, strongModel);
};
model = nil;
blockModel_2();
NSLog(@"model = %@ pointer = %p pointer_content = %p", model_2, &model_2, model_2);
NSLog(@"blockModel = %@ pointer = %p pointer_content = %p", blockModel, &blockModel, blockModel);
// log:
// strongModel = <BaseModel: 0x60800001b590> pointer = 0x7fff558af9e8 pointer_content = 0x60800001b590
// model = (null) pointer = 0x7fff558afad0 pointer_content = 0x0
// weakMoedl = <BaseModel: 0x60800001b590> pointer = 0x7fff558afac8 pointer_content = 0x60800001b590
}
示例5(__weak和__strong的使用)
- (void)methond_5
{
BaseModel *model = [[BaseModel alloc] init];
__weak BaseModel *weakModel = model;
void (^ block)() = ^ {
__strong BaseModel *strongModel = weakModel;
NSLog(@"虽然此时 model = nil, weakModel 也被 __weak 修饰,但是在下面👇线程中 weakModel 被 threadStrong 强引用,weakModel 的引用计数 +1 ,当 model = nil 时,weakModel 也不会被释放,所以此时 strongModel = weakModel != nil");
NSLog(@"strongModel = %@ pointer = %p pointer_content = %p", strongModel, &strongModel, strongModel);
};
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
__strong BaseModel *threadStrong = weakModel;
NSLog(@"weakModel 在线程中被强引用,引用计数+1");
sleep(5);
NSLog(@"threadStrong = %@ pointer = %p pointer_content = %p", threadStrong, &threadStrong, threadStrong);
});
sleep(1);
// 此时 weakModel 在线程中已被强引用,引用计数 +1,model = nil 并不能使得 weakModel 也等于 nil
model = nil;
block();
NSLog(@"model = %@ pointer = %p pointer_content = %p", model, &model, model);
NSLog(@"weakMoedl = %@ pointer = %p pointer_content = %p", weakModel, &weakModel, weakModel);
// weakModel 在线程中被强引用,引用计数+1
// 虽然此时 model = nil, weakModel 也被 __weak 修饰,但是在下面👇线程中 weakModel 被 threadStrong 强引用,weakModel 的引用计数 +1 ,当 model = nil 时,weakModel 也不会被释放,所以此时 strongModel = weakModel != nil
// log:
// strongModel = <BaseModel: 0x60000000ad90> pointer = 0x7fff50507958 pointer_content = 0x60000000ad90
// model = (null) pointer = 0x7fff50507ad0 pointer_content = 0x0
// weakMoedl = <BaseModel: 0x60000000ad90> pointer = 0x7fff50507ac8 pointer_content = 0x60000000ad90
// threadStrong = <BaseModel: 0x60000000ad90> pointer = 0x7000041c4d88 pointer_content = 0x60000000ad90
}
示例6(__weak和__strong的使用 -> block 内修改全局变量)
@interface TestVC ()
{
NSString *testVar;
}
@property (nonatomic, strong) NSString *testString;
@property (nonatomic, copy) void (^ blockModel)();
@end
@implementation TestVC
- (void)methond_6
{
// 正确使用
__weak __block TestVC *weakSelf = self;
self.blockModel = ^{
__strong TestVC *strongSelf = weakSelf;
strongSelf.testString = @"testString";
strongSelf -> testVar = @"可以修改全局变量,并且不会导致 block 无法释放";
//当然也可以直接把这个全局变量改为属性声明,直接用 weakSelf. 或 strongSelf. 就行
};
//blockModel 被 self 持有,所以在 block 内部必须使用 __weak 修饰的 weakSelf,又因为要修改全局变量 testVar 使用 "->", 所以又使用 __strong 修饰的 strongSelf
// 编译不通过
self.blockModel = ^{
weakSelf.testString = @"testString";
//被 __weak 修饰过的 weakSelf 不能使用 "->"
//weakSelf -> testVar = @"这样写编译不通过,直接报红";
};
//报红:"dereferencing a __weak pointer is not allowed die to possible null value caused by a race condition, assign it to strong variable first"
// 无法修改全局变量 testVar(原理同示例3 👆)
__weak __block NSString *weakVar = testVar;
self.blockModel = ^{
weakSelf.testString = @"testString";
weakVar = @"无法修改全局变量 testVar";
};
self.blockModel = ^{
weakSelf.testString = @"testString";
self -> testVar = @"可以修改全局变量 testVar,但会引起 block 无法被释放,导致内存泄漏";
};
}
@end
总结
- 当在 block 内部修改外部局部变量时,需要用
__block
修饰;
e.g.:
NSNumber *number = @1;
__block NSNumber *blockNum = number;
void (^ block)() = ^ {
blockNum = @2;
};
- 当 block 被
self
持有,并且不对self
做修改,如self = nil;
(对self
的属性修改不算是对self
的修改),或者是不对self
的全局变量做修改(因为会用到 "->"),只需要用__weak
修饰即可;
e.g.:
__weak TestVC *weakSelf = self;
self.blockModel = ^{
weakSelf.testString = @"testString";
[weakSelf testMethod];
};
- 当 block 被
self
持有,并且对self
做修改,如self = nil;
,则需要用__weak
和__block
修饰;
e.g.:
__weak __block TestVC *weakSelf = self;
self.blockModel = ^{
weakSelf.testString = @"testString";
[weakSelf testMethod];
weakSelf = nil;
};
- 当 block 和
self
相互持有时,或者 block 内需要修改self
的全局变量时,则 block 外部需要用__weak
修饰,block 内部需要使用__strong
修饰的变量(为了安全起见,block 内部最好还是使用__strong
修饰的变量吧,不明白的请看上面👆示例5(__weak和__strong的使用));
e.g.:
__weak TestVC *weakSelf = self;
self.blockModel = ^{
__strong TestVC *strongSelf = weakSelf;
strongSelf.testString = @"testString";
strongSelf -> testVar = @"可以修改全局变量,并且不会导致 block 无法释放";
};
- 当 block 和
self
相互持有时,并且有修改self
,则外部需要用__weak
和__block
修饰,block 内部需要使用__strong
修饰的变量;
e.g.:
__weak __block TestVC *weakSelf = self;
self.blockModel = ^{
__strong TestVC *strongSelf = weakSelf;
strongSelf.testString = @"testString";
strongSelf -> testVar = @"可以修改全局变量,并且不会导致 block 无法释放";
strongSelf = nil;
};
以上5种情况基本说明了各个修饰词的使用场景,如果把握不来的,或者不理解的,为了安全起见直接按地种情况去写,老铁,没毛病。反正记住 以下几点:
__weak
是防止循环引用的;__block
是在 block 内部可以修改外部变量的 (在非ARC环境下也可以防止循环引用);__strong
是在内部防止外部的weak
变量被提前释放,在内部无法获取weak
变量;
以上示例代码基本可以说明__weak
、__block
、__strong
的使用规则了,如果还有哪些不清楚的,没有在示例代码中展现出来,建议自己动手写写看。如有对内存分配不太理解的小伙伴可以看看这篇文章《iOS程序中的内存分配(栈区和堆区的对比)》。
补充(weak的实现原理)
weak
变量在引用计数为0时,会被自动设置成nil
,这个特性是如何实现的?
很少有人知道weak
表其实是一个hash(哈希)表,Key是所指对象的地址,Value是 weak
指针的地址数组。更多人的人只是知道 weak
是弱引用,所引用对象的计数器不会加一,并在引用对象被释放的时候自动被设置为nil
。通常用于解决循环引用问题。但现在单知道这些已经不足以应对面试了,好多公司会问 weak
的原理。weak
的原理是什么呢?具体细节分析请看《iOS 底层解析weak的实现原理(包含weak对象的初始化,引用,释放的分析)》。
weak 实现原理的概括Runtime
维护了一个 weak
表,用于存储指向某个对象的所有 weak
指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak
指针的地址(这个地址的值是所指对象的地址)数组。
weak 的实现原理可以概括一下三步:
1、初始化时:runtime
会调用 objc_initWeak
函数,初始化一个新的 weak
指针指向对象的地址。
2、添加引用时:objc_initWeak
函数会调用 objc_storeWeak()
函数, objc_storeWeak()
的作用是更新指针指向,创建对应的弱引用表。
3、释放时,调用 clearDeallocating
函数。clearDeallocating
函数首先根据对象地址获取所有 weak
指针地址的数组,然后遍历这个数组把其中的数据设为 nil
,最后把这个 entry
从 weak
表中删除,最后清理对象的记录。