前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >关于 循环引用问题

关于 循环引用问题

作者头像
honey缘木鱼
发布2019-12-20 10:51:23
3.2K0
发布2019-12-20 10:51:23
举报
文章被收录于专栏:娱乐心理测试娱乐心理测试

一.概述

多个对象相互持有,A对象强引用B对象,同时B对象也强引用于A对象,两者相互等待对方发消息告诉自己需要Release,一直等待,形成闭环,内存无法释放,导致内存泄露。

iOS内存中的分区有:堆、栈、静态区。其中,栈和静态区是操作系统自己管理回收,不会造成循环引用。所以我们只需要关注堆的内存分配,循环引用会导致堆里的内存无法正常回收。 栈区:由编译器自动分配释放, 存放函数的参数值, 局部变量的值等。 堆区:一般由程序员分配释放,存放new,alloc等关键字创造的对象。

二.产生及解决方法

1.Block

首先我们要先了解block为什么要用copy修饰?

官方文档

block是一个对象,在创建时内存默认分配在栈上,不是堆上,所以它的作用域仅限创建时候的当前上下文(函数, 方法...), 当我们在该作用域外调用该block时, 程序就会崩溃. 所以当我们需要在block定义域以外的地方使用时就需要用到Copy,将block从内存栈区移到堆区。

Block引起循环引用的几种场景及解决方案? (1).@property修饰的属性

代码语言:javascript
复制
@interface Page1ViewController ()
@property(copy,nonatomic)dispatch_block_t block;
@property(nonatomic,strong)NSString *str;
@end

@implementation Page1ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.str = @"123";
        self.block = ^{
           NSLog(@"%@**********",self.str);
       };
}

self将block作为自己的属性变量,持有block对象,而在堆中的block的方法体里面又引用了 self ,就会导致循环引用。

Xcode也很人性化的提示:Capturing 'self' strongly in this block is likely to lead to a retain cycle,由此我们也会注意到自己这里编写不规范。 解决方法:

代码语言:javascript
复制
@property(copy,nonatomic)dispatch_block_t block;
@property(nonatomic,strong)NSString *str;
@end

@implementation Page1ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.str = @"123";
     __weak typeof(self) weakself = self;
        self.block = ^{
           NSLog(@"%@**********",weakself.str);
       };
}

当两个对象相互强引用对方时,我们需要把其中一方变为弱引用,这里我们把self利用__weak变成了弱引用,解决了这种循环引用的问题!

(2).不用@property修饰的私有属性或全局变量

代码语言:javascript
复制
@interface Page1ViewController ()
@property(copy,nonatomic)dispatch_block_t block;
@end

@implementation Page1ViewController
{
 NSString *_str;//全局变量
}
- (void)viewDidLoad {
 [super viewDidLoad];
 _str = @"123";
     self.block = ^{
        NSLog(@"%@**********",_str);
    };
}

编译器报警告:

代码语言:javascript
复制
Block implicitly retains 'self'; explicitly mention 'self' to indicate this is intended behavior
Insert 'self->'                                                                     
Capturing 'self' strongly in this block is likely to lead to a retain cycle

编译器建议我们用self->去修饰,然后self用weakself来代替即为:weakself->时,如下图报错:Dereferencing a __weak pointer is not allowed due to possible null value caused by race condition, assign it to strong variable first。

报错

根据提示需要使用一个strong类型,因此在Block里头使用__strong转一下weakSelf即可:

代码语言:javascript
复制
- (void)viewDidLoad {
    [super viewDidLoad];
    _str = @"123";
      __weak typeof(self) weakself = self;
        self.block = ^{
            __strong typeof(weakSelf) strongSelf = weakSelf;
            NSLog(@"%@**********",strongSelf->_str);
       };
}

Block使用 __weak可能会引起内存提前释放的问题?

代码语言:javascript
复制
- (void)viewDidLoad {
    [super viewDidLoad];
    _str = @"123";
      __weak typeof(self) weakself = self;
        self.block = ^{
           dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    NSLog(@"%@", weakself.str);
             });
       };
}

在上面代码中,在5s内的时间,该控制器pop到上一级,该控制器执行dealloc方法被销毁,内存被提前释放,从而weakself.str即为null。

解决办法( __strong):

代码语言:javascript
复制
- (void)viewDidLoad {
    [super viewDidLoad];
    _str = @"123";
      __weak typeof(self) weakself = self;
        self.block = ^{
             __strong typeof(self) strongSelf = weakself;
           dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    NSLog(@"%@", strongSelf.str);
             });
       };
}

