• 【iOS】—— 循环引用问题


    一、引用计数

    介绍循环引用问题前,首先我们要简单的介绍一下iOS的内存管理方式引用计数。引用计数是一个简单而有效的管理对象生命周期的方式:

    • 当我们创建一个新对象时,它的引用计数为1。
    • 当有一个新的指针指向这个对象时,我们将引用计数加1。
    • 当某个指针不再指向这个对象时,我们将引用计数减1。
    • 当对象的引用计数为0时,说明这个对象不再被任何指针指向了,就可以将对象销毁,回收内存。
      32423423

    二、循环引用

    引用计数这种管理内存的方式虽然简单,但是有一个比较大的瑕疵,它不能很好的解决循环引用问题。

    对象A和对象B,相互引用了对方作为自己的成员变量,只有当自己销毁时,才会将成员变量的引用计数减1,这就导致了A的销毁依赖于B的销毁,同样B的销毁依赖于A的销毁,这样就造成了循环引用问题。
    5435345
    当然循环引用也为几种:

    • 自循环引用
    • 相互循环引用
    • 多循环引用

    1.自循环引用

    假如有一个对象,内部强持有它的成员变量obj,若此时我们给obj赋值为原对象时,就是自循环引用。
    4234234

    2.相互循环引用

    对象A内部强持有obj,对象B内部强持有obj,若此时对象A的obj指向对象B,同时对象B中的obj指向对象A,就是相互引用。
    345345

    3.多循环引用

    假如类中有对象1…对象N,每个对象中都强持有一个obj,若每个对象的obj都指向下个对象,就产生了多循环引用。
    45345345

    三、常见的循环引用问题及其解决方法

    1.delegate

    例如:我们平时经常用的协议传值,如果我们委托方的delegate属性使用strong强引用,就会造成代理方和委托方互相强引用出现循环引用问题。代理方强引用委托方对象,并且委托方对象中的delegate属性又强引用代理方对象,这就造成了循环引用问题。

    @property (nonatomic, strong) id <MyCustomDelegate> delegate;
    
    • 1

    234234

    解决方法:

    为了解决这个问题,我们只需要将委托方的delegate属性改为weak修饰就行了,这样委托方的delegate就不会强引用代理方对象了,简单解决了这个循环引用问题。

    @property (nonatomic, weak) id <MyCustomDelegate> delegate;
    
    • 1

    324234234

    2.block

    (1)并不是所有block都会产生循环引用,block是否产生循环引用是需要我们去判断的,例如:

    //这样是不会产生循环引用,因为这个block不被self持有,是被UIView的类对象持有,这个block和self没有任何关系,所以可以任意使用self。
    [UIView animateWithDuration:0.0 animations:^{
        [self viewDidLoad];
    }];
    
    • 1
    • 2
    • 3
    • 4

    (2)self -> reachabilityManager -> block -> self,才会产生循环引用,并且Xcode会给出循环引用warning,例如:

    //self -> reachabilityManager -> block -> self 都是循环引用
        self.reachabilityManager.stateBlock = ^(int number){
            NSLog(@"%@",self. reachabilityManager);
        };
    //或者(block内部没有显式地出现"self",只要你在block里用到了self所拥有的东西,一样会出现循环引用!)
        self.reachabilityManager.stateBlock = ^(int number){
            NSLog(@"%@",_ reachabilityManager);
        };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    综合上述来看,要判断block是否造成了循环引用,我们要看block中的引用的变量和block外部引用block的变量会不会形成一个强引用的闭环,以此来判断block是否造成了循环引用的问题。

    解决方法

    解决它其实很简单,无非就是self引用了blockblock又引用了self嘛,让他们其中一个使用weak修饰不就行了:

    __weak __typeof(self) weakSelf = self;
    [self.reachabilityManager setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
        NSLog(@"%@",weakSelf.reachabilityManager);
    }];
    
    • 1
    • 2
    • 3
    • 4

    但是仅仅使用__weak修饰self存在一个缺陷:__weak可能会导致内存提前回收weakSelf,在未执行NSLog()时,weakSelf就已经被释放了,然后执行NSLog()时就打印(null)

    所以为了解决这个缺陷,我们需要这样在block内部再用__strong去修饰weakSelf

    __weak __typeof(self) weakSelf = self;
    [self.reachabilityManager setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
        __strong __typeof(weakSelf) strongSelf = weakSelf;
        NSLog(@"%@",strongSelf.reachabilityManager);
    }];
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这就是我们平时所说的强弱共舞。

    我们发现上述这个方法确实解决所有问题,但是可能会有两个不理解的点:
    即使用weakSelf又使用strongSelf,这么做和直接用self有什么区别?为什么不会有循环引用?这是因为block外部的weakSelf是为了打破环循环引用,而block内部的strongSelf是为了防止weakSelf被提前释放,strongSelf仅仅是block中的局部变量,在block执行结束后被回收,不会再造成循环引用。

    这么做和使用weakSelf有什么区别?唯一的区别就是多了一个strongSelf,而这里的strongSelf会使self的引用计数+1,使得self只有在block执行完,局部的strongSelf被回收后,self才会dealloc

    3.NSTimer

    在使用NSTimer时我们会遇到很多循环引用问题,比如下面一段代码:

    - (void)viewDidLoad {
        [super viewDidLoad];
        self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
    }
    
    - (void)doSomething {
        
    }
    
    - (void)dealloc {
        [self.myTimer invalidate];
        self.myTimer = nil;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这是典型的循环引用,因为myTimer会强引用self,而 self又持有了timer,所有就造成了循环引用。那有人可能会说,我使用一个weak指针,比如:

    __weak typeof(self) weakSelf = self;
        self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
    
    • 1
    • 2

    但是其实并没有用,因为不管是weakSelf还是strongSelf,最终Runloop强引用NSTimer其也就间接的强引用了对象,结果就会导致循环引用。
    234234234
    那怎么解决呢?有两点:

    • (1)让视图控制器对NSTimer的引用变成弱引用
    • (2)让NSTimer对视图控制器的引用变成弱引用

    分析一下两种方法,第一种方法如果控制器对NSTimer的引用改为弱引用,则会出现NSTimer直接被回收,所以不可使,因此我们只能从第二种方法入手。

    主要有如下三种方式:

    3.1 使用中间类

    创建一个继承NSObject的子类MyTimerTarget,并创建开启计时器的方法。

    // MyTimerTarget.h
    #import <Foundation/Foundation.h>
    @interface MyTimerTarget : NSObject
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval target:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats;
    @end
    
    // MyTimerTarget.m   
    #import "MyTimerTarget.h"
    @interface MyTimerTarget ()
    @property (assign, nonatomic) SEL outSelector;
    @property (weak, nonatomic) id outTarget;
    @end
    
    @implementation MyTimerTarget
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval target:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats {
          MyTimerTarget *timerTarget = [[MyTimerTarget alloc] init];
          timerTarget.outTarget = target;
          timerTarget.outSelector = selector;
          NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:interval target:timerTarget selector:@selector(timerSelector:) userInfo:userInfo repeats:repeats];
          return timer;
    }   
    - (void)timerSelector:(NSTimer *)timer {
          if (self.outTarget && [self.outTarget respondsToSelector:self.outSelector]) {
            [self.outTarget performSelector:self.outSelector withObject:timer.userInfo];
          } else {
            [timer invalidate];
          }
    }
    @end
    
    // 调用方 
    @interface ViewController ()
    @property (nonatomic, strong) NSTimer *myTimer;
    @end
    
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.myTimer = [MyTimerTarget scheduledTimerWithTimeInterval:1 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
    }
    
    - (void)doSomething {
        
    }
    
    - (void)dealloc {
        NSLog(@"MyViewController dealloc");
    }
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    VC强引用timer,因为timertargetMyTimerTarget实例,所以timer强引用MyTimerTarget实例,而MyTimerTarget实例弱引用VC,解除循环引用。这种方案VC在退出时都不用管timer,因为自己释放后自然会触发timerSelector:中的[timer invalidate]逻辑,timer也会被释放。

    3.2 使用类方法

    我们还可以对NSTimer做一个category,通过blocktimertargetselector绑定到一个类方法上,来实现解除循环引用:

    // NSTimer+MyUtil.h
    #import <Foundation/Foundation.h>
    @interface NSTimer (MyUtil)
    + (NSTimer*)MyUtil_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)(void))block repeats:(BOOL)repeats;
    @end
    
    // NSTimer+MyUtil.m
    #import "NSTimer+MyUtil.h"
    @implementation NSTimer (MyUtil)
    + (NSTimer *)MyUtil_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)(void))block repeats:(BOOL)repeats {
        return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(MyUtil_blockInvoke:) userInfo:[block copy] repeats:repeats];
    }
    
    + (void)MyUtil_blockInvoke:(NSTimer *)timer {
        void (^block)(void) = timer.userInfo;
          if (block) {
             block();
          }
    }
    @end
    
    //  调用方
    @interface ViewController ()
    @property (nonatomic, strong) NSTimer *myTimer;
    @end
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.myTimer = [NSTimer MyUtil_scheduledTimerWithTimeInterval:1 block:^{
                NSLog(@"doSomething");
          } repeats:YES];
    }
    - (void)dealloc {
        if (_myTimer) {
            [_myTimer invalidate];
        }
        NSLog(@"MyViewController dealloc");
    }
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    这种方案下,VC强引用timer,但是不会被timer强引用,但有个问题是VC退出被释放时,如果要停掉timer需要自己调用一下timerinvalidate方法。

    3.3 使用 weakProxy

    创建一个继承NSProxy的子类MyProxy,并实现消息转发的相关方法。NSProxy是iOS开发中一个消息转发的基类,它不继承自NSObject。因为他也是Foundation框架中的基类,通常用来实现消息转发,我们可以用它来包装NSTimertarget,达到弱引用的效果。

    // MyProxy.h
    #import <Foundation/Foundation.h>
    @interface MyProxy : NSProxy
    + (instancetype)proxyWithTarget:(id)target;
    @end
    
    // MyProxy.m
    #import "MyProxy.h"
    
    @interface MyProxy ()
    @property (weak, readonly, nonatomic) id weakTarget;
    @end
    @implementation MyProxy
    
    + (instancetype)proxyWithTarget:(id)target{
        return [[MyProxy alloc] initWithTarget:target];
    }
    - (instancetype)initWithTarget:(id)target {
        _weakTarget = target;
        return self;
    }
    - (void)forwardInvocation:(NSInvocation*)invocation {
        SEL sel = [invocation selector];
        if (_weakTarget &&[self.weakTarget respondsToSelector:sel]) {
            [invocation invokeWithTarget:self.weakTarget];
        }
    }
    - (NSMethodSignature*)methodSignatureForSelector:(SEL)sel {
        return [self.weakTarget methodSignatureForSelector:sel];
    }
    - (BOOL)respondsToSelector:(SEL)aSelector {
        return [self.weakTarget respondsToSelector:aSelector];
    }
    
    @end
    
    //  调用方
    @interface ViewController ()
    @property (nonatomic, strong) NSTimer *myTimer;
    @end
    
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:[MyProxy proxyWithTarget:self] selector:@selector(doSomething) userInfo:nil repeats:YES];
    }
    - (void)dealloc {
        if (_myTimer) {
            [_myTimer invalidate];
        }
        NSLog(@"MyViewControllerdealloc");
    }
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    上面的代码中,了解一下消息转发的过程就可以知道-forwardInvocation: 是会有一个NSInvocation对象,这个NSInvocation对象保存了这个方法调用的所有信息,包括Selector名,参数和返回值类型,最重要的是有所有参数值,可以从这个NSInvocation对象里拿到调用的所有参数值。这时候我们把转发过来的消息和weakTargetselector信息做对比,然后转发过去即可。

    这里需要注意的是,在调用方的dealloc中一定要调用timerinvalidate方法,因为如果这里不清理timer,这个调用方dealloc被释放后,消息转发就找不到接收方了,就会crash

  • 相关阅读:
    最新MySql安装教学,非常详细
    Webpack 中 Plugin 的作用是什么?常用 plugin 有哪些?
    ICer技能03Design Compile
    第10章 游戏客户洞察案例实战
    性能测试度量指标
    Diffusion Model 相关文章(图像生成方面)
    国际公认—每个领导者必须拥抱的11项领导力转变
    ffmpeg转码视频
    PMP每日一练 | 考试不迷路-11.09(包含敏捷+多选)
    [uni-app] uni.showToast 一闪而过问题/设定时间无效/1秒即逝
  • 原文地址:https://blog.csdn.net/m0_55124878/article/details/125770832