内存-②循环引用

内存-②循环引用

[toc]

目录

1
2
3
4
5
6
7
8
9
10
1、Objective-C中block为何得用copy修饰,能否用其他
2、block的循环引用
3、block循环引用的解决
4、判断该block是否会发生循环引用
5、为什么masonry的block里引用self不需要weak?
6、是否所有的Block中,使用self 都会导致循环引用?
7、block修改外部局部变量
(1)、在block中无法直接修改外部变量的原因
(2)、解决如何在 block 中修改外部变量
>

一、循环引用原因

< 返回目录

导致iOS对象无法按预期释放的一个无形杀手是——循环引用。循环引用可以简单理解为A引用了B,而B又引用了A,双方都同时保持对方的一个引用,导致任何时候引用计数都不为0,始终无法释放。若当前对象是一个ViewController,则在dismiss或者pop之后其dealloc无法被调用,在频繁的push或者present之后内存暴增,然后APP就duang地挂了。

循环引用会导致内存泄露,因为循环应用会导致,有些对象没办法在已经不会再使用的时候被释放掉

下面列举我们变成中比较容易碰到的三种循环引用的情形:

1
2
3
4
5
6
(1)计时器NSTimer:✅__weak typeof(self) weakSelf = self;
循环引用:self -> timer -> block -> self
(2)block:✅copy
@property (nonatomic, copy) void (^myBlock)(void);
(3)委托delegate:✅weak
@property (nonatomic, weak) id<YourDelegateProtocol> delegate;

二、NSTimer的循环引用

< 返回目录

NSTimer的循环引用详情,我们放在下面讲解NSTimer(前面已经提过NSTimer 其实就是RunLoop中的CFRunLoopTimerRef,一个基于时间的触发器)的时候介绍。

解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@interface HWWeakTimerTarget : NSObject

@property (nonatomic, weak) id target; // 注意是 weak
@property (nonatomic, assign) SEL selector; // assign
@property (nonatomic, weak) NSTimer* timer; // 注意是 weak

@end

@implementation HWWeakTimerTarget

- (void)fire:(NSTimer *)timer {
if(self.target) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.target performSelector:self.selector withObject:timer.userInfo afterDelay:0.0f];
#pragma clang diagnostic pop
} else {
[self.timer invalidate];
}
}

@end

@implementation HWWeakTimer

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats {
HWWeakTimerTarget *timerTarget = [[HWWeakTimerTarget alloc] init];
timerTarget.target = aTarget;
timerTarget.selector = aSelector;
timerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval
target:timerTarget
selector:@selector(fire:)
userInfo:userInfo
repeats:repeats];
return timerTarget.timer;
}

@end

三、委托delegate的循环引用

< 返回目录

在委托问题上出现循环引用问题已经是老生常谈了,声明delegate时请用weak(ARC),如果是MRC则用assign。千万别手贱。

四、block的循环引用

< 返回目录

1、Objective-C中block为何得用copy修饰,能否用其他

答:因为block在创建的时候,它的内存是分配在栈(stack)上,而不是在堆(heap)上。他本身的作于域是属于创建时候的作用域,一旦在创建时候的作用域外面调用block将导致程序崩溃。所以,为了能够在block的声明域外也能够使用block,我们需要将block拷贝到堆上,所以使用copy属性。对于堆中的block,也就是copy修饰的block。他的生命周期是随着对象的销毁而结束的。只要对象不销毁,我们就可以调用的到在堆中的block。

Block 在创建时可能存储在不同的内存区域(栈或堆)中,而在将其赋值给对象或作为函数或方法的参数时,需要确保 Block 存储在堆中,以便在调用时能够正常执行。如果 Block 存储在栈中,则在函数或方法返回后,Block 对象所在的内存区域将被释放,从而导致程序崩溃或者产生其他未定义的行为。所以block应该被持有,即应该使用copy或者strong修改。那为什么我们还是习惯使用copy呢?因为使用 copy 可以提高线程安全性,因为每个线程可以独立地使用 block 的副本,而不必担心其他线程对原始 block 的修改。举例点击一个按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@interface MyClass : NSObject
@property (nonatomic, copy) void (^updateUIBlock)(void); // 使用 copy 属性
@end

@implementation MyClass

