iOS底层原理探索----- 内存管理(二)

196 阅读5分钟

NSTimer的循环引用

常见问题

日常开发中,经常会用到NSTimer定时器,一些不正确的写法,会导致NSTimer造成循环引用:

- (void)viewDidLoad { 
    [super viewDidLoad]; 
    
    self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES]; 
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; 
} 

- (void)fireHome { 
    num++;
    NSLog(@"hello word - %d",num); 
}

- (void)dealloc {
    [self.timer invalidate]; 
    self.timer = nil;
}

上述案例,一定会产生循环引用:

  • 创建NSTimer时,将self传入target,导致NSTimer持有self,而self又持有timer

  • 使用NSTimerinvalidate方法,可以解除NSTimerself的持有;

  • 但案例中,NSTimerinvalidate方法,由UIViewControllerdealloc方法执行。但selftimer持有,只要timer有效,UIViewControllerdealloc方法就不会执行。故此双方相互等待,造成循环引用。

target传入弱引用对象

NSTimertarget参数传入一个弱引用的self,能否打破对self的强持有:

- (void)viewDidLoad { 
    [super viewDidLoad]; 
    
    __weak typeof(self) weakSelf = self; 
    
    self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES]; 
    
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; 
}
  • 肯定是不行的,因为在timerWithTimeInterval内部,使用强引用对象接收target参数,所以外部定义为弱引用对象没有任何意义。

这种方式,类似于以下代码:

__weak typeof(self) weakSelf = self; 
typeof(self) strongSelf = weakSelf;

在官方文档中,对target参数进行了明确说明:

image.png

  • target:定时器触发时指定的消息发送到的对象。计时器维护对该对象的强引用,直到它(计时器)失效。

Block的区别,Block将捕获到的弱引用对象,赋值给一个强引用的临时变量,当Block执行完毕,临时变量会自动销毁,解除对外部变量的持有。

常规解决方案

更换API

使用携带Block的方法创建NSTimer,避免target的强持有:

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval 
                                    repeats:(BOOL)repeats
                                      block:(void (^)(NSTimer *timer))block;

在适当时机调用invalidate

根据业务需求,可以将NSTimerinvalidate方法写在viewWillDisappear方法中:

- (void)viewWillAppear:(BOOL)animated { 
    [super viewWillAppear:animated]; 
    
    self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES]; 
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}

- (void)viewWillDisappear:(BOOL)animated { 
    [super viewWillDisappear:animated]; 
    [self.timer invalidate]; 
    self.timer = nil; 
}

或者,写在didMoveToParentViewController方法中:

- (void)viewDidLoad { 
    [super viewDidLoad]; 
    
    self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES]; 
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; 
}

- (void)didMoveToParentViewController:(UIViewController *)parent {
    if (parent == nil) {
        [self.timer invalidate]; 
        self.timer = nil; 
    }
}

切断target强持有

除了常规解决方案,还可以通过切断target的强持有,解决循环引用的问题。

中介者模式

- (void)viewDidLoad { 
    [super viewDidLoad]; 
    
    NSObject *objc = [[NSObject alloc] init]; 
    class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:"); 
    
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:objc selector:@selector(fireHome) userInfo:nil repeats:YES];
} 

void fireHomeObjc(id obj) { 
    num++; 
    NSLog(@"hello word - %d -%@",num, obj); 
} 

- (void)dealloc {
    [self.timer invalidate]; 
    self.timer = nil; 
}
  • 创建NSObject实例对象objc,通过RuntimeNSObject增加fireHome方法,IMP指向fireHomeObjc的函数地址;

  • 创建NSTimer,将objc传入target参数,这样避免NSTimerself的强持有;

  • 当页面退出时,由于self没有被NSTimer持有,正常调用dealloc方法:

    • dealloc中,对NSTimer进行释放。此时NSTimerobjc的强持有解除,objc也跟着释放。

封装自定义Timer

创建LGTimerWapper,实现自定义Timer的封装。

打开LGTimerWapper.h文件,写入以下代码:

#import <Foundation/Foundation.h> 

@interface LGTimerWapper : NSObject 

- (instancetype)lg_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo; 

- (void)lg_invalidate; 

@end

打开LGTimerWapper.m文件,写入以下代码:

#import "LGTimerWapper.h" 
#import <objc/message.h> 

@interface LGTimerWapper()

@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL aSelector;
@property (nonatomic, strong) NSTimer *timer; 

@end 

@implementation LGTimerWapper 

