美文网首页
解决Timer或者CADisplayLink的释放问题

解决Timer或者CADisplayLink的释放问题

作者: iLeooooo | 来源:发表于2020-01-17 16:16 被阅读0次

    转载自:饼哥IT

    在swift中,如果使用了Timer或者CADisplayLink,每次在deinit方法里面去停止定时器的时候发现,deinit方法更本就不会走!主要原因:循环引用

    1、三种循环引用模式

    ​ 1)自循环引用:对象的强持有变量指向自身,比如ViewController强持有一个block,在block里又捕获持有ViewController,造成自循环引用;

    ​ 2)相互循环引用:比如定义一个A类和B类,A类有一个B类的属性,B类有一个A类的属性,修饰词都是strong类型,在A类里面访问B类属性和B类里边访问A类属性,那么就会出现相互持有,不会走deinit方法;

    ​ 3)多循环引用:比如类似于三角恋关系,A持有B,B持有C,C又持有A,造成循环引用;

    2、如何解决循环引用

    ​ 1)_ _weak:弱引用,项目中使用最多的方式,无论是使用weak修饰self,还是修饰delegate等,使用广泛。weak指针指向的对象在被废弃之后会被自动置为nil:

    ​ 当weak指针指向的对象被废弃之后,deinit的内部实现当中会调用清除弱引用的一个方法。然后在清除弱引用的方法当中,会通过哈希算法来查找被废弃对象在弱引用表当中的位置,来提取所对应的弱引用指针的列表数组,然后进行for循环遍历,把每一个weak指针都置为nil。

    ​ 2)_ _unsafe_unretained:不安全引用,修饰对象不会增加引用计数,当指针指向的对象被废弃后,指针不会置为nil,成为悬垂指针

    ​ 3)_ _block:在MRC模式下,_ _block修饰对象不会增加引用计数,避免了循环引用;在ARC模式下, _ _block一般用来在block内部修改外部的局部变量。

    @property(nonatomic, copy) void (^testBlock)(void);
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        __weak typeof(self) wself = self;
    //    __unsafe_unretained typeof(self) wself = self;
        self.testBlock = ^{
                //当然,如果不在block内使用self,就不会捕获self,自然就没有循环引用了
            NSLog(@"testBlock = %@", wself); 
        };
        self.testBlock();
    }
    
    - (void)dealloc {
        NSLog(@"ViewController dealloc");
    }
    
    3、解决NSTimer、CADisplayLink内存泄漏问题

    由于NSTimer在项目中经常使用,并且就算使用weak修饰,还是会存在内存泄漏问题,所以单独拿出来用代码解释说明,并提供优雅的解决方案。CADisplayLink也是定时器。

    @interface BViewController ()
    @property(nonatomic, assign) NSInteger num;
    @property(nonatomic, weak) NSTimer *timer; //使用weak修饰也不能解决内存泄漏
    @end
    
    @implementation BViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.num = 0;
        // 默认自动添加到RunLoop,也就是RunLoop持有timer,直到调用invalidate,才移除timer
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerFire:) userInfo:nil repeats:YES];
    }
    
    - (void)dealloc {
        NSLog(@"BViewController dealloc");
        // invalidate:停止定时器,并把timer从RunLoop中移除,并把timer的target强引用去除
        [self.timer invalidate];
        self.timer = nil;
    }
    
    - (void)timerFire:(NSTimer *)timer {
        self.num ++;
        NSLog(@"num = %ld", self.num);
    }
    
    @end
    

    先从一个AViewController跳转至BViewController,点击返回按钮,发现并没有调用dealloc析构函数,为什么?首先BViewController弱持有timer,timer强持有BViewController,当点击返回按钮时,NavigationController不再持有BViewController,当前的RunLoop仍然强持有timer,而timer强持有BViewController,所以BViewController引用计数不为0,自然得不到释放。


    NSTimer强引用ViewController

    其实不管BViewController用strong还是用weak修饰timer,最终timer都会强持有BViewController,造成内存泄漏。现在列出三种解决方案:

    1、使用block回调

    ​使用block回调处理事件,而不是使用target,这样就不会强持有target,但是是iOS 10之后才有的方法。

    @interface BViewController ()
    @property(nonatomic, assign) NSInteger num;
    @property(nonatomic, strong) NSTimer *timer;
    @end
    
    @implementation BViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.num = 0;
        __weak typeof(self) wself = self;
        // 注意:这个方法是iOS 10之后的
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
            [wself timerFire:timer];
        }];
    }
    
    - (void)dealloc {
        NSLog(@"BViewController dealloc");
        // invalidate:停止定时器,并把timer从RunLoop中移除,并把timer的target强引用去除
        [self.timer invalidate];
        self.timer = nil;
    }
    
    - (void)timerFire:(NSTimer *)timer {
        self.num ++;
        NSLog(@"num = %ld", self.num);
    }
    
    @end
    
    2、使用动态消息解析

    ​RunLoop会对timer强引用,timer对BViewController强引用,那么我们可以创建一个中间类来弱引用BViewController,让timer对中间类强引用,方法2和下面的方法3都是基于这个思想做的。


    给NSTimer添加中间件

    创建一个中间类TimerMiddleware:

    // .h文件
    @interface TimerMiddleware : NSObject
    + (instancetype)middlewareWithTarget:(id)target;
    @end
    
    // .m文件
    @interface TimerMiddleware ()
    /**
     这里使用weak,弱引用,才能对BViewController弱引用,才能解决内存泄漏
     如果使用strong,那么还是会内存泄漏
     */
    @property(nonatomic, weak) id target;
    @end
    
    @implementation TimerMiddleware
    + (instancetype)middlewareWithTarget:(id)target {
        TimerMiddleware *middleware = [TimerMiddleware new];
        middleware.target = target;
        return middleware;
    }
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        return self.target;
    }
    
    @end
    

    在BViewController中:

    // 其他代码和上面一样
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.num = 0;
        TimerMiddleware *middleware = [TimerMiddleware middlewareWithTarget:self];
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:middleware selector:@selector(timerFire:) userInfo:nil repeats:YES];
    }
    

    运行结果:BViewController会走dealloc方法,说明解决了内存泄漏问题。

    3、使用NSProxy转发消息

    1、NSProxy是跟NSObject一个级别的基类,用来设计做消息转发的;
    2、NSProxy是抽象类,使用时候我们需要使用其子类;
    3、NSProxy和NSObject都遵循NSObject协议;
    4、NSProxy不会跟NSObject类一样去父类搜索方法实现,会直接进入消息转发流程,所以效率更高。
    先创建一个TimerProxy,继承自NSProxy:

    // .h文件
    @interface TimerProxy : NSProxy
    + (instancetype)proxyWithTarget:(id)target;
    @end
    
    // .m文件
    @interface TimerProxy ()
    @property(nonatomic, weak) id target;
    @end
    
    @implementation TimerProxy
    
    + (instancetype)proxyWithTarget:(id)target {
        TimerProxy *proxy = [TimerProxy alloc]; //注意:没有init方法
        proxy.target = target;
        return proxy;
    }
    
    // NSProxy接收到消息会自动进入到调用这个方法 进入消息转发流程
    - (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
        return [self.target methodSignatureForSelector:sel];
    }
    
    - (void)forwardInvocation:(NSInvocation *)invocation {
        [invocation invokeWithTarget:self.target];
    }
    @end
    

    在BViewController中:

    // 其他代码和上面一样
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.num = 0;
        TimerProxy *proxy = [TimerProxy proxyWithTarget:self];
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:proxy selector:@selector(timerFire:) userInfo:nil repeats:YES];
    }
    

    总结:如果不管iOS系统版本,那么使用block方式简单;如果需要兼容系统版本,那么使用NSProxy更加高效,一次封装,终生受用😄。

    4.Swift版本NSProxy转发消息

    创建WeakProxy类:

    class WeakProxy: NSObject {
        weak var target: NSObjectProtocol?
        var sel: Selector?
        
        public required init(target:NSObjectProtocol?, sel: Selector?) {
            self.target = target
            self.sel = sel
            super.init()
            guard target?.responds(to: sel) == true else {
                return
            }
            let method = class_getInstanceMethod(self.classForCoder, #selector(WeakTimerProxy.redirectionMethod))!
            class_replaceMethod(self.classForCoder, sel!, method_getImplementation(method), method_getTypeEncoding(method))
        }
        
        @objc func redirectionMethod() {
            if self.target != nil {
                self.target?.perform(self.sel)
            }
        }
    }
    

    使用方法:Timer

    private func timer() {
        let proxy = WeakProxy.init(target: self, sel: #selector(testtimer))
        if !ptimer.isValid {
            ptimer = Timer.scheduledTimer(timeInterval: 1, target: proxy, selector: #selector(testtimer), userInfo: nil, repeats: true)
        }
        ptimer.fire()
        RunLoop.current.add(ptimer, forMode: .common)
    }
    
    @objc private func testtimer() {
        print("----1234")
    }
    

    使用方法:CADisplayLink

    private var cadisplay: CADisplayLink?
    
    private func cadisplaylinkTest() {
        let weakcad = WeakProxy.init(target: self, sel: #selector(cadisplayAction))
        cadisplay = CADisplayLink.init(target: weakcad, selector: #selector(cadisplayAction))
        cadisplay?.add(to: RunLoop.current, forMode: .common)
        cadisplay?.preferredFramesPerSecond = 1
    }
    
    @objc private func cadisplayAction() {
        print("---cadisplay-1234")
    }
    

    使用的时候都不用取消Time,页面会直接释放,走deinit方法~

    相关文章

      网友评论

          本文标题:解决Timer或者CADisplayLink的释放问题

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