iOS 永久解决NSTimer/CADisplayLink循环引用的问题

发布时间 2023-03-22 21:16:52作者: qqcc1388

在iOS中定时器的循环引用会导致定时器无法正常关闭,页面无法正常释放导致内存泄漏。
正常来讲 我们的vc强引用定时器 定时器强引用vc从而导致引用环无法结束,通过中间人的方式可以解决相互之间引用的问题
让中间人弱引用vc 定时器强引用中间人对象 这样就断开了定时器和vc间的循环

大致代码如下:

@interface WeakTarget : NSObject

+ (instancetype)proxyWithTarget:(id)target;

@end

#import "WeakTarget.h"

@interface WeakTarget ()

@property (weak,nonatomic,readonly) id target;

@end

@implementation WeakTarget

- (instancetype)initWithTarget:(id)target{
    _target = target;
    return self;
}

+ (instancetype)proxyWithTarget:(id)target{
    return [[self alloc] initWithTarget:target];
}

+(BOOL)resolveInstanceMethod:(SEL)sel{
    return YES;
}

+(BOOL)resolveClassMethod:(SEL)sel{
    return YES;
}

-(id)forwardingTargetForSelector:(SEL)aSelector{
    if([self respondsToSelector:aSelector]){
        return self.target;
    }
    return [super forwardingTargetForSelector:aSelector];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    return [self.target methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)invocation{
    SEL sel = [invocation selector];
    if ([self.target respondsToSelector:sel]) {
        [invocation invokeWithTarget:self.target];
    }
}

- (BOOL)respondsToSelector:(SEL)aSelector{
    return [self.target respondsToSelector:aSelector];
}
@end

使用:这里以NSTimer为例,CADisplayLink同理

  _timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:[WeakTarget proxyWithTarget:self] selector:@selector(timeInterval) userInfo:nil repeats:YES];
  [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
 /// 开启定时器
  [_timer fire];

  /// 注意别忘记在dealloc方法中关闭并销毁定时器
    if(self.timer.isValid){
        [self.timer invalidate];
    }
   _timer = nil;

大致是用到消息转发的机制,利用这个机会再次重温一下消息转发的流程

runtime会为每个类(不是类的实例)添加一个方法列表,该列表通过hash表实现,方便使用时快速查找,缓存中的方法通过hash查找,
消息传递的过程:
在我们调用一个方法的时候,先从缓存中去查找,看缓存中是否有对应方法选择器的实现,如果命中通过函数指针调用方法,完成一次消息的传递
如果没有命中,会通过当前类的isa指针去查找类的方法列表,看是否有同样名称的方法,若果命中,则通过函数指针调用该方法,结束消息的传递
如果当前类的方法类表中仍然没有找到同名方法,则逐级父类的方法列表中查找,通过当前类对象的[super class]指针查找父类的方法列表,如果命中,则返回,如果未命中,则启动消息转发流程
消息转发流程分为3个环节
1、运行时决议是否有动态添加的方法 通过class_addMethod动态添加方法

/// 类方法
-(bool)resolveClassMethod:(SEL)sel
/// 实例方法
-(bool)resolveInstanceMethod:(SEL)sel

消息转发的第一步,通过判断运行时是否需要动态添加方法,如果动态添加方法,通过class_addMethod添加并返回yes,否则走后续流程

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self performSelector:@selector(foo:)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(foo:)) {
        class_addMethod([self class], sel, (IMP)fooMethod, "v@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void fooMethod(id obj, SEL _cmd){
    NSLog(@"Doing foo");
}

2.如果运行时未添加该方法,则走后续流程,判断其他对象是否实现了需要的方法,并返回其他对象

-(id)forwardingTargetForSelector:(SEL)aSelector{
    if([self respondsToSelector:aSelector]){
        return self.target;
    }
    return [super forwardingTargetForSelector:aSelector];
}
- (BOOL)respondsToSelector:(SEL)aSelector{
    return [self.target respondsToSelector:aSelector];
}

如本例中WeakTarget未实现Interval方法,判断target是否实现了interval方法,如果发现target有实现则返回target对象

3、如果这一步未返回需要实现的其他对象,则走最后一步消息转发流程 通过methodSignatureForSelector和forwardInvocation来实现

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
-(void)forwardInvocation:(NSInvocation *)anInvocation

先通过methodSignatureForSelector获取调用方法的签名,返回NSInvocation对象,可以在该方法中指定某个类来实现某个方法

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    return [self.target methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)invocation{
    SEL sel = [invocation selector];
    if ([self.target respondsToSelector:sel]) {
        [invocation invokeWithTarget:self.target];
    }
}

如果forwardInvocation中未成功返回执行方法,则消息转发失败,报错指定方法未找到