好多场景会导致循环引用,例如使用Block、线程、委托、通知、观察者都可能会导致循环引用。

1、委托

遵守一个规则,委托方持有代理方的强引用,代理方持有委托方的弱引用。

实际场景中,委托方会是一个控制器对象,代理方可能是一个封装着网络请求并获取数据的对象。

例如:ViewController中需从网络中获取数据,让后展示到列表当中,从网络获取的类是 DataUpdateOp

//ViewController.m
- (IBAction )onRefreshClicked:(id)sender {
//场景获数据的操作对象
self.updateOp = [DataUpdateOp new];
[self.updateOp startUsingDelegate:self withSelector:@selector(onDataAvailable:)];
} - (void)onDataAvailable:(NSArray *)records {
//任务完成时,将操作对象置nil
self.updateOp = nil;
}
//如果控制器 delloc 则取消操作
- (void)delloc {
//取消
if(slef.updateOp !=nil){
[self.updateOp cancel];
}
} //DataUpdateOp.h
@protocol DataUpdateOpDeleate<NSObject>
- (void)onDataAvailable:(NSArray *)records;
@end @interface DataUpdateOp
@property (nonatomic, weak)id <DataUpdateOpDeleate> delegate; - (void)startUpdate;
- (void)cancel;
@end
//DataUpdateOp.m
@implementation DataUpdateOp
- (void)startUpdate {
dispatch_async{
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, ), ^{
//执行网络请求后获取到结果
NSArray *records = ...
dispatch_async(dispatch_get_main_queue(),^{
//尝试获取委托对象的强引用
id<DataUpdateOpDeleate> delegate = self.delegate;
if (!delegate){
return;
}else{//判断原始对象仍然存在吗?
//回传数据
[delegate onDataAvailable:records];
}
})
};
});
} //显示的要求废弃回调对象
- (void)cancel {
//取消执行中的网络请求
self.delegate = nil;
}

当然,大多数情况下,很多人愿意用block 回传网络请求数据,像对AFNetworking做一个简单的二次封装。

这里只是将一下如果用代理的话,应该如何避免循环引用。而且做了验证控制器对象在没有被回收的时候才做响应的操作。

实际场景中,因为网络请求的封装不尽相同,可能会更复杂。

2、Block

block捕获外部变量(一般是控制器本身或者控制器的属性)会导致循环引用

-(void)someMethod {
SomeViewController *vc = [[SomeViewController alloc] init];
  [self presentViewController:vc animated:YES
completion:^{
self.data = vc.data;
[self dismissViewControllerAnimated:YES completion:nil];
}];
}

这时候引起了循环引用,present vc之后,vc被展示出来,子视图一致存在,在completion块中,有引用了self,也就是父控制器。这时父控制器子控制器都在内存当中,如果子控制器里面做了耗时操作,耗内存的操作,可能会导致内存不足。

解决方法: 使用 'weak strong dance' 技术

-(void)someMethod {
SomeViewController *vc = [[SomeViewController alloc] init];
__weak typeof(self) weakSelf = self; //弱引用self 方便被 completion捕获
[self presentViewController:vc animated:YES
completion:^{
typeof(self) theSelf = weakSelf; //通过一弱引用获取一个强引用
if(theSelf != nil) { //只在控制器 不为nil的时候才继续
theSelf.data = vc.data;
[theSelf dismissViewControllerAnimated:YES completion:nil];
}
}];
}

3、线程与计时器

不正确是使用 NSThread 和 NSTimer对象也可能导致循环引用

运行异步操作的典型步骤:

1、如果没有编写更高级的代码来管理自定义的队列,则在全局队列上使用 dispatch_async方法。

2、在需要的时间和地点用NSThread开启异步执行。

3、使用NSTimer周期性的执行一短代码

错误示例:

@implementation SomeViewController
- (void)startPollingTask {
self.timer = [NSTimer scheduledTimerWithTimeInterval: target:self
selector:@selector(updateTask:) userInfo:nil repeats:YES];
} - (void)updateTask:(NSTimer *)timer {
//...
} - (void)delloc {
[self.timer invalidated];
} @end

以上代码:对象持有了计时器,同时计时器也持有了对象,运行循环也持有了计时器,直到计时器的invalidate方法被调用。

这就造成对计时器对象的附加引用,即使代码中没有显示的引用关系。这仍然会导致循环引用。

实际上:NSTimer对象导致了被运行时持有的间接引用,这些引用是强引用,而且目标的引用计数器会以2(而不是1)增长。必须对计时器调用 inivalidatae方法,移除引用。

如果以上代码中,控制器被创建多次,那么控制器是不会被销毁的。会造成严重的内存泄漏。

如果使用了NSThread,也同样会发生这样的问题。

解决办法:

1、主动调用invalidated,

2、将代码分离到多个类中。

首先,不要指望delloc方法会被调用,因为一旦和控制器发生循环引用,那么delloc方法永远不会被调用。delloc()中的 [self.timer invalidated];永远不会被执行。