原理:用__weak来解决循环引用问题,block内部的strongSelf仅仅是个局部变量,存在栈中,会在block执行结束之后回收,不会再造成循环引用,并且会使页面返回上一级时,不执行dealloc方法,直到block执行完,控制器执行dealloc方法,内存释放! 注:每次写很复杂,可以定义宏全局

代码语言:javascript
复制
   // weak obj
    /#define WEAK_OBJ(type)  __weak typeof(type) weak##type = type;
    // strong obj
    /#define STRONG_OBJ(type)  __strong typeof(type) str##type = weak##type;

2. Delegate

如果你知道Delegate为什么用weak修饰不用strong,也就明白了它为什么能造成循环引用,也能更好的避免发生此问题。

代码语言:javascript
复制
@interface TestViewController : UIViewController
 @property (nonatomic, strong) id  delegate;
@end

@implementation Page1ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    TestViewController *testVC = [[TestViewController alloc]init];
     testVC.delegate = self;
}

页面Page1强引用了Test页面,Test的delegate属性指向Page1,因为delegate是用strong修饰的,所以Test也强引用了Page1,造成循环引用,要想打破循环引用,要像上面block一样,一方变为弱引用,所以修饰delegate要用weak不能用strong。

3. NSTimer

因为NSTimer 的 target 对传入的参数都是强引用,所以当类具有NSTimer类型的成员变量,并且需要反复执行计时任务时容易造成循环引用。

代码语言:javascript
复制
@implementation Page1ViewController{
    NSTimer *_timer;
}
- (void)viewDidLoad {
    [super viewDidLoad];
  _timer = [NSTimer scheduledTimerWithTimeInterval:5.0
                                    target:self
                                 selector:@selector(addCount) userInfo:nil
                                      repeats:YES];
}

因为控制器强引用了_timer, _timer 的 target 对传入的参数self也是强引用,相互持有,形成闭环。 解决方法(手动释放):

代码语言:javascript
复制
[_timer invalidate];
 _timer = nil;

注意:有人把销毁_timer的方法放在dealloc里,感觉就是自我安慰,循环引用造成不调用dealloc方法,放在viewDidDisappear中又限制太死,最好的方法为(NSTimer的类别):

代码语言:javascript
复制
@interface NSTimer (EXBlock)
+ (NSTimer *)ex_scheduledTimeWithTimeInterval:(NSTimeInterval)interval
  block:(void(^)())block
repeats:(BOOL)repeats;
@end

@implementation NSTimer (EXBlock)
+ (NSTimer *)ex_scheduledTimeWithTimeInterval:(NSTimeInterval)interval
  block:(void(^)())block
                                      repeats:(BOOL)repeats{
    return [self scheduledTimerWithTimeInterval:interval
                               target:self
                            selector:@selector(ex_blockInvoke:)
                                userInfo:[block copy]
                                repeats:repeats];
}
 + (void)ex_blockInvoke:(NSTimer *)timer{
     void(^block)() = timer.userInfo;
   if (block) {
        block();
    }
 }
@end

使用这个类别的方式如下:

代码语言:javascript
复制
- (void)viewDidLoad {
    [super viewDidLoad];
 __weak Page1ViewController * weakSelf = self;
_timer =   [NSTimer ex_scheduledTimeWithTimeInterval:4.0f
                                       block:^{
                                           Page1ViewController * strongSelf = weakSelf;
                                           [strongSelf addCount];
                                       }
                                     repeats:YES];
}

-(void)dealloc
{
    [_timer invalidate];
}

原理:在NSTimer类别定义的类方法中,有一个类型为块的参数(定义的块位于栈上,为了防止块被释放,需要调用copy方法,将块移到堆上),__strong ViewController *strongSelf = weakSelf主要是为了防止执行块的代码时,类被提前释放了。

三.检测循环引用造成的内存泄漏

我们在编写项目时,并不是所有的循环引用编译器都会提示,所以在做完项目后,我们还需要检测项目中是否有内存泄漏的情况,以下是几种检测方法。

1.Analyze静态分析

打开product--->Analyze,项目会自动运行,工具对代码直接进行分析根据代码的上下文的语法结构, 让编译器分析内存情况, 检查是否有内存泄露。 Analyze主要分析以下四种问题: 1、逻辑错误:访问空指针或未初始化的变量等; 2、内存管理错误:如内存泄漏等; 3、声明错误:从未使用过的变量; 4、Api调用错误:未包含使用的库和框架。

缺点: 静态内存分析由于是编译器根据代码进行的判断, 做出的判断不一定会准确,不能把所有的内存泄露查出来,有的内存泄露是在运行时,用户操作时才产生的。

2.Instruments中的Leak动态分析

product->profile ->leaks 打开工具主窗口,手动运行检测:

