内存泄露方案以及NSProxy的使用

396 阅读7分钟

内存泄露出现的常见一般为引用环,或者是强引用导致的

常见的引用环:block循环引用、代理循环引用

常见的强引用:Timer强引用

案例demo

引用环

代理

引用环中,代理出现的循环引用不常见,标准代理都是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];