- (id)init {
self = [super init];
if (self) {
__weak typeof(self) weakSelf = self; // 使用 __weak 避免循环引用
self.updateUIBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
dispatch_async(dispatch_get_main_queue(), ^{
[strongSelf __updateUI];
});
}
};
}
return;
}
- (void)__updateUI {
// 更新 UI 的代码
NSLog(@"UI updated on main thread");
}

- (void)clickToUpdateUI {
// 执行异步任务,并在完成后调用 updateUIBlock
[self performBackgroundTaskWithCompletion:self.updateUIBlock];
}

// completion这个block如果是原block的副本,那么这里即使多个后台任务尝试同时更新 UI,每个任务都会有 `updateUIBlock` 的独立副本,不会相互干扰。这确保了 UI 更新操作的线程安全性。
- (void)performBackgroundTaskWithCompletion:(void (^)(void))completion {
// 模拟后台线程任务
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// ... 执行一些后台操作 ...

// 任务完成后,调用 completion block
if (completion) {
completion();
}
});
}

@end

扩展:为什么NSString 可以使用strong和copy,却选择使用 copy?(个人理解:因为NSString是不可变的,既然不可变还不如使用copy再加一层线程安全)

使用 copy 属性修饰符通常有以下原因:

  1. 确保线程安全:如果你在多线程环境中操作字符串,使用 copy 可以确保每个线程都有自己的副本,从而避免线程安全问题。
  2. 避免外部修改:如果你不希望外部代码修改原始字符串,使用 copy 可以确保你的属性持有的是原始字符串的一个副本,外部代码对原始字符串的修改不会影响到你的属性。
2、block的循环引用

由于block在copy时都会对block内部用到的对象进行强引用(ARC)或者retainCount增1(非ARC)。所以,不管是在ARC还是非ARC环境下对block使用不当都会引起循环引用问题

一般表现为,某个类将block作为自己的属性变量(则该类就对block强引用了),然后该类在block的方法体里面又使用了该类本身。即当对象(比如self)拥有一个block属性的时候,在block属性中又引用了对象的其他成员变量或者调用了对象的其他方法。形成你中有我,我中有你,谁都无法将谁释放的困局。形如:

1
2
3
4
5
6
7
8
9
10
11
self.myBlock = ^{
[self doSomething];
};

又或者

ClassA* objA = [[ClassA alloc] init];
objA.myBlock = ^{
[self doSomething];
};
self.objA = objA;

block的这种循环引用会被编译器捕捉到并及时提醒。

以上参考自:Block的循环引用

3、block循环引用的解决
(1)、常规简单解法

解决方法,就一句话的事情:

1
2
3
4
5
6
7
8
__weak typeof (self) weakSelf = self; 
self.myBlock = ^{
[weakSelf doSomething];
};

附1:如果是non-ARC环境下就将__weak替换为__block即可。non-ARC情况下,__block变量的含义是在Block中引入一个新的结构体成员变量指向这个__block变量,那么__block typeof(self) weakSelf = self;就表示Block别再对self对象retain啦,这就打破了循环引用。

附2:__weak 是 iOS 5.0 推出的,_weak 相当于 weak,不会做强引用,如果对象被释放,执行的地址,会指向 nil
(2)、block中使用 weak–strong dance 技术避免循环引用
1
2
3
4
5
6
7
8
__weak typeof(self)weakSelf = self;
[header setTapHandle:^{
__strong __typeof(weakSelf)strongSelf = weakSelf;
if (strongSelf) {
NSLog(@"strongSelf = %@", strongSelf);
}
[weakSelf headerAction:header];
}];

精髓2:①在 block 之前定义对 self 的一个弱引用weakSelf,因为是弱引用,所以当 self 被释放时weakSelf会变为nil;② 在 block 中引用该弱应用,考虑到多线程情况,通过使用强引用 strongSelf 来引用该弱引用,这时如果 self 不为 nil 就会 retain self,以防止在后面的使用过程中 self 被释放;③在之后的 block 块中使用该强引用 bself,注意在使用前要对 bSelf 进行了 nil 检测,因为多线程环境下在用弱引用weakSelf对强引用strongSelf赋值时,弱引用weakSelf可能已经为nil了。通过这种手法,block 就不会持有 self 的引用,从而打破了循环引用。

iOS开发中在block中为什么要weak和strong配合使用

答:weak是为了解决循环引用。strong是为了防止block持有的对象提前释放。

1
2
3
4
5
6
7
8
9
10
11
 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self dismissViewControllerAnimated:YES completion:nil];

