最近发现,在我的代码出现内存泄漏这种情况,然后开始找啊,发现是第三方库 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];
}