主要讲解CADisplayLink 和 NSTimer 的循环引用问题
iOS 内存管理 部分一
iOS 内存管理 部分二
iOS 内存管理 部分三
iOS 内存管理 部分四
1. CADisplayLink 和 NSTimer的循环引用
关于什么是 CADisplayLink
不再赘述, 网上有很多讲解很好的教程; 正常的使用时我们这样写, 但是这样写即使是在dealloc
中写了invalid
也不会释放, 因为有强引用环的存在,
#NSTiemr 的使用
- (void)timerAction {
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(printAction) userInfo:nil repeats:YES];
[self.timer fire];
}
- (void)printAction {
NSLog(@"%s", __func__);
}
- (void)dealloc {
[self.timer invalidate];
NSLog(@"%s", __func__);
}
#CADisplayLink 的使用
- (void)displaylinkAction {
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(printAction)];
[self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)printAction {
NSLog(@"%s", __func__);
}
- (void)dealloc {
[self.link invalidate];
NSLog(@"%s", __func__);
}
他们的引用关系如下, 所以导致不能释放;
2. 解决方案
1 . 使用 Block
通过使用_weak
, 来使block
对 self
弱引用, 进而打到打破循环引用的问题;关于block
对self
的引用问题请看这篇文章;
测试代码
- (void)timerBlockAction {
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf printAction];
}];
}
2. 使用中转转发对象
使用一个三方转发对象来断开这个引用环
2.1 为了对比我们对
NSTimer
使用NSObject
的类型来中转 转发;
代码如下
///NSObject 类型中转转发对象的.h文件
#import
@interface ObjectObj : NSObject
+ (ObjectObj *)ShareTarget:(id)obj;
@property (nonatomic, weak) id target;
@end
///NSObject 类型中转转发对象的.m文件
#import "ObjectObj.h"
@implementation ObjectObj
+ (ObjectObj *)ShareTarget:(id)obj {
ObjectObj *object = [[ObjectObj alloc] init];
object.target = obj;
return object;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.target;
}
@end
#调用部分
- (void)timerAction {
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[ObjectObj ShareTarget:self] selector:@selector(printAction) userInfo:nil repeats:YES];
[self.timer fire];
}
我们从上面的图中可以确定只要环中有一个弱引用就可以破环循环引用;但是为什么这种方式可行呢, 其实这样做的本质是使用了消息转发, 中转对象并不能响应方法printAction()
, 所以会进行方法查找(父类/缓存)-动态解析- 消息转发, 最终返回一个可以处理printAction()
方法的对象, 最后的效果就是 VC
在执行pop
操作时可以进行调用dealloc()
方法释放内存; 关于消息发送方法的查找过程;
2.2为了对比我们对CADisplayLink
使用NSProxy
的类型来中转 转发;
代码如下
///NSProxy 类型中转转发对象的.h文件
#import
@interface ProxyObj : NSProxy
+ (ProxyObj *)ShareTarget:(id)obj;
@property (nonatomic, weak) id target;
@end
///NSProxy 类型中转转发对象的.m文件
#import "ProxyObj.h"
@implementation ProxyObj
+ (ProxyObj *)ShareTarget:(id)obj {
///注意NSProxy实例对象创建不需要 init
ProxyObj *object = [ProxyObj alloc] ;
object.target = obj;
return object;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
///返回 target 的方法签名
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
///将invocation的 target 设置为 self.target
invocation.target = self.target;
[invocation invoke];
}
@end
#调用部分
- (void)displaylinkAction {
self.link = [CADisplayLink displayLinkWithTarget:[ProxyObj ShareTarget:self] selector:@selector(printAction)];
[self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
跟上面的NSObject
类型的中转转发一样, 也可以实现最终效果, 但是使用NSProxy
效率更高, 因为NSObject
的过程要经过方法查找(父类/缓存)-动态解析-消息转发三个阶段, 而 NSProxy
只有消息转发这个步骤(从其 API
可以查看到它只有消息转发的方法, 没有其他两个步骤的方法), 省去前面两个步骤, 从而使效率更高;
2. 使用 NSProxy 的注意事项
在讨论这个问题之前, 我们先补充下什么是 GNUStep
:
GNUStep
是 GNU 计划的项目之一, 我们都知道 iOS
中Foundation
框架是不开源的; 因此 GNUStep
将Foundation
重新实现了一遍, 虽然不是Apple
官方的源码, 但是目前仍然是最有参考价值的源码;
通过上面我们知道了NSProxy
的用法, 但是有一些注意事项我们需要注意, 看下面代码
- (void)proxyAttention {
ObjectObj *obj1 = [ObjectObj ShareTarget:self];
ProxyObj *obj2 = [ProxyObj ShareTarget:self];
NSLog(@"%ld___%ld", (long)[obj1 isKindOfClass:[ViewController1 class]],
(long)[obj2 isKindOfClass:[ViewController1 class]]);
}
#打印结果为
2020-08-03 15:13:22.910122+0800 MemoryMore1[6035:1149461] 0___1
至于第一个为什么会打印0, 我们可以看这篇文章中的isKindOfClass()
方法的讲解, 但是为什么第二个会打印1呢;
这个就需要去GNUStep
源码中找下NSProxy
的实现;
/**
* Calls the -forwardInvocation: method to determine if the 'real' object
* referred to by the proxy is an instance of the specified class.
* Returns the result.
* NB. The default operation of -forwardInvocation: is to raise an exception.
*/
- (BOOL) isKindOfClass: (Class)aClass
{
NSMethodSignature *sig;
NSInvocation *inv;
BOOL ret;
sig = [self methodSignatureForSelector: _cmd];
inv = [NSInvocation invocationWithMethodSignature: sig];
[inv setSelector: _cmd];
[inv setArgument: &aClass atIndex: 2];
[self forwardInvocation: inv];
[inv getReturnValue: &ret];
return ret;
}
从源码中我们可以看到, NSProxy
的isKindOfClass()
方法不是跟NSObject
那种进行判断是否是一个类或者子类, 而是调用了消息转发的相关方法, 因此实际去跟isKindOfClass()
进行判断的是NSInvocation
内部的target
; 由于[ProxyObj ShareTarget:self]
初始化时传入的当前VC
并在消息转发时将其设置为NSInvocation
内部的target
, 所以打印出二者相等, 打印出 1;
参考文章和下载链接
文中测试代码
CADisplayLink 详解
NSProxy 简析
GNUStep
GNU 官网
Foundation 的 GNU 下载