内存泄露出现的常见一般为引用环,或者是强引用导致的
常见的引用环:block循环引用、代理循环引用
常见的强引用:Timer强引用
引用环
代理
引用环中,代理出现的循环引用不常见,标准代理都是weak修饰,由此形成的循环引用不是很常见,常见的为使用属性之间互相引用,应当改为代理,或者weak来处理
其引用环一般为 self -> delegate -> self ,此时只需要将delegate参数设置为指向weak的self即可
block
block内存泄露
block是非常常见的内存泄露源之一,甚至很多人认为只要是block则一定会产生循环引用,这是不科学的
前面文章讲了,block会自动捕获外部变量,并对外部的对象进行额外的引用,以便于在block内部使用,因此才引发的block内存泄露问题
下面举个案例说明:
//测试block引起的leak
- (void)testBlockLeak {
self.testBlock = ^ {
self.name = @"";
};
self.testBlock();
}
通过上面代码可以看到, block内部使用到了self,block会捕获外部self,并对其引用,因此出现了下面的场景:
//self和testBlock则行程相互引用的场景
self -> testBlock -> self
解决方案1
使用weak来定义weakself,这是比较常见的解决方案,主要是解决起来方便,局部出现问题,局部解决,也是最快的解决方式,缺点是weak的性能损耗
- (void)testBlockLeak3 {
//下面两个都可以
__weak typeof(self) wself = self;
// __weak __typeof(self) wself = self;
self.testBlock2 = ^(ViewController *vc) {
wself.name = @"";
};
self.testBlock2(self);
}
解决方案2
调用时直接传入self作为参数,这是较好的解决方案,如果是提前写好的可以使用这种方式,系统常见的协议中,一般第一个参数都是代理对象本身,有异曲同工之效
- (void)testBlockLeak2 {
self.testBlock2 = ^(ViewController *vc) {
vc.name = @"";
};
self.testBlock2(self);
}
block非内存泄露使用
上面提到了block会自动捕获外部变量,因此有可能会造成引用环,从而导致内存泄露,因此block内存泄露的前提是要行程引用环,就如上面的案例 self -> block -> self
看下面一段代码:
- (void)animateBlock {
self.animateView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 40, 40)];
self.animateView.backgroundColor = [UIColor redColor];
[self.view addSubview:self.animateView];
[UIView animateWithDuration:3 animations:^{
self.animateView.frame = CGRectMake(0, 300, 40, 40);
}];
}
可以检测,其没有内存泄露,从引用上观察,仅仅是UIView的内部block引用了self,self并没有对该block有直接或者间接的引用关系,因此无内存泄露
timer强引用
timer这个控件很特殊,其运行过程中会被runloop强引用,因此其需要手动设置失效并移除之
并且timer在使用的过程中会对target指向的对象进行强引用,因此,无法在target的dealloc中取消timer
一般timer的使用案例如下所示:
方案1
当前控制器从父控制器中移除时,主动移除timer,view视图一样,也有类似的方法
或者是退出控制器的时候,再返回时主动进行移除
由于强引用,使用该方式,在主动移除timer之前,不会走self的dealloc方法,因此需要在合适的时机主动移除
可能你会想,如果timer的target是weakself会怎么样呢,注意timer对target进行了强引用,可以自行尝试一下,是没有任何用处的
//正常控制器中使用timer
- (void)testTimer1 {
//runloop->timer->self->timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1
target:self selector:@selector(loopSelector) userInfo:nil repeats:YES];
}
//从父控制器移除移除timer,dealloc不用移除了
- (void)willMoveToParentViewController:(UIViewController *)parent {
if (!parent) {
[self.timer invalidate];
self.timer = nil;
}
}
方案2
使用timer自带的block回调方法,block的方式可以自行接触timer对self的强引用,而block的引用可以直接使用weakself来解决,如下所示
如果app是ios10以上的,推荐使用该方案,使用纯系统api相对非常简洁
- (void)testTimer2 {
//不会存在循环引用 self->timer runloop->timer->block
__weak typeof(self) wself = self;
//注意:该方法必须要ios10之后才行
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES
block:^(NSTimer * _Nonnull timer) {
wself.name = @"";
NSLog(@"timer的打印");
}];
}
方案3
使用中间变量target做代理处理,其原理是引入一个中间变量target,从而阻断此强引用链,原理如下所示:
//只有target被强引用,因此可以在self释放的时候,直接移除timer,而self释放target也会自动引用计数减少而释放
runloop -> timer -> target self -> target
使用方法如下所示:
- (void)testTimer3 {
//使用target作为中间代理对象,这个selector是对target发送消息
//因此target要解决方法调用的问题,否则会崩溃
__weak typeof(self) wself = self;
self.target = [[LSTarget alloc] initWithBlock:^{
[wself loopSelector];
}];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1
target:self.target selector:@selector(loopMethod) userInfo:nil repeats:YES];
}
这里使用的是block用于转接timer的固定loopMethod方法,也可以两个使用相同的select都行,只不过该方法看着比较怪异,有点模仿方案2的嫌疑,用的也比较少,且对于带参数和不带参数方法还要繁琐一些
方案4
使用封装的timerWapper工具来解决,该类同时作为timer的引用者和self的引用者,其引用关系如下所示:
//这样就解开了对self的强引用,可以在self释放时,解除timer对timerWapper的强引用
runloop -> timer <- timerWapper <- self
其实现如下所示:
调用方法
- (void)testTime4 {
self.lsTimer = [[LSTimerWapper alloc] initTimerWithInterval:1
target:self selector:@selector(loopSelector) repeat:YES];
}
timerWapper的实现如下所示:
@interface LSTimerWapper ()
{
BOOL _canRespondsSelector;
}
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation LSTimerWapper
- (instancetype)initTimerWithInterval:(NSTimeInterval)interval
target:(id)target selector:(SEL)selector repeat:(BOOL)repeat {
self.target = target;
self.selector = selector;
self.timer = [NSTimer scheduledTimerWithTimeInterval:interval
target:self selector:@selector(loopMethod) userInfo:nil repeats:YES];
_canRespondsSelector = [target respondsToSelector:selector];
return self;
}
- (void)loopMethod {
if (_canRespondsSelector) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.target performSelector:self.selector];
#pragma clang diagnostic pop
}
}
- (void)removeTimer {
[self.timer isValid];
self.timer = nil;
}
该方案虽然看起来稍复杂,仍然存在带参函数的痛点,相比于方案3,还是要好一些
注:改善保存一个block,可以解决带参函数的问题
方案5(推荐)
使用NSProxy转发消息给使用对象,其可以解决方案3中的中间变量的繁琐之处,且更为通用
我们都知道,平时基本上见到的类都继承自NSObject,而NSProxy就是例外,其跟NSObject并列的基类,为虚拟类
NSProxy不走NSObject的那套方法查找流程,当NSProxy类中找不到相应的实现方法,就会直接开启消息转发流程,因此对于需要使用转发的功能来说,此功能效率要高得多,我们可以直接通过其来当中间类,来解决timer的强引用关系
另外,除了timer以外,其他的需要中间类的控件,都可以使用NSProxy来转发消息
解除强引用的方式和方案3一样
封装后的NSProxy使用非常简单,如下所示:
- (void)testTimer5 {
//使用虚类NSProxy转发消息给当前类
self.proxy = [LSProxy propxyWithPerformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1
target:self.proxy selector:@selector(loopSelector) userInfo:nil repeats:YES];
}
NSProxy内部实现如下所示:
@interface LSProxy ()
@property (nonatomic, weak) id object;
@end
@implementation LSProxy
+ (instancetype)propxyWithPerformObject:(id)object {
LSProxy *proxy = [LSProxy alloc];
proxy.object = object;
return proxy;
}
//NSProxy方法查找,不会去父类查找,本类找不到方法,直接开启消息转发流程,性能相比较之下不错,很专业
//重定向和消息转发随便一个都行
//重定向
- (id)forwardingTargetForSelector:(SEL)aSelector{
// if (self.object) {
// return self.object;
// }else {
// NSLog(@"object不存在无法重定向,崩溃了");
// return nil;
// }
return self.object; //将方法调用重定向到self.object代理类
}
//实例方法的消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.object methodSignatureForSelector:sel]; //使用self.object的方法签名
}
- (void)forwardInvocation:(NSInvocation *)invocation {
if (self.object)
[invocation invokeWithTarget:self.object]; //对self.object执行该方法
else
NSLog(@"object不存在重定向失败,嗝屁了");
}
通过NSProxy可以直接通过重定向或者消息转发都行,可以直接将消息发送给方法原类,也避免了代理函数的一系列问题,且更为通用
实际这么使用看起来很麻烦,通过它我们可以简易封装自动释放的Timer
合并之后的实现如下所示,可以通过该对象,让其声明周期依托于返回对象,因此无需手动 invalidate 释放,很是方便
@interface LSProxyTimerWrapper : NSObject
- (instancetype)initTimerWithInterval:(NSTimeInterval)interval target:(id)target
selector:(SEL)selector userInfo:(nullable id)userInfo repeat:(BOOL)repeat;
@end
@interface LSProxyTimerWrapper ()
@property (nonatomic, strong) LSProxy *proxy;
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation LSProxyTimerWrapper
- (instancetype)initTimerWithInterval:(NSTimeInterval)interval target:(id)target
selector:(SEL)selector userInfo:(nullable id)userInfo repeat:(BOOL)repeat {
self.proxy = [LSProxy propxyWithPerformObject:target];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1
target:self.proxy selector:selector userInfo:userInfo repeats:YES];
return self;
}
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
}
@end
使用如下所示,你没看错不需要释放,只需要使用的场景持有返回对象即可
//使用虚类NSProxy转发消息给当前类,封装一下
//属性持有自动释放,释放时机跟当前控制器一样,无需在dealloc中结束
self.proxyWrapper = [[LSProxyTimerWrapper alloc]
initTimerWithInterval:1 target:self
selector:@selector(loopSelector) userInfo:nil repeat:YES];