好多场景会导致循环引用,例如使用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. Diocp截图

    跑了个数据库的查询和插入,删除.     Http SVR DEMO http://123.232.98.202:8081/

  2. 转载:安装Ubuntu 15.10后要做的事

    转载:安装Ubuntu 15.10后要做的事 原文转载于:http://blog.csdn.net/skykingf/article/details/45267517 Ubuntu 15.10发布了, ...

  3. Python之获取微信好友信息

    save_info.py: #!/usr/bin/python # -*- coding: UTF-8 -*- import itchat import pickle itchat.auto_logi ...

  4. Android WiFi 获取国家码

    记录一下Android获取国家码的方式 Wifi 国家码获取途径 1.DefaultCountryTablefield in WCNSS_qcom_wlan_nv.bin-read during dr ...

  5. 基于【CentOS-7+ Ambari 2.7.0 + HDP 3.0】搭建HAWQ数据仓库 —— MariaDB 安装配置

    一.安装并使用MariaDB作为Ambari.Hive.Hue的存储数据库. yum install mariadb-server mariadb 启动.查看状态,检查mariadb是否成功安装 sy ...

  6. css文件的MIME错误引发的Jquery Mobile绘制错误

    静态文件serve设置的MIME不对,引起的浏览器警告 Resource interpreted as Stylesheet but transferred with MIME type applic ...

  7. Rest风格理解

    之前一直不理解restful风格,今天终于理解了些(20170527) 正常我们在浏览器的地址栏中输入的地址很多都是发起的,发起的都是get请求 通过ajax可以设置put请求,F12查看浏览器请求头 ...

  8. flume 1.8 安装部署

    环境 centos:7.2 JDK:1.8 Flume:1.8 一.Flume 安装 1)        下载 wget http://mirrors.tuna.tsinghua.edu.cn/apa ...

  9. 使用Python管理压缩包

    一. 使用tarfile库读取与创建tar包 1. 创建tar包 In [1]: import tarfile In [2]: with tarfile.open('demo.tar',mode='w ...

  10. 数据结构与算法——基数排序简单Java实现

    基数排序(radix sort)又称“桶子法”,在对多个正整数进行排序时可以使用.它的灵感来自于队列(Queue),它最独特的地方在于利用了数字的有穷性(阿拉伯数字只有0到9的10个). 基数排序使用 ...