循环应用的知识整理

这个知识点,是个iOS开发人员从初期都会遇到。之前也看了很多别人的博客,自己想要重新归纳总结一下。
首先,我强烈推荐唐巧博客,作为一个优秀的iOS开发者,他之前也谈过循环引用的问题。

一、循环引用的原理

1、基本知识
首先,得说下内存中和变量有关的分区:堆、栈、静态区。其中,栈和静态区是操作系统自己管理的,对程序员来说相对透明,所以,一般我们只需要关注堆的内存分配,而循环引用的产生,也和其息息相关,即循环引用会导致堆里的内存无法正常回收。说起对内存的回收,肯定得说下以下老生常谈的回收机制:

对堆里面的一个对象发送release消息来使其引用计数减一;
查询引用计数表,将引用计数为0的对象dealloc;
那么循环引用怎么影响这个过程呢?
2、样例分析

  • In some situations you retrieve an object from another object, and then directly or indirectly release the parent object. If releasing the parent causes it to be deallocated, and the parent was the only owner of the child, then the child (heisenObject in the example) will be deallocated at the same time (assuming that it is sent a release rather than an autorelease message in the parent’s dealloc method).

大致意思是,B对象是A对象的属性,若对A发送release消息,致使A引用计数为0,则会dealloc A对象,而在A的dealloc的同时,会向B对象发送release消息,这就是问题的所在。
看一个正常的内存回收,如图1:


图1

接下来,看一个循环引用如何影响内存回收的,如图2:

图2

那么推广开来,我们可以看图2,是不是很像一个有向图,而造成循环引用的根源就是有向图中出现环。但是,千万不要搞错,下面这种,并不是环,如图3:

图3

3、结论
由以上的内容,我们可以得到一个结论,当堆中的引用关系图中,只要出现环,就会造成循环引用。
细心的童鞋肯定还会发现一个问题,即是不是只有A对象和B对象这种关系(B是A的属性)才会出现环呢,且看第二部分的探究:环的产生。

二、环的产生

1、堆内存的持有方式
仔细思考下可以发现,堆内存的持有方式,一共只有两种:
方式a:将一个外部声明的空指针指向一段内存(例如:栈对堆的引用),如图4:

图4

方式b:将一段内存(即已存在的对象)中的某个指针指向一段内存(堆对堆的引用),如图5:

图5

一中所讲的B是A的属性无疑是方式b,除去这种关系,还有几种常见的关系也属于方式b,比如:block对block所截获变量的持有,再比如:容器类NSDictionary,NSArray等对其包含对象的持有。

2、方式a对产生环的影响
如图6:

图6

3、方式b对产生环的影响
如图7:

图7

4、结论
方式b是造成环的根本原因,即堆对堆的引用是产生循环引用的根本原因。
可能有的童鞋可能说,那方式a的指针还有什么用呢?当然是有用的,a的引用和b的引用共同决定了一个对象的引用计数,即,共同决定这个对象何时需要dealloc,如图8:

图8

三.误区

1.就是不是所有循环引用,系统都会提示你的,实际上,大部分的循环引用系统都不会默认提醒你,所以不要以为系统没有提示的时候就是安全的。
2.不是只要block里面含有self,就会导致循环引用

举例:
2.当 block 本身不被 self 持有,而被别的对象持有,同时不产生循环引用的时候,就不需要使用 weak self 了。最常见的代码就是 UIView 的动画代码,我们在使用 UIView 的 animateWithDuration:animations 方法 做动画的时候,并不需要使用 weak self,因为引用持有关系是:

UIView 的某个负责动画的对象持有了 block
block 持有了 self
因为 self 并不持有 block,所以就没有循环引用产生,因为就不需要使用 weak self 了。

[UIView animateWithDuration:0.2 animations:^{
    self.alpha = 1;
}];

当动画结束时,UIView 会结束持有这个 block,如果没有别的对象持有 block 的话,block 对象就会释放掉,从而 block 会释放掉对于 self 的持有。整个内存引用关系被解除。

另一个例子

