CADisplaylink内存泄漏的问题

文章讲述了作者在项目中遇到内存泄漏问题,源于CADisplayLink的强引用导致target未释放。通过分析内存管理机制,提出使用YYWeakProxy来解决这个问题,确保在不再需要时正确地移除定时器和释放资源。
摘要由CSDN通过智能技术生成

最近发现,在我的代码出现内存泄漏这种情况,然后开始找啊,发现是第三方库 YoungTag ,这是在code4上面找到当,改了一下换成公司需要的样子,没想到呀,有内存泄漏,看下面源码:

@interface YoungSphere() <UIGestureRecognizerDelegate>


@end

@implementation YoungSphere
{
    NSMutableArray *tags;
    NSMutableArray *coordinate;
     YoungPoint normalDirection;
    CGPoint last;
    
    CGFloat velocity;
    
    CADisplayLink *timer;
    CADisplayLink *inertia;
}

- (void)setup {
    UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
    [self addGestureRecognizer:gesture];
    inertia = [CADisplayLink displayLinkWithTarget:self selector:@selector(inertiaStep)];
    [inertia addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    
    timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(autoTurnRotation)];
    [timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}

注意我红框框起来的位置

WeChatba8e11b56c7729cf2c816a803817b3ef.png

这里使用 CADisplaylink 来实现定时器的功能,让云标签转起来。对,没错,问题就出在 CADisplaylink 对象这里,我在网上查了一些文章,以下是几篇具有代表性的

基本的意思就是,NSTimer,CADisplayLink,必须依赖于一个runloop,也就是必须添加到runloop中才能有效target-selector方法调用,在初始化CADisplayLink、NSTimer时传入的 target 对象被 CADisplayLink、NSTimer 对象强引用,NSTimer,CADisplayLink被加入到runloop中,如果runloop一直不被释放,从而导致加入到runloop中的NSTimer,CADisplayLink对象得不到释放,因而导致 target 得不到释放。平常开发中基本上都是把 NSTimer,CADisplayLink 添加到主线程runloop中,在程序运行过程中,mainrunloop是不会得到释放的。所以导致被添加到mainrunloop中的 NSTimer,CADisplayLink对象以及NSTimer,CADisplayLink引用的target对象也不会得到释放。内存泄漏的问题就出现了。

别慌,苹果爸爸给了解决方案

解决方法核心就是这个方法

/* Removes the object from all runloop modes (releasing the receiver if
 * it has been implicitly retained) and releases the 'target' object. */

- (void)invalidate;

英文不好,先给你有道翻译一下:从所有运行循环模式中移除对象(如果是,则释放接收方)
它被隐式保留)并释放“target”对象。

意思就是在适当的时候使用NSTimer或者CADisplayLink对象调用“invalidate”方法就OK。什么时候适当,就是在不用了的时候。

有一个坑,得注意一下,重点(敲黑板了,特意加粗一下)

不能在 NSTimer,CADisplayLink引用的"target"的"dealloc"方法中调用 invalidate 方法,

// 在 target 执行 invalidate 时无效的
- (void)dealloc {
    timer.paused = YES;
    inertia.paused = YES;
    [timer invalidate];
    [inertia invalidate];
}

解释一下为什么无效,可能很多同学已经知道,但是我还是要逼逼一下。前面讲到 runloop 强引用 NSTimer/CADisplayLink 对象,而NSTimer/CADisplayLink 对象又强引用了 target 对象,在 target 对象 dealloc 方法根本不会被执行,所以写了等于没写。那要写在哪里呢??下面给出两种解决方法

方法一

在 target 类中写一个destroyTime,不使用timer/inertia了的时候,手动调用一下destroyTime方法就OK

- (void)destroyTime {
    timer.paused = YES;
    inertia.paused = YES;
    [timer invalidate];
    [inertia invalidate];
}
方法二

在YYPFSLabel中看到的,使用“YYWeakProxy”, “YYWeakProxy” 继承自 “NSProxy”。NSProxy 是 Foundation 框架两大基类之一,实现了 NSObject 协议。基本实现原理,不废话看源码

//一个弱引用target对象
@property (nonatomic, weak, readonly) id target;

+ (instancetype)proxyWithTarget:(id)target {
    return [[YYWeakProxy alloc] initWithTarget:target];
}
//将消息接收对象改为 _target
- (id)forwardingTargetForSelector:(SEL)selector {
    return _target;
}
//self 对 target 是弱引用,一旦 target 被释放将调用下面两个方法,如果不实现的话会 crash
- (void)forwardInvocation:(NSInvocation *)invocation {
    void *null = NULL;
    [invocation setReturnValue:&null];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}

屌的人写的代码就是不一样,如此巧妙解决了这个问题。

(control + c)
NSProxy 做为消息转发的抽象代理类,没有 init 方法,子类必须实现 initWithXXX: forwardInvocation: 和 methodSignatureForSelector: 方法)。
当不能识别方法时候,就会调用forwardingTargetForSelector方法,在这个方法中,我们可以将不能识别的传递给其它对象处理
需要重载methodSignatureForSelector和forwardInvocation的,为什么呢?因为_target是弱引用的,所以当_target可能释放了,当它被释放了的情况下,那么forwardingTargetForSelector就是返回nil了.然后methodSignatureForSelector和forwardInvocation没实现的话,就直接crash了!!!
这也是为什么这两个方法中是随便写的 ,而没有将消息转发给其他对象的操作
(control + v),原谅我懒得解释消息转发的基础知识,这一段是复制粘贴的。有兴趣的同学可以读一下 objective-c 消息发送机制

然后上面setup方法中的代码就可以写成这样了,完美解决问题

- (void)setup {
    UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
    [self addGestureRecognizer:gesture];
    
    inertia = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(inertiaStep)];
    [inertia addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    
    timer = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(autoTurnRotation)];
    [timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}

  • 21
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值