NSProxy与定时器, 解决循环引用

前言

今天看别人的代码, 发现用到了NSProxy这个类, 就查了一下, 然后就发现, 自己用了这么久的定时器NSTimer, 居然大部分都会有内存问题, 就觉得必须记录一下, 如果你也像我一样用的NSTimer, 那你可能就要注意了, 请看如下问题代码:

@property (nonatomic, weak) NSTimer *timer;
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    // 定时器 重不重复没影响
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerRunning) userInfo:nil repeats:YES];
    // 这句话 是为了让滑动scrollView的时候定时器不会停止, 加不加对今天的问题没影响
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)timerRunning
{
    NSLog(@"1");
}
- (void)dealloc
{
    NSLog(@"FirstViewController --dealloc");
    [self.timer invalidate];
}

上面的用法是有问题的, NSTimer必须要调用invalidate方法, 才能释放, 然而上面的- dealloc方法就不会走, 所以定时器也不会释放。(不信的可以亲自试试, 一定要用target-action的模式, 用block是不会出现这个问题的, 想知道为什么, 继续往下看)

无法释放的原因如下原因如下:

其实产生循环引用的根本就是引用计数, 然而上面的情况并不仅仅是循环引用, 如果不用属性保存NSTimer, pop控制器后, 依然会造成定时器在后台打印。 我们用引用计数来解释原因:

  • 控制器push或者present过来, 控制器的引用计数+1
  • 添加控制器相当于在Runloop中注册timer, Runloop会强引用定时器
  • 定时器通过target-action的方式引用控制器, target-action的设计模式中, 对target的引用应该是弱引用的, 为什么会造成强引用, 我猜测(知道真相的小伙伴可以留言告诉我)可能是NSTimer把控制器交给runloop进行强引用, 以便于在到达注册时间时发送消息, 因此即便用弱引用的weakSelf修饰控制器, 依然无法解决内存无法释放的问题, 因为你控制器的指针传到了runloop手里, runloop就将控制器的引用计数+1了。
  • pop或者dismiss的时候, 控制器引用计数-1, 然后界面消失, 你就再也找不到控制器了, 然而控制器的引用计数还有1呢, 控制器的内存就泄露啊, 没有被销毁; 因为引用计数从1到0的时候才会调用dealloc方法, 因此, 定时器也没有被invalidate, 它会在后台一直循环打印, 不胜其烦!

那么这个问题怎么解决呢?

我之前的解决方式是, 在viewDidDisappear的时候调用invalidate, 虽然解决了问题, 但是还是有新问题的, 因为不止是消失的时候viewDidDisappear会走, pushpresent控制器的时候viewDidDisappear也会走啊, 那么怎么办?

NSProxy就是你的曙光了

NSProxy是iOS开发中一个消息转发的基类,它不继承自NSObject。因为他也是Foundation框架中的基类, 通常用来实现消息转发, 我们也可以用它来包装控制器, 达到弱引用的效果。PS: 如果你只是想要解决以上的问题, 可以完全不用理解消息转发机制, 直接使用代码就够了, 用法超级简单; 如果你想了解, 请点击这里

NSProxy是一个抽象类, 需要使用它的子类, 然后需要实现init以及消息转发的相关方法。

// 当一个消息转发的动作NSInvocation到来的时候,在这里选择把消息转发给对应的实际处理对象
- (void)forwardInvocation:(NSInvocation *)anInvocation

// 当一个SEL到来的时候,在这里返回SEL对应的NSMethodSignature
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

// 是否响应一个SEL
+ (BOOL)respondsToSelector:(SEL)aSelector

首先创建一个NSProxy的子类WeakProxy, 并在.h文件中声明以下属性和方法

@interface WeakProxy : NSProxy

@property (weak,nonatomic,readonly)id target;
+ (instancetype)proxyWithTarget:(id)target;
- (instancetype)initWithTarget:(id)target;

@end

.m里实现如下

@implementation WeakProxy

- (instancetype)initWithTarget:(id)target{
    _target = target;
    return self;
}
+ (instancetype)proxyWithTarget:(id)target{
    return [[self alloc] initWithTarget:target];
}
- (void)forwardInvocation:(NSInvocation *)invocation{
    SEL sel = [invocation selector];
    if ([self.target respondsToSelector:sel]) {
        [invocation invokeWithTarget:self.target];
    }
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    return [self.target methodSignatureForSelector:aSelector];
}
- (BOOL)respondsToSelector:(SEL)aSelector{
    return [self.target respondsToSelector:aSelector];
}

@end

以上代码就可以用了, 用法如下:


@property (nonatomic, weak) NSTimer *timer;

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[WeakProxy proxyWithTarget:self] selector:@selector(timerRunning) userInfo:nil repeats:YES];
}

- (void)timerRunning
{
    NSLog(@"1");
}
- (void)dealloc
{
    NSLog(@"FirstViewController --dealloc");
    [self.timer invalidate];
}

以上, 我们就解决了定时器不释放的问题, 解决原理如下:

我们依然从引用计数的角度分析:

  • pushpresent进入控制器, 引用计数+1
  • 定时器向NSRunloop注册事件, NSRunloop强引用WeakProxy对象, WeakProxy弱引用控制器(因为是弱引用, 引用计数不变), 当到达定时器注册的时间时, Runloop会向WeakProxy对象发送消息, WeakProxy触发消息转发, 把消息转发给弱引用的控制器处理
  • popdismiss控制器时, 引用计数-1 变为0触发-dealloc方法, 调用定时器的invalidate注销了定时器, 同时Runloop注销掉对WeakProxy对象的强引用, 至此, 所有的内存都被释放了, 问题就解决了
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容