__weak typeof(self) weakSelf = self;
self.block = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", weakSelf);
});
};
self.block();
}

点击屏幕,当前控制器消失,同时被销毁掉,5秒后打印的weakSelf就是一个(null)。
而我们如果在block内使用__strong后就能保证再打印完strongSelf之后再释放当前控制器。

4、判断该block是否会发生循环引用

判断该block是否会发生循环引用例子

5、为什么masonry的block里引用self不需要weak?

这个就和网络请求里面使用self道理是一样的。因为UIView未强持有block,所以这个block只是个栈block,而且构不成循环引用的条件。栈block有个特性就是它执行完毕之后就出栈,出栈了就会被释放掉。看mas_makexxx的方法实现会发现这个block很快就被调用了,完事儿就出栈销毁,构不成循环引用,所以可以直接放心的使用self。

1
2
3
4
5
6
7
8
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}

如果要强引用,block应该是masnory的一个属性,即被masnory对象持有。且是copy修饰符

6、是否所有的Block中,使用self 都会导致循环引用?

7、block修改外部局部变量

如果是全局变量呢?

(1)、在block中无法直接修改外部变量的原因

错误示例:

1
2
3
4
5
6
7
8
9
10
11
//声明一个局部整型变量 
int intValue = 3; //漏掉了__block修饰符

//声明一个返回值为int,一个int参数的block变量
int (^block)(int) = ^(int m){
intValue++;
return m * intValue;
};

//调用block变量,5作为参数之后的结果
NSLog(@"block(5) = %d",block(5));

在上面的例子中,我们编译程序后发现编译器会有红色错误,错误提示为
Variable is not assignable (missing __block type specifier)

为什么会出现不能被赋值的错误提示呢?

block在实现时就会对它引用到的它所在方法中定义的栈变量进行一次只读拷贝,在 block 块内使用该只读拷贝。
那为了避免上述错误,就要精髓1:使用__block修饰符来修饰外部变量,用来通知编译器该外部变量intValue与block中的intValue指的是同一块儿内存地址,而不需要内存拷贝。

(2)、解决如何在 block 中修改外部变量

有两种办法
① 第一种是可以修改 static 全局变量;
② 第二种是可以修改用新关键字 __block 修饰的变量。

1
2
3
4
5
6
7
8
9
10
11
12
__block int blockLocal  = 100;
static int staticLocal = 100;

void (^aBlock)(void) = ^(void){
NSLog(@" >> Sum: %d\n", global + staticLocal);

global++;
blockLocal++;
staticLocal++;
};

aBlock();

附:静态变量 和 全局变量 在加和不加 __block 都会直接引用变量地址。也就意味着静态变量和全局变量的修改可以直接修改,不需要作添加__block的步骤。

在ARC下获取对象的引用计数值

在ARC下获取对象的引用计数值,可以使用CFGetRetainCount。

1
2
3
4
5
6
7
对于Core Foundation对象:
NSLog(@"CFGetRetainCount is %ld", CFGetRetainCount(aCFString));

对于Foundation对象
NSLog(@"CFGetRetainCount is %ld", CFGetRetainCount((__bridge CFTypeRef)aNNstring));

NSLog(@"%@ CFGetRetainCount is %ld", NSStringFromClass([self class]), CFGetRetainCount((__bridge CFTypeRef)self));

以下获取引用计数的方法错了,但是不知道正确的应该怎么用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)dealloc {
NSLog(@"%@ dealloc", NSStringFromClass([self class]));
}

- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];

NSLog(@"%@ viewWillDisappear and CFGetRetainCount is %ld", NSStringFromClass([self class]), CFGetRetainCount((__bridge CFTypeRef)self));
}

- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];

NSLog(@"%@ viewDidDisappear and CFGetRetainCount is %ld", NSStringFromClass([self class]), CFGetRetainCount((__bridge CFTypeRef)self));
}

几篇文章:

常见笔试/面试题

< 返回目录

block和weak修饰符的区别是什么?
1
2
3
4
1,在MRC时代,__block修饰,可以避免循环引用;ARC时代,__block修饰,同样会引起循环引用问题;
2,__block不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型;
3,__weak只能在ARC模式下使用,也只能修饰对象,不能修饰基本数据类型;
4,__block对象可以在block中被重新赋值,__weak不可以;

END

< 返回目录