- (instancetype)lg_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo {
    if (self == [super init]) {
        self.target = aTarget;
        self.aSelector = aSelector; 
        if ([self.target respondsToSelector:self.aSelector]) {
            Method method = class_getInstanceMethod([self.target class], aSelector); 
            const char *type = method_getTypeEncoding(method);
            class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type); 
            self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo]; 
        }
    } 
    
    return self; 
} 

void fireHomeWapper(LGTimerWapper *warpper){
    if (warpper.target) { 
        void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend; 
        lg_msgSend((__bridge void *)(warpper.target), warpper.aSelector,warpper.timer); 
    } else { 
        [warpper lg_invalidate];
    } 
} 

- (void)lg_invalidate { 
    [self.timer invalidate]; 
    self.timer = nil; 
}

@end

LGTimerWapper的调用代码:

- (void)viewDidLoad { 
    [super viewDidLoad]; 
    
    self.timerWapper = [[LGTimerWapper alloc] lg_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES]; 
} 

- (void)fireHome { 
    num++; 
}
  • LGTimerWapper中,定义初始化lg_initWithTimeInterval方法和lg_invalidate释放方法;

  • 初始化方法:

    • 控件内部的target使用weak修饰,对ViewController进行弱持有;

    • 检测target中是否存在该selector。如果存在,对当前类使用Runtime添加同名方法编号,指向自身内部fireHomeWapper的函数地址;

    • 创建真正的NSTimer定时器,将控件自身的实例对象传入target,避免NSTimerViewController强持有。

  • NSTimer回调时,会进入fireHomeWapper函数:

    • 函数内部不负责业务处理,如果target存在,使用objc_msgSend,将消息发送给target自身下的selector方法。
  • 当页面退出时,ViewController可以正常释放。但LGTimerWapperNSTimer相互持有,双方都无法释放;

  • 由于双方都无法释放,NSTimer的回调会继续调用:

    • 当进入fireHomeWapper函数,发现target已经不存在了,调用LGTimerWapperlg_invalidate方法,内部对NSTimer进行释放;

    • NSTimer释放后,对LGTimerWapper的强持有解除,LGTimerWapper也跟着释放。

NSProxy虚基类

NSProxy的作用:

  • OC不支持多继承,但是它基于运行时机制,可以通过NSProxy来实现伪多继承;

  • NSProxyNSObject属于同一级别的类,也可以说是一个虚拟类,只实现了NSObject的协议部分;

  • NSProxy本质是一个消息转发封装的抽象类,类似一个代理人。

可以通过继承NSProxy,并重写以下两个方法实现消息转发:

- (void)forwardInvocation:(NSInvocation *)invocation; 

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel;

NSProxy除了可以用于多继承,也可以作为切断强持有的中间人。

打开LGProxy.h文件,写入以下代码:

#import <Foundation/Foundation.h>

@interface LGProxy : NSProxy

+ (instancetype)proxyWithTransformObject:(id)object; 

@end

打开LGProxy.m文件,写入以下代码:

#import "LGProxy.h" 

@interface LGProxy() 

@property (nonatomic, weak) id object; 

@end 

@implementation LGProxy 

+ (instancetype)proxyWithTransformObject:(id)object { 
    LGProxy *proxy = [LGProxy alloc]; 
    proxy.object = object;
    return proxy; 
} 

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { 
    if (!self.object) { 
        NSLog(@"异常收集-stack"); 
        return nil; 
    } 
    
    return [self.object methodSignatureForSelector:sel]; 
} 

- (void)forwardInvocation:(NSInvocation *)invocation {
    if (!self.object) { 
        NSLog(@"异常收集-stack"); 
        return; 
    }
    
    [invocation invokeWithTarget:self.object]; 
} 

@end

LGProxy的调用代码:

- (void)viewDidLoad { 
    [super viewDidLoad];
    
    self.proxy = [LGProxy proxyWithTransformObject:self]; 
    
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES]; 
}

- (void)fireHome { 
    num++;
    NSLog(@"hello word - %d",num); 
} 

- (void)dealloc {
    [self.timer invalidate]; 
    self.timer = nil;
}
  • LGProxy初始化方法,将传入的object赋值给弱引用对象;

  • UIViewController中,创建LGProxy对象proxy。创建NSTimer对象,将proxy传入target,避免NSTimerViewController强持有;

  • NSTimer回调时,触发LGProxy的消息转发方法:

    • methodSignatureForSelector:设置方法签名;

    • forwardInvocation:自身不做业务处理,将消息转发给object

  • 当页面退出时,ViewController可以正常释放:

    • dealloc中,对NSTimer进行释放。此时NSTimerproxy的强持有解除,proxy也跟着释放。