内存泄漏

使用Leak动态分析,我们可以快速定位到内存泄漏的代码,方便我们检测!

3.第三方工具MLeaksFinder

优点:可以自动在 App 运行过程检测到内存泄露的对象并立即提醒,无需打开额外的工具,无需添加任何业务逻辑代码,而且只在 debug 下开启,完全不影响你的 release 包。

原理:MLeaksFinder是从UIViewController入手的,UIViewController在POP或dismiss之后该控制器及其上的view,view的subviews都会被释放掉,MleaksFinder就是在控制器POP或dismiss之后去查看该控制器和其上的view是否都被释放掉。

使用:使用pods或者下载导入项目,运行,通过提示框和控制器台打印来提示哪里有内存泄漏的问题。

内存泄漏提示

4.自定义检测工具

需求:检测UIViewController 是否发生内存泄漏。 思路:我们需要检测控制器对象在POP后是否还存活,存活则表示有内存泄漏。 原理:利用dispatch_after的延时处理事物的方式,当页面被POP后,延迟事件还能响应,则判断控制器未被释放,有内存泄漏。 做法:分类+runtime (1).给NSObject添加一个分类:实现方法交换

代码语言:javascript
复制
+(void)swizzleSEL:(SEL)originSEL withSEL:(SEL)swizzlingSEL{
    Class class = [self class];
    Method originMethod = class_getInstanceMethod(class, originSEL);
    Method currentMethod = class_getInstanceMethod(class, swizzlingSEL);
    BOOL didAddMethod = class_addMethod(class, originSEL, method_getImplementation(currentMethod), method_getTypeEncoding(currentMethod));
    //这种判断方式使代码更健壮 防止currentMethod 方法未实现
    if(didAddMethod){
        class_replaceMethod(class, swizzlingSEL, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
    }else
    {
        method_exchangeImplementations(originMethod, currentMethod);
    }  
}

(2).给UIViewController添加一个分类 在 + (void)load 通过swizzleSEL 实现 viewWillAppear和viewDidDisAppear 和新方法的交换,并在viewWillAppear方法绑定一个标志,NO则表示Push,YES则表示Pop,当标志为YES时,则实现延迟方法。

代码语言:javascript
复制
+(void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleSEL:@selector(viewWillAppear:) withSEL:@selector(dt_viewWillAppear:)];
        [self swizzleSEL:@selector(viewDidDisappear:) withSEL:@selector(dt_viewDidDisAppear:)];
    });
}

-(void)dt_viewWillAppear:(BOOL)animate{
    [self dt_viewWillAppear:animate];
    objc_setAssociatedObject(self, @"VCFLAG", @(NO), OBJC_ASSOCIATION_ASSIGN);
}
-(void)dt_viewDidDisAppear:(BOOL)animate{
    [self dt_viewDidDisAppear:animate];
    if([objc_getAssociatedObject(self, @"VCFLAG") boolValue]){
      __weak typeof(self) weakself = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(6*NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong typeof(weakself) strongself = weakself;
        NSLog(@"发生内存泄漏的控制器------->%@",strongself);
    });
    }
}

(3).给UINavigationController添加一个分类 在 load方法中实现popViewControllerAnimated和新方法的交换,并在新方法中赋值标志为YES,让其触发延迟事件。

代码语言:javascript
复制
+(void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleSEL:@selector(popViewControllerAnimated:) withSEL:@selector(dt_popViewControllerAnimated:)];
    });
}
-(UIViewController*)dt_popViewControllerAnimated:(BOOL)animated{
    UIViewController *popVC = [self dt_popViewControllerAnimated:animated];
    objc_setAssociatedObject(popVC, @"VCFLAG", @(YES), OBJC_ASSOCIATION_ASSIGN);
    return popVC;
}

(4).测试,在控制内写一个循环引用问题,如下图:

四.总结

反思自己在开发中,很多知识点总是会用,却不懂原理,没有系统的学习研究,几年的开发,仍然处在业务层,要多学习整理底层原理,才会对代码有更清晰的认识!

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一.概述
  • 二.产生及解决方法
    • 1.Block
      • 2. Delegate
        • 3. NSTimer
        • 三.检测循环引用造成的内存泄漏
          • 1.Analyze静态分析
            • 2.Instruments中的Leak动态分析
              • 3.第三方工具MLeaksFinder
                • 4.自定义检测工具
                • 四.总结
                相关产品与服务
                检测工具
                域名服务检测工具(Detection Tools)提供了全面的智能化域名诊断,包括Whois、DNS生效等特性检测,同时提供SSL证书相关特性检测,保障您的域名和网站健康。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档