iOS 循环引用讲解(中)
谈到循环引用,可能是delegate为啥非得用weak修饰,可能是block为啥要被特殊对待,你也可能仅仅想到了一个weakSelf,因为它能解决99%的关于循环引用的事情。下面我以个人的理解谈谈循环引用,读完这篇文章,大约需要15-20分钟的时间。
一、循环引用的产生
当A对象里面强引用了B对象,B对象又强引用了A对象,这样两者的retainCount就一直都无法为0于是内存无法释放,导致内存泄露,所谓的内存泄露,本应该释放的对象,在生命周期结束之后依旧存在。换句话说:得说下内存中和变量有关的分区:堆、栈、静态区。其中,栈和静态区是操作系统自己管理的,对程序员来说相对透明,所以,一般我们只需要关注堆的内存分配,而循环引用的产生,也和其息息相关,即循环引用会导致堆里的内存无法正常回收。如下图:
此处:若想释放内存,需要A的引用计数为0,而B对象持有A,所以想要A dealloc,需要B发送release消息到A。而B只有dealloc的时候才会发送release消息到A,并且B dealloc也需要A发送release消息到B。这样A和B相互等待对方的release消息,造成循环引用,内存无法释放。
二、循环引用的情况
下面我们分析造成循环引用的几种情况:
1.delegate与环
1 @protocol ClssADelegate 2 - (void)eat; 3 @end 4 @interface ClassA : UIViewController 5 @property (nonatomic, strong) id delegate; 6 @end 7 //ClassB: 8 @interface ClassB () 9 @property (nonatomic, strong) ClassA *classA; 10 @end 11 @implementation ClassB 12 - (void)viewDidLoad { 13 [super viewDidLoad]; 14 self.classA = [[ClassA alloc] init]; 15 self.classA.delegate = self; 16 }
如上代码,B强引用A,而A的delegate属性指向B,这里的delegate是用strong修饰的,所以A也会强引用B,这是一个比较典型的循环引用样例。所以要将代理delegate改为弱引用weak。
2.block与环
1 @interface ClassA () 2 @property (nonatomic, copy) dispatch_block_t block; 3 @property (nonatomic, assign) NSInteger tem; 4 @end 5 @implementation ClassA 6 - (void)viewDidLoad { 7 [super viewDidLoad]; 8 self.block = ^{ 9 self.tem = 1; 10 }; 11 }
如上代码,self持有block,而堆上的block又会持有self,所以会导致循环引用,这个例子非常好,因为xcode都能检测出来,报出警告:[capturing self strongly in this block is likely to lead to a retain cycle],当然大部分循环引用的情况xcode是不会报警告的。解决这种循环引用的常用方式如下:
@interface ClassA () @property (nonatomic, copy) dispatch_block_t block; @property (nonatomic, assign) NSInteger tem; @end @implementation ClassA - (void)viewDidLoad { [super viewDidLoad]; __weak typeof(self) weakSelf = self self.block = ^{ weakSelf.tem = 1; }; }
结论:
如上delegate和block引起的循环引用的处理方式,有一个共同的特点,就是使用weak(弱引用)来打破坏,使环消失了,所以得出结论,我们可以通过将Strong(强引用)用weak来代替来解决循环引用。
>>>>>>>拓展
(1)weakSelf与其缺陷
1 //ClassB是一个UIViewController,假设从ClassA pushViewController将ClassB展示出来 2 @interface ClassB () 3 @property (nonatomic, copy) dispatch_block_t block; 4 @property (nonatomic, strong) NSString *str; 5 @end 6 @implementation ClassB 7 - (void)dealloc { 8 } 9 - (void)viewDidLoad { 10 [super viewDidLoad]; 11 self.str = @"111"; 12 __weak typeof(self) weakSelf = self; 13 self.block = ^{ 14 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 15 NSLog(@"%@", weakSelf.str); 16 }); 17 }; 18 self.block(); 19 }
这里有两种情况:
(!)若从Apush到B,10s之内没有pop回到A的话,B中block会执行打印出来111.
(!!)若从Apush到B,10s之内pop回A的话(B控制器已经释放掉了),B会立即执行dealloc,从而导致B中block打印出(null),这种情况是使用weakSelf的缺陷,可能会内存被提前释放。
(2)weakSelf和strongSelf
1 @interface ClassB () 2 @property (nonatomic, copy) dispatch_block_t block; 3 @property (nonatomic, strong) NSString *str; 4 @end 5 @implementation ClassB 6 - (void)dealloc { 7 } 8 - (void)viewDidLoad { 9 [super viewDidLoad]; 10 self.str = @"111"; 11 __weak typeof(self) weakSelf = self; 12 self.block = ^{ 13 __strong typeof(self) strongSelf = weakSelf; 14 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 15 NSLog(@"%@", strongSelf.str); 16 }); 17 }; 18 self.block(); 19 }
这样做解决了上面的问题,但可能有一些问题不是很理解:
(!)这么做和直接用self有什么区别,为什么不会有循环引用;外部的weakSelf是为了打破环,从而没有循环引用,而内部的strongSelf仅仅是个局部变量,存在栈中,会在block执行结束之后回收,不会再造成循环引用。
(!!)这么做和weakSelf有什么区别:唯一的区别就是多了一个strongSelf,这么的strongSelf会使classB的对象引用计数+1,使用ClassB pop到A的时候,并不会执行dealloc,因为计数还不为0,strongSelf仍持有classB,而在block执行完,局部的strongSelf才会回收,此时ClassB dealloc。
这样做其实已经可以解决所有问题,但是强迫症的我们依然能找到它的缺陷:
3、@weakify和@strongify
查看github上开源的libextobjc库,可以发现,里面的EXTScope.h里面有两个关于weak和strong的宏定义。
1 // 宏定义 2 #define weakify(...) \ 3 ext_keywordify \ 4 metamacro_foreach_cxt(ext_weakify_,, __weak, __VA_ARGS__) 5 #define strongify(...) \ 6 ext_keywordify \ 7 _Pragma("clang diagnostic push") \ 8 _Pragma("clang diagnostic ignored \"-Wshadow\"") \ 9 metamacro_foreach(ext_strongify_,, __VA_ARGS__) \ 10 _Pragma("clang diagnostic pop") 11 12 // 用法 13 @interface ClassB () 14 @property (nonatomic, copy) dispatch_block_t block; 15 @property (nonatomic, strong) NSString *str; 16 @end 17 @implementation ClassB 18 - (void)dealloc { 19 } 20 - (void)viewDidLoad { 21 [super viewDidLoad]; 22 self.str = @"111"; 23 @weakify(self) 24 self.block = ^{ 25 @strongify(self) 26 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 27 NSLog(@"%@", self.str); 28 }); 29 }; 30 self.block(); 31 }
可以看出,这样就完美的解决了上述的缺陷,我们可以在block随意使用self。
3.NSTimer的循环引用
使用NSTimer可能会碰到循环引用的问题。特别是当类具有NSTimer类型的成员变量,并且需要反复执行计时任务时。例如:
1 _timer = [NSTimer scheduledTimerWithTimeInterval:5.0 2 target:self 3 selector:@selector(startCounting) userInfo:nil 4 repeats:YES];
类有一个成员变量_timer
,给_timer
设置的target
为这个类本身。这样类保留_timer
,_timer
又保留了这个类,就会出现循环引用的问题,最后导致类无法正确释放。
解决这个问题的方式也很简单,当类的使用者能够确定不需要使用这个计时器时,就调用
1 [_timer invalidate]; 2 _timer = nil;
这样就打破了保留环,类也可以正确释放。但是,这种依赖于开发者手动调用方法,才能让内存正确释放的方式不是一个非常好的处理方式。所以需要另外一种解决方案。如下所示:
1 @interface NSTimer (JQUsingBlock) 2 + (NSTimer *)jq_scheduledTimerWithTimeInterval:(NSTimeInterval)ti 3 block:(void(^)())block 4 repeats:(BOOL)repeats; 5 @end 6 7 @implementation NSTimer (JQUsingBlock) 8 9 + (NSTimer *)jq_scheduledTimerWithTimeInterval:(NSTimeInterval)ti 10 block:(void(^)())block 11 repeats:(BOOL)repeats{ 12 13 return [self scheduledTimerWithTimeInterval:ti 14 target:self 15 selector:@selector(jq_blockInvoke:) 16 userInfo:[block copy] 17 repeats:repeats]; 18 } 19 20 + (void)jq_blockInvoke:(NSTimer *)timer{ 21 22 void(^block)() = timer.userInfo; 23 if (block) { 24 block(); 25 } 26 } 27 28 @end
定义一个NSTimer
的类别,在类别中定义一个类方法。类方法有一个类型为块的参数(定义的块位于栈上,为了防止块被释放,需要调用copy
方法,将块移到堆上)。使用这个类别的方式如下:
1 __weak ViewController *weakSelf = self; 2 _timer = [NSTimer jq_scheduledTimerWithTimeInterval:5.0 3 block:^{ 4 __strong ViewController *strongSelf = weakSelf; 5 [strongSelf startCounting]; 6 } 7 repeats:YES];
NSTimer
对类的保留,从而打破了循环引用的产生。__strong ViewController *strongSelf = weakSelf
主要是为了防止执行块的代码时,类被释放了。在类的dealloc
方法中,记得调用[_timer invalidate]
。今天先到此为止,改天继续讲解@property与Ivar等区别!!!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异