IOS-强引用分析

1,162 阅读4分钟

什么是强引用

  • 我们在使用NSTimerscheduledTimerWithTimeInterval:target:selector:userInfo:repeats:方法创建定时器使用的时候,timer会对传入的target进行强引用直到timer无效。
  • 这里是官方文档上面的描述
  • 我们通过使用一个上述方法创建的timer来观察这些现象,在一个push进来的TimerViewController实现一个timer打印事件
static int num = 0;
@interface TimerViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation TimerViewController

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

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

- (void)dealloc
{
    NSLog(@"%s",__func__);
}
  • 通过返回回到上一个界面的时候发现并没有执行dealloc,而且计时器仍然继续运行。因为有循环引用导致TimerViewController无法调用dealloc
  • 通过我们之前在block中解决循环引用的方法,我们用一个weakSelf尝试解决这种循环引用。修改代码
__weak typeof (&*self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1. target:weakSelf selector:@selector(printString) userInfo:nil repeats:YES];
  • 运行代码,发现还是没有执行dealloc方法,原因是block捕获的weakSelf是对象的指针地址,然后通过地址再去操作内存数据,而timer持有的是weakSelf指向的对象的内存空间。
  • timer对target就是强引用。

weakSlf&self

  • 通过打印来观察weakSlfself的不同
  • 可以看到weakSlfself的地址不是一样的,指向的是同一片内存空间
  • 再来看下weakSlf有没有对self引用计数进行操作,发现weakSelf并没有对self的引用计数有操作。

解决强引用问题

在界面结束的方法里面将timer设为nil

  • 重写didMoveToParentViewController方法
- (void)didMoveToParentViewController:(UIViewController *)parent
{
    [super didMoveToParentViewController:parent];
    if (parent == nil) {
        if (self.timer) {
            [self.timer invalidate];
            self.timer = nil;
            NSLog(@"timer 结束了");
        }
    }
}
  • 运行代码,发现确实解决了循环引用的问题

中介者模式

  • 中介者模式就是不使用self,由于使用self会有引用循环,所以我们可以将target换成其他对象,我创建一个NSObject的实例对象作为target
NSObject *obj = [NSObject alloc];
    class_addMethod([obj class], @selector(printString), (IMP)printStringWithObj, "v@:");
    
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1. target:obj selector:@selector(printString) userInfo:nil repeats:YES];
  • 运行代码,发现TimerViewController执行了dealloc,但是obj并没有被释放,timer也在继续执行,因为runloop强持有了timer,而且runloop的生命周期比控制器长,所以我们需要在dealloc里将timer停止并设为nil。
- (void)dealloc
{
    NSLog(@"%s",__func__);
    if (self.timer) {
        [self.timer invalidate];
        self.timer = nil;
        NSLog(@"timer 结束了");
    }
}
  • 再次运行就发现已经解决了上述问题。

基于中介者模式的封装

  • 通过上面代码我们知道中介者模式可以解决强引用的问题,我们在此基础上封装一个timer

@interface XQTimerWapper : NSObject
- (instancetype)xq_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
- (void)xq_invalidate;
@end

#import "XQTimerWapper.h"
#import <objc/message.h>
@interface XQTimerWapper ()

@property (nonatomic, weak) id aTarget;
@property (nonatomic, assign) SEL aSelector;

@property (nonatomic, strong) NSTimer *timer;

@end

@implementation XQTimerWapper

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

void doSomeThing(XQTimerWapper *timerWapper)
{
    if (timerWapper.aTarget) {
        void (*xq_msgSend)(void* ,SEL ,id ) = (void *)objc_msgSend;
        xq_msgSend((__bridge void *)(timerWapper.aTarget),timerWapper.aSelector,timerWapper.timer);
    }else{
        [timerWapper.timer invalidate];
        timerWapper.timer = nil;
    }
}

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

- (void)dealloc
{
    NSLog(@"%s",__func__);
}
@end
  • TimerViewController直接赋值,再在dealloc里调用停止方法。
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.timerWapper = [[XQTimerWapper alloc] xq_initWithTimeInterval:1. target:self selector:@selector(printString) userInfo:nil repeats:YES];
}

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

- (void)dealloc
{
    NSLog(@"%s",__func__);
    [self.timerWapper xq_invalidate];
}

基于虚基类NSProxy

  • OC是只能单继承的语言,但是它是基于运行时的机制,所以可以通过NSProxy来实现伪多继承,填补了多继承的空白
  • NSProxyNSObject是同级的一个类,也可以说是一个虚拟类,只是实现了NSObject的协议
  • NSProxy其实是一个消息重定向封装的一个抽象类,类似一个代理人,可以通过继承它,并重写下面两个方法来实现消息转发到另一个实例
- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
  • NSProxy的使用场景主要有两种
    • 实现多继承功能
    • 解决了NSTimer&CADisplayLink创建时对self强引用问题,参考YYKit的YYWeakProxy。

循环引用解决原理:主要是通过自定义的NSProxy类的对象来代替self,并使用方法实现消息转发

  • 自定义一个继承自NSProxy的类
@interface XQProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end

@interface XQProxy ()
@property (nonatomic, weak) id object;
@end

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

// 强引用 -> 消息转发
-(id)forwardingTargetForSelector:(SEL)aSelector {
    return self.object;
}
  • 修改执行代码
- (void)viewDidLoad {
    [super viewDidLoad];
   
    self.proxy = [XQProxy proxyWithTransformObject:self];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1. target:self.proxy selector:@selector(printString) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

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

- (void)dealloc
{
    NSLog(@"%s",__func__);
    if (self.timer) {
        [self.timer invalidate];
        self.timer = nil;
        NSLog(@"timer 结束了");
    }
}
  • 运行代码,发现也成功解决了强引用的问题,我们平常开发中是推荐用这种方式的来解决循环引用问题的