[self.view mas_makeConstraints:^(MASConstraintMaker *make) {
    make.centerY.equalTo(self.otherView.mas_centerY);
}];

block中持有了self,但是self.view并没有持有这个block,因为看到Masonry的源码是这样的:

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

它仅仅是block(constrainMaker)。如果改成了self.block = block(constrainMaker),那么view也持有了block,这就是为什么开发者使用Masonry进行开发,但是不加weakSelf,strongSelf也不会导致循环引用的原因。

四.日常开发中常遇到的循环引用的例子

例子1

//Student.m
#import <Foundation/Foundation.h>
typedef void(^Study)();
@interface Student : NSObject
@property (copy , nonatomic) NSString *name;
@property (copy , nonatomic) Study study;
@end

//ViewController.m
#import "ViewController.h"
#import "Student.h"

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    Student *student = [[Student alloc]init];
    student.name = @"Hello World";

    student.study = ^{
        NSLog(@"my name is = %@",student.name);
    };
}

这里形成环的原因block里面持有student本身,student本身又持有block。

例子2

#import "ViewController.h"
#import "Student.h"

@interface ViewController ()
@property (copy,nonatomic) NSString *name;
@property (strong, nonatomic) Student *stu;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    Student *student = [[Student alloc]init];

    self.name = @"halfrost";
    self.stu = student;

    student.study = ^{
        NSLog(@"my name is = %@",self.name);
    };

    student.study();
}

这里也会产生循环引用,因为student持有block,block持有self,self持有student,正好三个形成圆环了,所以这也是循环引用,不过你用Instrument的leak会监测不出来的,但是实际上确实循环引用了

例子3

#import "ViewController.h"
#import "Student.h"

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    Student *student = [[Student alloc]init];

    __block Student *stu = student;
    student.name = @"Hello World";
    student.study = ^{
        NSLog(@"my name is = %@",stu.name);
        stu = nil;
    };
}

这里是因为student持有block,block持有_block,_block持有student,依旧是形成环

例子4(解决方案)

#import "ViewController.h"
#import "Student.h"

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    Student *student = [[Student alloc]init];
    student.name = @"Hello World";
    __block Student *stu = student;

    student.study = ^{
        NSLog(@"my name is = %@",stu.name);
        stu = nil;
    };

    student.study();
}

这里不会循环引用,因为student.study()执行后,stu=nil(也就是说_block=nil),也就是说student持有block,block不持有_block了(因为_block = nil后,就正常释放了).这是一种解决循环引用的方法。
点评(使用__block解决循环引用虽然可以控制对象持有时间,在block中还能动态的控制是__block变量的值,可以赋值nil,也可以赋值其他的值,但是有一个唯一的缺点就是需要执行一次block才行。否则还是会造成循环引用。)

例子5(解决方案)

#import "ViewController.h"
#import "Student.h"

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    Student *student = [[Student alloc]init];
    student.name = @"Hello World";
    __weak typeof(student) weakSelf = student;

    student.study = ^{
        NSLog(@"my name is = %@",weakSelf.name);
    };

    student.study();
}

乖乖的使用weakSelf即可,就是让block不持有self,这样就可以避免循环引用

例子6(引用Effective Objective-c 书中的一段代码)

EOCNetworkFetcher.h

typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);

@interface EOCNetworkFetcher : NSObject

@property (nonatomic, strong, readonly) NSURL *url;

- (id)initWithURL:(NSURL *)url;

- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;

@end

EOCNetworkFetcher.m

@interface EOCNetworkFetcher ()

@property (nonatomic, strong, readwrite) NSURL *url;
@property (nonatomic, copy) EOCNetworkFetcherCompletionHandler completionHandler;
@property (nonatomic, strong) NSData *downloadData;

@end

@implementation EOCNetworkFetcher

- (id)initWithURL:(NSURL *)url {
    if(self = [super init]) {
        _url = url;
    }
    return self;
}

- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion {
    self.completionHandler = completion;
    //开始网络请求
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        _downloadData = [[NSData alloc] initWithContentsOfURL:_url];
        dispatch_async(dispatch_get_main_queue(), ^{
             //网络请求完成
            [self p_requestCompleted];
        });
    });
}

