iOS中Timer循环引用原因及解决方案

一、准备

timer的创建

第一种:

  • 如果在主线程里创建,需要修改下Mode为NSRunLoopCommonModes,不然,当滚动事件发生时,会导致NSTimer不执行,主线程的RunLoop是默认开启的,所以不需要[[NSRunLoop currentRunLoop] run]。
  • 如果在子线程里创建,且当前线程里无滚动事件,则不需要修改Mode,子线程的RunLoop默认不开启的,需要手动加入Runloop
__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:NSDefaultRunLoopMode];
- (void)fireHome {
    num++;
    NSLog(@"hello word - %d",num);
}

第二种:
另一种创建timer方法,自动加入Runloop无需手动添加,等同于上述方法:

__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1
                                              target:weakSelf
                                            selector:@selector(fireHome)
                                            userInfo:nil
                                             repeats:YES];

二、timer循环引用分析:

1. timer循环引用分析

self -> timer -> self循环引用分析:

  • self 强持有 timer 我们都能直接看出来,那么timer是什么时候强持有 self的呢?看苹果官方文档可知:target方法中,timerself对象进行了强持有,因此造成了循环引用。
  • 但是当我们按照惯例用weakSelf去打破强引用的时候,发现weakSelf没有打破循环引用,timer仍然在运行。
  • self -> timer -> weakSelf -> self

2. __weak typeof(self) weakSelf = self分析

从上面我们会疑惑为什么blockself -> block -> wealSelf -> self可以打破循环引用,而 self -> timer -> weakSelf -> self无法打破呢?
带着这个疑问,我们要了解__weak typeof(self) weakSelf = self;做了什么。

从上图可知,weakSelfself两个指针地址不同但内存空间地址相同,也就是两个对象同时持有同一个内存空间。
并且正常情况下经过__weak typeof(self) weakSelf = self操作我们需要进行引用计数处理,但是实际情况是经过弱引用表并没有处理引用计数。


3. 分析block使用weakSelf为什么可以打破循环引用呢?

a). 通过命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m生成 .cpp代码:

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg; 
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        _Block_retain_object(object);
        *dest = object; // 实际上是指针赋值

b). 在.cpp文件中我们可以看到如上代码段,虽然weakself对象传入进来,但是内部实际操作的是对象的指针,也就是weakself的指针,我们知道weakselfself虽然内存地址相同,但指针是不一样的,也就是block中并没有直接持有self,而是通过weakSelf指针操作,所以就打破了self -> block -> weakSelf -> selfself这一层的循环引用,变成了self -> block -> weakSelf (临时变量的指针地址)来打破循环引用。

4. 总结

  1. self -> block -> weakSelf -> self:block使用weakSelf之所以能够打破循环引用是因为block内部操作的是weakSelf的指针地址,它和self是两个不同的指针地址,即 没有直接持有self,所以可以weakSelf可以打破self的循环引用关系self -> block -> weakSelf
  2. self -> timer -> weakSelf -> self:那timer之所以无法打破循环关系是因为timer创建时target是对weakSelf的对象强持有操作,而weakSelf和self虽然是不同的指针但是指向的对象是相同的,也就相当于间接的强持有了self,所以weakSelf并没有打破循环引用关系。

二、解决timer循环引用的四种方法

1. 使用invalidate结束timer运行

我们第一时间肯定想到的是[self.timer invalidate]不就可以了吗,当然这是正确的思路,那么我们调用时机是什么呢?viewWillDisAppear还是viewDidDisAppear?实际上在我们实际操作中,如果当前页面有push操作的话,当前页面还在栈里面,这时候我们释放timer肯定是错误的,所以这时候我们可以用到下面的方法:

- (void)didMoveToParentViewController:(UIViewController *)parent {
    // 无论push 进来 还是 pop 出去 正常运行
    // 就算继续push 到下一层 pop 回去还是继续
    if (parent == nil) {
       [self.timer invalidate];
        self.timer = nil;
        NSLog(@"timer 走了");
    }
}

2. 中介者模式

