循环应用的知识整理

2017-01-12  本文已影响26人  李昭宏

这个知识点,是个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
上一篇下一篇

猜你喜欢

热点阅读