- (void)p_requestCompleted {
    if(_completionHandler) {
        _completionHandler(_downloadData);
    }
}

@end

EOCClass.m

@implementation EOCClass {
    EOCNetworkFetcher *_networkFetcher;
    NSData *_fetchedData;
}

- (void)downloadData {
    NSURL *url = [NSURL URLWithString:@"http://www.baidu.com"];
    _networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    [_networkFetcher startWithCompletionHandler:^(NSData *data) {
        _fetchedData = data;
    }];
}
@end

上面的循环引用是
1、completion handler的block因为要设置_fetchedData实例变量的值,所以它必须捕获self变量,也就是说handler块保留了EOCClass实例。(block持有EOCClass)
2、EOCClass实例通过strong实例变量保留了EOCNetworkFetcher,最后EOCNetworkFetcher实例对象也会保留了handler的block。(EOCClass持有block)
所以循环引用了,书本上教了有三种释放的方法

方法一:手动释放EOCNetworkFetcher使用之后持有的_networkFetcher,这样可以打破循环引用
- (void)downloadData {
    NSURL *url = [NSURL URLWithString:@"http://www.baidu.com"];
    _networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    [_networkFetcher startWithCompletionHandler:^(NSData *data) {
        _fetchedData = data;
        _networkFetcher = nil;//加上此行,打破循环引用
    }];
}
//释放对象属性
方法二:直接释放block。因为在使用完对象之后需要人为手动释放,如果忘记释放就会造成循环引用了。如果使用完completion handler之后直接释放block即可。打破循环引用
- (void)p_requestCompleted {
    if(_completionHandler) {
        _completionHandler(_downloadData);
    }
    self.completionHandler = nil;//加上此行,打破循环引用
}
//释放block
方法三:使用weakSelf、strongSelf
- (void)downloadData {
   __weak __typeof(self) weakSelf = self;
   NSURL *url = [NSURL URLWithString:@"http://www.baidu.com"];
   _networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
   [_networkFetcher startWithCompletionHandler:^(NSData *data) {
        __typeof(&*weakSelf) strongSelf = weakSelf;
        if (strongSelf) {
            strongSelf.fetchedData = data;
        }
   }];
}
//使用weakSelf,这样block就不持有对象了.

六.为何要用StrongSelf,用weakSelf不就可以解决循环引用了吗

这个问题,公司的同事也问过我,无奈我原理理解不透彻,和他说strongSelf可以保证block里面的self可以完全执行完block中的所有操作,然后block再完全释放。可是当时同事直接用weakSelf,跑了一次项目,没什么问题,所以他觉得没必要写strongSelf,我也找不到漏洞,只好接受了他们的观点。后面看到下面的例子,才明白为何一定要用StrongSelf

#import "ViewController.h"
#import "Student.h"

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    Student *student = [[Student alloc]init];
    student.name = @"Hello World";
    __weak typeof(student) weakSelf = student;

    student.study = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"my name is = %@",weakSelf.name);
        });
    };

    student.study();
}

输出:

my name is = (null)

重点就在dispatch_after这个函数里面。在study()的block结束之后,student被自动释放了。又由于dispatch_after里面捕获的__weak的student,根据第二章讲过的__weak的实现原理,在原对象释放之后,__weak对象就会变成null,防止野指针。所以就输出了null了。

究其根本原因就是weakSelf之后,无法控制什么时候会被释放,为了保证在block内不会被释放,需要添加__strong。

在block里面使用的__strong修饰的weakSelf是为了在函数生命周期中防止self提前释放。strongSelf是一个自动变量当block执行完毕就会释放自动变量strongSelf不会对self进行一直进行强引用。

所以StrongSelf是在这个时候才发挥作用

#import "ViewController.h"
#import "Student.h"

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    Student *student = [[Student alloc]init];

    student.name = @"Hello World";
    __weak typeof(student) weakSelf = student;

    student.study = ^{
        __strong typeof(student) strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"my name is = %@",strongSelf.name);
        });

    };

    student.study();
}

输出

my name is = Hello World
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容