换个思路,timer会造成循环引用是因为target强持有了self,造成的循环引用,那我们是否可以包装一下target,使得timer绑定另外一个不是self的target对象来打破这层强持有关系。

@property (nonatomic, strong) id target;

self.target = [[NSObject alloc] init]; // 自己创建的target
class_addMethod([NSObject class],
                @selector(fireHome),
                (IMP)fireHomeObjc,
                "v@:");
self.timer = [NSTimer
              scheduledTimerWithTimeInterval:1
              target:self.target
              selector:@selector(fireHome)
              userInfo:nil
              repeats:YES];

根据打印结果我们发现在dealloc的时候也可以实现timer的释放,打破了循环引用。

class_addMethod的作用:看着是给target增加了一个方法,但是实际上timer的执行是在fireHomeObjc里面执行的,而不是应该执行的fireHome函数。
分析一下:在没有使用自定义的target之前,fireHome函数的IMP是指向fireHome的这是毋庸置疑的,而使用class_addMethod之后,相当于重新指定了fireHome的IMP指针,让他指向了fireHomeObjc。

  • 代码优化:既然class_addMethod中需要一个函数的IMP,那么我们直接获取fireHome的IMP就可以了。
self.target = [[NSObject alloc] init];
Method method = class_getInstanceMethod([self class], @selector(fireHome));
class_addMethod([self.target class], @selector(fireHome), method_getImplementation(method), "v@:");
self.timer = [NSTimer
              scheduledTimerWithTimeInterval:1
              target:self.target
              selector:@selector(fireHome)
              userInfo:nil
              repeats:YES];

3. NSProxy虚基类的方式

NSProxy是一个虚基类,它的地位等同于NSObject。
command+shift+0打开Xcode参考文档搜索NSProxy,说明如下:

NSProxy
An abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet.

Declaration

@interface NSProxy

Overview

Typically, a message to a proxy is forwarded to the real object or causes the proxy to load (or transform itself into) the real object. Subclasses of NSProxycan be used to implement transparent distributed messaging (for example, NSDistant<wbr>Object) or for lazy instantiation of objects that are expensive to create.
NSProxy implements the basic methods required of a root class, including those defined in the NSObject protocol. However, as an abstract class it doesn’t provide an initialization method, and it raises an exception upon receiving any message it doesn’t respond to. A concrete subclass must therefore provide an initialization or creation method and override the forward<wbr>Invocation: and method<wbr>Signature<wbr>For<wbr>Selector: methods to handle messages that it doesn’t implement itself. A subclass’s implementation of forward<wbr>Invocation: should do whatever is needed to process the invocation, such as forwarding the invocation over the network or loading the real object and passing it the invocation. method<wbr>Signature<wbr>For<wbr>Selector: is required to provide argument type information for a given message; a subclass’s implementation should be able to determine the argument types for the messages it needs to forward and should construct an NSMethod<wbr>Signatureobject accordingly. See the NSDistant<wbr>Object, NSInvocation, and NSMethod<wbr>Signature class specifications for more information.

我们不用self来响应timer方法的target,而是用NSProxy来响应。

  • DZProxy.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface DZProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end
NS_ASSUME_NONNULL_END
  • DZProxy.m
#import "DZProxy.h"
@interface DZProxy()
@property (nonatomic, weak) id object;
@end

@implementation DZProxy
+ (instancetype)proxyWithTransformObject:(id)object {
    DZProxy *proxy = [DZProxy alloc];
    proxy.object = object; // 我们拿到外边的self,weak弱引用持有
    return proxy;
}
// 仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部self,这一步至关重要,主要涉及到runtime的消息机制。
// proxy虚基类并没有持有vc,而是消息的转发,又给了vc
- (id)forwardingTargetForSelector:(SEL)aSelector {
    return self.object;
}
  • VC
- (void)viewDidLoad {
    [super viewDidLoad];
    self.proxy = [DZProxy proxyWithTransformObject:self];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
}

- (void)dealloc{
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%s",__func__);
}

虚基类方法是用proxy打破self 这一块的循环。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容