美文网首页
关于内存泄露

关于内存泄露

作者: Jack_deng | 来源:发表于2018-01-19 16:53 被阅读0次

    提到内存泄露,很多人就会想到循环引用,再想到没有走dealloc方法,甚至把这三者等同起来。其实这是不对的,下面我用一个例子来说明这个问题。但是我想先问一个问题,我在某个控制器的viewDidLoad 写了这句代码 [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeChange) userInfo:nil repeats:YES];。大家肯定知道这个控制器释放不了了,那大家知道它内存泄露的原因吗?
    其实循环引用只是导致内存泄露的一种情况,虽然也是主要的情况。还有一种是单例或者类单例引起的内存泄露。某个对象的引用计数始终大于等于1的话,那么系统就不会回收它,它就内存泄露了。比如以上情况,NSTimer对象会被加入到当前的RunLoop,而整个程序运行期间,主线程只有一个(类似于单例),而这个timer会强引用它的target,也就是self对象,所以就有 主线程强引用timer对象强引用self。导致self的引用计数始终大于等于1。
    也许这个例子还不好理解,那么我就再举一个单例造成内存泄露的例子。上代码

    #import <Foundation/Foundation.h>
    
    typedef void(^MyBlock)(void);
    
    @interface DXSingleObj : NSObject // DXSingleObj是一个单例
    
    + (instancetype)sharedInstance;
    @property (nonatomic, copy) MyBlock myBlock;
    
    @end
    
    #import "DXSingleObj.h"
    @interface DXSingleObj ()
    
    @end
    
    @implementation DXSingleObj
    
    + (instancetype)sharedInstance
    {
       static dispatch_once_t onceToken;
       static id instance;
       dispatch_once(&onceToken, ^{
           instance = [self new];
       });
       return instance;
    }
    
    - (void)dealloc
    {
       NSLog(@"%s",__FUNCTION__);
    }
    @end
    

    我们在某个控制器的viewDidLoad加上如下代码

    - (void)viewDidLoad {
       [super viewDidLoad];
       
       DXSingleObj *dxObj = [DXSingleObj sharedInstance];
       dxObj.myBlock = ^{
           NSLog(@"%@",self.view);
       };
    }
    

    这种情况下,这个控制器就不会释放了,退出这个控制器,它的dealloc方法也不会走。 引用关系就是 单例强引用block属性强引用self。导致self不能被释放。
    解决方法也很简单,加一个weak,代码如下

    - (void)viewDidLoad {
       [super viewDidLoad];
       __weak DXViewController *weakSelf = self;
       DXSingleObj *dxObj = [DXSingleObj sharedInstance];
       dxObj.myBlock = ^{
           NSLog(@"%@", weakSelf.view);
       };
    }
    

    看起来weak解决内存泄露好像很好用,那么回到上面这句代码,我们把self改为weakSelf,那么它还会内存泄露吗?[NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(timeChange) userInfo:nil repeats:YES];
    其实这里传weakSelf还是没用。到这里是不有点迷糊了?不是说好的weak是弱引用,我传弱引用还不能打断引用链条吗?其实weak和weakSelf不是万能的,要看到底能不能改变这个属性的强弱引用。因为NSTimer内部有一个strong类型的target成员变量,_target = target,我们传weakSelf的话,就是_target = weakTarget。所以weak到底能否生效的话是看weak能否影响这个属性的强弱类型。
    所以循环引用只是内存泄露的子集。判断这个对象是否内存泄露要看它的引用计数,只要引用计数一直大于等于1,就内存泄露了。

    如果控制器的dealloc方法走了,那么是不是说明一定就没有内存泄露了? 其实不是的,控制器的dealloc方法走了,只是说明这个控制器释放了,但不能说明它里面没有内存泄露。请看例子

    // 这是一个普通类的头文件
    #import <Foundation/Foundation.h>
    
    @interface DXAction : NSObject
    - (void)doAction;
    - (void)cancelAction;
    @end
    
    // 再看.m文件
    #import "DXAction.h"
    
    @implementation DXAction
    - (void)doAction
    {
        NSLog(@"doAction---doAction");
        [self performSelector:@selector(doAction) withObject:nil afterDelay:1];
    }
    - (void)cancelAction
    {
        [NSObject cancelPreviousPerformRequestsWithTarget:self];
    }
    - (void)dealloc
    {
        NSLog(@"%s",__func__);
    }
    @end
    
    // 再看控制器的代码
    #import "DXViewController.h"
    #import "DXAction.h"
    
    @interface DXViewController ()
    @property (nonatomic, strong) DXAction *action;
    @end
    
    @implementation DXViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        self.view.backgroundColor = [UIColor orangeColor];
    
        DXAction *action = [[DXAction alloc] init];
        [action doAction];
        self.action = action;
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        [self dismissViewControllerAnimated:YES completion:nil];
    }
    - (void)dealloc
    {
        NSLog(@"%s",__FUNCTION__);
    }
    @end
    

    点击进入控制器,然后退出控制器回到上个控制器,看到打印如下:

    doAction---doAction
    doAction---doAction
    -[DXViewController dealloc]
    doAction---doAction
    

    说明控制器销毁了,但是这个DXAction对象还是没有销毁,内存泄露了。先说一下DXAction对象为啥不能销毁,这也是performSelector: withObject: afterDelay:使用不当造成内存泄露的原因, 这个方法会被当前线程(这里是主线程)强引用。除非这个方法执行完,否则self对象会一直被主线程强引用,不能释放。
    我们平时的习惯是看一下控制器的dealloc方法有没有走,走了就觉得这个控制器没有内存泄露了,其实这是不完全可靠的

    下面说一下我们经常用的dispatch_after(现在dispatch_after支持取消啦GCD延时取消),如果这个延时代码写在控制器里面

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
           // 写代码 
        });
    

    如果这个block里面没有self,那么退出界面,这个block还是会照常执行的。原因是因为主线程强引用这个block,直到block执行完才会将它置空。如果block里面有self的话,那么只有等到执行完这个block,那么这个控制器才能释放。控制器的延时释放,可能会有一些问题。
    刚好这个dispatch_after可以借机来说一下weak-strong dance,还是上代码

    
    #import "DXViewController.h"
    
    @interface DXViewController ()
    @property (nonatomic, copy) NSString *testStr;
    @end
    
    @implementation DXViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        self.view.backgroundColor = [UIColor orangeColor];
        self.testStr = @"test";
        __weak DXViewController *weakSelf = self;
        // 这里用dispatch_after的block模拟平时的block循环引用
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSMutableArray *arr = [NSMutableArray array];
            [arr addObject:weakSelf.testStr];
    }
    
    

    我见过很多人说,我一直用weak,没有配合strong一起使用,也从没出现过问题啊。那么,你跑一下这个代码,工程就会崩溃。什么时候用weak大家应该都比较清楚了,那么strong啥时候必须要配合使用,啥时候不用strong其实也没问题呢?
    我先贴一下weak配合strong使用的标准代码吧,这也是苹果文档推荐的

        self.testStr = @"test";
        __weak DXViewController *weakSelf = self; // 1. 先用weakSelf
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            __strong DXViewController *strongSelf = weakSelf; //2. 再用strongSelf
            NSMutableArray *arr = [NSMutableArray array];
            if (strongSelf) { // 3. strongSelf判空
                [arr addObject:strongSelf.testStr];
            }
            else
            {
                NSLog(@"strongSelf为空啦");
            }
        });
    

    跑一下这个工程,进入控制器后立即退出控制器,就会出现如下打印

    -[DXViewController dealloc]
    strongSelf为空啦
    

    __strong DXViewController *strongSelf = weakSelf; 这个strongSelf只是一个局部变量,超过作用域后会释放,所以不会造成内存泄露的问题,而strongSelf又保证了在block执行期间,weakSelf不会中途被释放(前提是进入block时weakSelf就不为空)。但是__strong DXViewController *strongSelf = weakSelf;这句代码起不到使weakSelf不为空的作用,所以还是要对strongSelf判空一下。否则跑上面的代码,去掉strongSelf判断空就会崩溃。这才是完整的weak-strong dance。

    其实如果能保证weakSelf在block执行完以前肯定不会为空的话,那么__strong DXViewController *strongSelf = weakSelf; 这句代码可以不用写的。如果不是对内存管理非常清楚的话,还是建议按上面这样写,只要使用了weak就一定配合strong使用,并且对strongSelf判空。

    结论,那么平时应该如何正确使用weak呢?首先,weak不能乱用(weak比较耗性能),确定会造成block循环引用的情况下,用weak解决,然后用了weak就配合strong一起用,并且做好strongSelf判空。 当前,如果对这个weak的使用一点都不懂的话,看到block就用weak吧🙂。(这种人我还真见过不少)

    最后出2个题,大家也经常遇到的
    1.这段代码写在viewDidLoad中,会有问题吗? 如果造成了控制器释放不了,请写出它的引用链条。

     self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
            NSLog(@"%@--self",self);
        }];
    

    2.DXBlockClass类定义如下,

    #import <Foundation/Foundation.h>
    typedef void(^Block)(void);
    @interface DXBlockClass : NSObject
    
    @property (nonatomic, copy) Block myBlock;
    
    + (void)doBlock:(Block)block;
    - (void)doBlock:(Block)block;
    @end
    
    #import "DXBlockClass.h"
    
    @implementation DXBlockClass
    + (void)doBlock:(Block)block
    {
        if (block) {
            block();
        }
    }
    - (void)doBlock:(Block)block
    {
        _myBlock = block;
        if (block) {
            block();
        }
    }
    @end
    

    在某个控制器的viewDidLoad写在这个代码,需要用weak解循环引用吗?为什么不会造成内存泄露(控制器的dealloc方法会走)?

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        DXBlockClass *blockClass = [[DXBlockClass alloc] init];
        [DXBlockClass doBlock:^{
            NSLog(@"%@-类方法-",self);
        }];
        [blockClass doBlock:^{
            NSLog(@"%@-对象方法-",self);
        }];
    }
    

    假如以前你对啥时候该用weak解内存泄露,啥时候该配合strong一起使用还不是非常了解的话。读完了这篇文章,如果能对weak和strong的使用了然于心的话,那么就请点一个喜欢吧。

    相关文章

      网友评论

          本文标题:关于内存泄露

          本文链接:https://www.haomeiwen.com/subject/mdfaoxtx.html