因为运行循环会跟踪活跃的计时器对象和线程对象,所以在代码找那个设置为nil并不能销毁对象。想要解决这个问题,可以创建一个自定义的方法,以更加明确的方式执行清理操作。

在一个视图控制器中,调用这个清理方法的最佳时机是用户离开视图控制器的时候,这个时机既可以是点击返回按钮,也可可以是其他类似的行为(类直到此事发生的地方),我们可以定义一个cleanUp()方法.

@implementation SomeViewController
- (void)startPollingTask {
self.timer = [NSTimer scheduledTimerWithTimeInterval: target:self
selector:@selector(updateTask:) userInfo:nil repeats:YES];
} - (void)updateTask:(NSTimer *)timer {
//...
} - (void)delloc {
[self.timer invalidate];
} @end

上面的这种写法不能清除timer

3.1清理Timer的方案两种方法:

1、方法一,在用户离开当前视图控制器的时候清理timer

//当视图控制器进入或者离开视图控制器时,调用 该方法
- (void)didMoveToParentViewController:(UIViewController *)parent {
//如果是离开父控制器, if中判断为YES, 才执行 cleanUp
if (parent == nil)
{
[self cleanUp];
}
} - (void)cleanUp {
[self.timer invalidate];
} //2、方法二 通过拦截返回按钮 执行清理
- (id)init {
if (self = [super init])
{
self.navigationItem.backBarButtonItem.target = self;
self.navigationItem.backBarButtonItem.action = @selector(backButtonPressDetected:);
}
return sel;
} - (void)backButtonPressDetected:(id)sender {
[self cleanUp];
[self.navigationController popViewControllerAnimated:YES];
}
 

3.2 方案二 将持有关系分散到多个类中---任务类执行具体动作,所有者类调用任务

优点1、清理器有良好的职责持有者
优点2、需要时任务可以被多个持有者重复使用
具体:控制器只负责展示数据, 新建一个类NewFeedUpdateTask,周期性的执行任务,检查填充视图控制器的最新的数据

//NewFeedUpdateTask.h
@interface NewFeedUpdateTask
@property (nonatomic, weak) id target;//target是弱引用,target会在这里实例化任务,并持有它
@property (nonatomic, assign) SEL selector; @end //NewFeedUpdateTask.m
@implementation NewFeedUpdateTask
//推荐使用的构造方法 外部最好不要用哪个alloc init了
- (void)initWithTimerInterval:(NSTimerInterval )interval target:(id)target selector:(SEL)selector{
if (sellf = [super init])
{
self.target = target;
self.selector = selector;
self.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(fetchAndUpdate:) userInfo:nil repeats:YES];
}
return self;
} //周期性执行的任务
- (void)fetchAndUpdate:(NSTimer *)timer {
//检索feed
NewsFeed *feed = [self getFromServerAndCreateModel];
//用weak修饰,确保,使用异步块的时候不会造成循环引用
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(),^{
__strong typeof(self) strongSlef = weakSelf;
if (!strongSlef){
return;
}
if (strongSlef.target == nil){
return;
} /**
strongSlef.target 和strongSlef.selector 是控制器传过来的,也就是可能有不同的控制器创建本对象,进而初始化 target 和 selector
使用本地变量 target 和 selector 有一个好处:
避免了在以下执行序列中发生竞争的情况
1】在某一个线程A中调用 [target responsToSelector:selector];
2】在线程B中修改 target 或者 selector
3】在线程A中调用[target performSelector:selector withObject:feed];
有了这个代码,即使 target 或者 selector 此刻已经发生了变化,performSelector 仍然会被正确的 target 和 selecctor所调用
**/
id target = strongSlef.target;
SEL selector = strongSlef.selector; if ([target respondsToSelector:selector]){
[target performSelector:selector withObject:feed];;
}
});
} - (void)shutDown {
[self.timer invalidate];
self.timer = nil;
} //viewController.m
@implementation viewController
- (void)viewDidLoad {
//初始化 定时执行任务的对象 ,内部会触发计时器
self.updateTask = [NewFeedUpdateTask initWithTimerInterval: target:self selector:@selector(updateUsingFeed:)]; } //是 NewFeedUpdateTask 对象周期性的回调方法。
- (void)updateUsingFeed:(NewsFeed *)feed {
//根据返回的数据 feed 更新ui
} //调用 任务对象的 shutDown方法,其内部会销毁定时器
- (void)delloc {
[self.updateTask shutDown];
}
@end

从使用方面来看,viewController 持有了 NewFeedUpdateTask对象, 控制器没有被除了父控制器之外的对象所持有。
因此,当用户离开页面时,也就是点击了返回按钮时,引用计数器会被降为0,视图控制器会被销毁。这反过来会导致跟新任务停止。
进而导致计时器会被设定为无效,从而触发关联对象包括(timer 和 updateTask )的析构。

注意

当使用 NSTimer 和 NSThread 时,总应该通过间接的层实现明确的销毁过程。这个间接层应该使用弱引用,从而保证所有的对象能够在停止使用后执行销毁动作,

 

