- CADisplayLink、NSTimer 会对target 产生强引用,如果target 又对他们产生了强引用,那么就会引发循环引用。
总结:
- CADisplayLink 想要返回界面就销毁定时器,可以使用 一个 中间代理
- NSTimer 则可以使用 block 和 中间代理
- 中间代理 可以使用继承至 NSObject 或 NSProxy
- NSProxy 是什么?
- 继承至 NSObject 或 NSProxy 的 区别
- NSTimer 和 CADisplayLink 可能不准时
一、CADisplayLink
- 不用设置 定时器的时间
- 调用非常频繁
@property (strong,nonatomic) CADisplayLink *link;
- (void)viewDidLoad {
[super viewDidLoad];
// self.link = [[CADisplayLink alloc] init];
// 每隔一段时间就调用 linkTest 方法
// 保证调用频率和屏幕的刷帧频率一致,60FPS
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
// 依赖于 runLoop,添加到 runLoop 中
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)linkTest {
NSLog(@"%s", __func__);
}
调用结果:非常的频繁
-
1秒内能调用 60 次。但如果linkTest 方法中是耗时操作,可能1秒钟不到 60次。
-
[CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)]
这句代码中 的 target 是 self 。 -
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)]
中的self.link
也有 self 。 -
也就说 照成了循环引用。
测试这个界面能否销毁。
- 先创建一个导航控制器,在导航控制器的 dealloc 方法中写上
[self.link invalidate];
- 运行结果:当点击导航栏的返回按钮,定时器依然在打印。没有被销毁。
解决方案:再创建一个对象
- ViewController 的 timer 指向 NSTimer
- NSTimer 中的 target 指向 OtherObject
- OtherObject 中的 target 指向 viewController
@interface WYProxy : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
@implementation WYProxy
+ (instancetype)proxyWithTarget:(id)target {
MJProxy *proxy = [[MJProxy alloc] init];
proxy.target = target;
return proxy;
}
@end
// 调用
self.link = [CADisplayLink displayLinkWithTarget:[WYProxy proxyWithTarget:self] selector:@selector(linkTest)];
- 使用消息转发技术
*当没有这个方法的时候,将消息转发给别人去调用。当调用 objc_msgSend发送消息去找这个消息, 有三个阶段:
* 搜索方法,自己 -> 父类 -> 缓存
* 如果找不到 -> 动态方法解析阶段 :允许动态添加这个方法的实现
* 如果还不行 -> 消息转发
* 先调用- (id)forwardingTargetForSelector:(SEL)aSelector
* 如果不行 ,在调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
* 然后调用- (void)forwardInvocation:(NSInvocation *)anInvocation
当执行 self.link = [CADisplayLink displayLinkWithTarget:[WYProxy proxyWithTarget:self] selector:@selector(linkTest)];
时,会到 WYProxy
类中,用消息转发技术。
- (id)forwardingTargetForSelector:(SEL)aSelector {
// 返回能够处理消息的对象
return self.target;
// 会变成 objc_msgSend(self.target, aSelector);
}
- 使用
- (id)forwardingTargetForSelector:(SEL)aSelector
方法,不管以后外界调用的 WYProxy 哪个方法,都不用去管了。因为只要调用WYProxy 类中的方法,都会转到- (id)forwardingTargetForSelector:(SEL)aSelector
中。
二、NSTimer
- 这个跟CADisplayLink 一样,点击返回的时候,定时器依然在工作,没有被销毁。
- 这是因为 控制器被 NSTimer 强引用 -> NSTimer 对 targer 强引用。造成循环引用。
@property (strong, nonatomic) NSTimer *timer;
- (void)viewDidLoad {
[super viewDidLoad];
/// 如果要是 scheduled 方法的,就是直接 加在 runLoop 中的
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
- (void)dealloc {
[self.timer invalidate];
}
错误解决方案:
__weak typeof(self) weakSelf = self;
// target 传进去的都是 内存地址而已
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:weakSelf selector:@selector(timerTest) userInfo:nil repeats:YES];
-
因为 weakSelf 是用在 block 中的。
-
NSTimer 内部有一个强指针,存储的是 外界传进来的target的内存地址。跟外界传的 weakSelf 还是 self 都没有关系。
-
所以 NSTimer 产生的强引用跟 weakSelf 或 self 没有关系,跟它自己的 target 属性有关。可能NSTimer 内部有一个 属性(只是可能)。
解决方案:1. 使用 block
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf timerTest];
}];
- NSTimer 对 block 产生 强引用,block 对 self 产生弱引用
三、NSProxy
- 不继承 NSObject , 它跟 NSObject 一个级别 。
- 是基类
- 不需要 init 方法 只用 [NSProxy alloc] 即可。
代码为:
@interface NSProxy <NSObject> {
Class isa;
}
@interface NSObject <NSObject> {
Class isa ;
}
如果把@interface WYProxy : NSObject
改成 @interface WYProxy : NSProxy
,运行会报错
- NSProxy 存在的意义就是 用来解决刚才的代理行为(转发行为)。
- 当你调用WYProxy的任何方法, 如果它继承制 NSProxy ,那么系统马上回调用一个方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
代码为:
// 返回方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
// 直接调用
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
- 使用 NSProxy 做消息转发 比 使用 NSObject 效率高。
- NSProxy 专门是做消息转发的。
当这两个类都没有执行消息转发时,会报什么样的错误?
- NSProxy 会直接报 methodSignatureForSelector 方法错误
- NSObject 会报 [NSObject timerTest] 没有找到 错误
他们两个的执行流程:
- 继承至 NSObject 的类:
- 先去类对象里面找 -> 父类的类对象找 -> 缓存中 -> 搜索
- 继承至 NSProxy 的类:
- 先会在本类中找,如果没有直接消息转发。不会去 类对象找,父类的类对象中找
执行结果: 1 ,0
解释:
- isKindOfClass : 检查 左边 是否是 右边的 类型或 子类
- 为什么 proxy2 打印结果是0?
- 这是因为 proxy 的类型是 MJProxy1 类型, WYProxy1 类型 继承至 NSObject。
- 也就是说 proxy2 是继承至 NSObject 。说明 proxy2 是 普通的OC 对象。
- 那么 isKindOfClass 就按照普通 OC 对象处理 。NSObject != viewController 类型 所以 打印为0
- 那为什么 proxy1 打印结果是1 ?
- 因为 proxy1 的类型是 WYProxy2 .WYProxy2 继承至 NSProxy .
- 只要是继承至 NSProxy ,那么大部分就会进入消息转发阶段。也就意味着,在 调用 isKindOfClass 的时候,也进入了消息转发。就会进入到
methodSignatureForSelector
和forwardInvocation
这两个方法。它的调用顺序就改成了 target 。也就是说先拿到[WYProxy proxyWithtarget: vc]
中的 vc ,然后[proxy1 isKindOfClass:[ViewController class]]
变成了[vc isKindOfClass:[ViewController class]]
四、不准时
NSTimer 依赖于 RunLoop,如果 runLoop的任务过于繁重,可能会导致 NSTimer不准时。
* 如果 runLoop 在处理定时器的同时,也在处理其他任务,那么就会导致 NSTimer 不准时。
不准时的含义:
例如每隔一秒钟执行一个任务,这个时候runloop 应该怎么执行定时器?
- 首先要知道的是,runloop 的本质是循环(也就是跑圈)
- 那 runloop 是怎么执行定时器的?
- 当 runloop 循环一圈回来,会计算一下刚才累计的时间是多少。例如:累计了0.2秒,但定时器是1.0秒才会触发任务。这个时候 runloop 就会 再去跑。也许跑到第五圈的时候,发现时间累计到了1秒钟。这个时候才回去处理定时器。
- 但问题是,每一次循环的时间不是固定的。因为每次循环的任务不是固定的。假设 第一圈0.2秒,第二圈0.3秒,第三圈0.3秒,三圈一共0.8秒,第四圈因为任务多,跑了0.5秒 加起来 1.3秒以后,执行 定时器任务。