内存管理:CADisplayLink、NSTimer、NSProxy 使用注意

  • 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 的时候,也进入了消息转发。就会进入到 methodSignatureForSelectorforwardInvocation 这两个方法。它的调用顺序就改成了 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秒以后,执行 定时器任务。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值