循环引用问题,你会了吗?原创
金蝶云社区-honey缘木鱼
honey缘木鱼
12人赞赏了该文章 305次浏览 未经作者许可,禁止转载编辑于2019年12月07日 18:29:55


一.概述


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


iOS内存中的分区有:堆、栈、静态区。其中,栈和静态区是操作系统自己管理回收,不会造成循环引用。所以我们只需要关注堆的内存分配,循环引用会导致堆里的内存无法正常回收。


栈区:由编译器自动分配释放, 存放函数的参数值, 局部变量的值等。

堆区:一般由程序员分配释放,存放new,alloc等关键字创造的对象。


二.产生及解决方法

1.Block

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

屏幕快照 2019-12-06 下午4.28.52.png


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

所以当我们需要在block定义域以外的地方使用时就需要用到Copy,将block从内存栈区移到堆区。


Block引起循环引用的几种场景及解决方案?

(1).@property修饰的属性


@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,由此我们也会注意到自己这里编写不规范。

解决方法:


@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修饰的私有属性或全局变量

       

@interface Page1ViewController ()

@property(copy,nonatomic)dispatch_block_t block;

@end


@implementation Page1ViewController

{

    NSString *_str;//全局变量

}

- (void)viewDidLoad {

    [super viewDidLoad];

    _str = @"123";

        self.block = ^{

           NSLog(@"%@**********",_str);

       };

}


编译器报警告:


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。


屏幕快照 2019-12-06 下午5.21.53.png



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


- (void)viewDidLoad {

    [super viewDidLoad];

    _str = @"123";

      __weak typeof(self) weakself = self;

        self.block = ^{

            __strong typeof(weakSelf) strongSelf = weakSelf;

            NSLog(@"%@**********",strongSelf->_str);

       };

}


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


- (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)


- (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方法,内存释放!

注:每次写很复杂,可以定义宏全局


   // 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,也就明白了它为什么能造成循环引用,也能更好的避免发生此问题。


@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类型的成员变量,并且需要反复执行计时任务时容易造成循环引用。


@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也是强引用,相互持有,形成闭环。

解决方法(手动释放):


[_timer invalidate];

 _timer = nil;


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



@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


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


- (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动态分析


2.Instruments中的Leak动态分析


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

1575687720625.jpg


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


3.第三方工具MLeaksFinder


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


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

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


屏幕快照 2019-12-07 上午11.28.08.png


4.自定义检测工具


需求:检测UIViewController 是否发生内存泄漏。  

思路:我们需要检测控制器对象在POP后是否还存活,存活则表示有内存泄漏。

原理:利用dispatch_after的延时处理事物的方式,当页面被POP后,延迟事件还能响应,则判断控制器未被释放,有内存泄漏。

做法:分类+runtime

(1).给NSObject添加一个分类:实现方法交换


+(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时,则实现延迟方法。


+(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,让其触发延迟事件。


+(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).测试,在控制内写一个循环引用问题,如下图:

屏幕快照 2019-12-07 下午4.57.37.png



四.总结


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


赞 12