iOS循环引用常见场景和解决办法的更多相关文章

  1. Android 常见异常及解决办法

    Ø  前言 本文主要记录 Android 的常见异常及解决办法,以备以后遇到相同问题时可以快速解决. 1.   java.lang.NullPointerException: Attempt to i ...

  2. Python3 Selenium定位不到元素常见原因及解决办法

    Python3 Selenium定位不到元素常见原因及解决办法 一.问题描述 在做web应用的自动化测试时,定位元素是必不可少的,这个过程经常会碰到定位不到元素的情况: 报错信息: no such e ...

  3. MVC MVC常见错误及解决办法

    MVC常见错误及解决办法 问题1: 必须添加对程序集“EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c5 ...

  4. 关于hasNextInt判断后无限循环输出else项的解决办法

    话不多说,上来就是干! import java.util.Scanner; public class Test_hasNextInt { /** * @param args */ public sta ...

  5. github常见操作和常见错误及其解决办法

    一.常见操作 1. 使用git在本地创建一个项目的过程 $ makdir ~/hello-world //创建一个项目hello-world $ cd ~/hello-world //打开这个项目 $ ...

  6. iOS循环引用

    iOS循环引用 当前类的闭包/Block属性,用到了当前类,就会造成循环引用 此闭包/Block应该是当前类的属性,我们经常对Block进行copy,copy到堆中,以便后用. 单方向引用是不会产生循 ...

  7. Connection reset by peer的常见原因及解决办法 RST 大文件上传

    Connection reset by peer的常见原因及解决办法 Connection reset by peer的常见原因 - 简书 https://www.jianshu.com/p/263e ...

  8. Docker Hadoop 配置常见错误及解决办法

    Docker Hadoop 配置常见错误及解决办法 问题1:wordcount运行卡住,hadoop 任务运行到running job就卡住了 INFO mapreduce.Job: Running ...

  9. Ubuntu下Linux配置内核各种常见错误和解决办法

    镜像下载.域名解析.时间同步请点击阿里云开源镜像站 这篇把Ubuntu下Linux配置内核各种常见错误和解决办法给大家讲解一下,希望可以帮助到大家. 一.Ubuntu系统中缺少各种依赖包导致的问题 1 ...

随机推荐

  1. mysqlcheck与myisamchk的区别

    mysqlcheck和myisamchk都可以用来检测和修复表.(主要是MyISAM表)但是也有以下不同点:1.都可以检查.分析和修复myisam表.但是mysqlcheck也可以检查.分析innod ...

  2. keras 文本分类 LSTM

    首先,对需要导入的库进行导入,读入数据后,用jieba来进行中文分词 # encoding: utf-8 #载入接下来分析用的库 import pandas as pd import numpy as ...

  3. 借着Python-3来聊聊utf-8字符集

    [关于文本文件] 文本文件也是以二进制序列的方式保存在磁盘中的,磁盘并不能保存文本:我们打开文本文件的时候之所以能看到文字,是因为 软件根据文件所用编码的字符集对文件进行解码的原因. [以utf-8字 ...

  4. 【转】SAP BW 顾问靠手 — SAP中的例程

    什么是例程(Routine)? 例程就是我们可以自己定义的程序代码.通过程序代码来完成我们的需求,因为业务是千变万化,如果想让产品能跟随上业务的脚步,就必须要有非常灵活的功能来补充.大家都知道软件产品 ...

  5. MXNET:权重衰减-gluon实现

    构建数据集 # -*- coding: utf-8 -*- from mxnet import init from mxnet import ndarray as nd from mxnet.gluo ...

  6. Android 录音和播放

    今天工作上需要做一个一边录音一边播放的功能,大致原因是有一个外部设备输入音频到我们机器,然后我们机器需要马上把音频播放出来.所以了解了一些有关录音和播放的知识.接到这个任务的第一反应就是看看Andro ...

  7. Redis 的 5 种数据结构

    1.string 可以是字符串,整数或者浮点数,对整个字符串或者字符串中的一部分执行操作,对整个整数或者浮点执行自增(increment)或者自减(decrement)操作. 字符串命令: ①get: ...

  8. react 生命周期钩子里不要写逻辑,否则不生效

    react 生命周期钩子里不要写逻辑,否则不生效,要把逻辑写在函数里,然后在钩子里调用函数,否则会出现问题.

  9. Win10系统中VirtualBox桥接时找不到网卡的问题

    1.主机中 点网络连接 ,点 本地网络,右键属性 2.安装 服务 磁盘安装 选择 VirtualBox 安装目录, 找到 目录文件 D:\Users\Oracle\VirtualBox\drivers ...

  10. 【netcore基础】.Net core使用swagger自动生成开发文档

    之前写过一篇 .Net 版本的博客 https://www.cnblogs.com/jhli/p/8317566.html 现在只不过用了 netcore 之后的版本,其实差不多 netcore版本的 ...