OC-内存管理(一)-定时器NSTimer NSProxy消息转发
NSTimer
NSTimer
会对target
产生强引用,如果target
再对NSTimer
产生强引用就会产生循环引用.我们直接用代码演示:
@interface ViewController ()
@property (nonatomic,strong)NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- (void)linkTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s---%@...",__func__,self.obj);
}
@end
以上代码每秒中调用一次timerTest
,即使已经退出当前控制器还会继续调用.虽然我们已经重写了dealloc
方法,并且在dealloc
方法内部调用了timer
的invalidate
方法,并且手动把timer
置为nil
.
上述代码的
dealloc
是永远不会调用的,因为timer
和viewcontroller
已经产生了循环引用.有人会想使用__weak
修饰self
不就可以了吗?__weak typeof(self)weakSelf = self; self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(timerTest) userInfo:nil repeats:YES];
结果是这样仍然也解决不了问题,之前我们使用
__weak
是解决block
的循环引用的.之所以能解决block
的循环引用是因为blcok
内部捕获的外部变量的引用关系取决于外部变量的修饰符,如果外面是个强指针,blcok引用的时候内部就用强指针保存,如果外面是个弱指针,block引用的时候内部就用弱指针保存(遇强捕强,遇弱捕弱).而在NSTimer
内部会强引用传进来的target
,都是传入一个内存地址,定时器内部都是对这个内存地址产生强引用,所以传弱指针没用的。.
那我们怎么解决这个问题呢?可以换一种初始化方法,使用带有block
的初始化方法:
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf timerTest];
}];
}
这样就能解决循环引用的问题,self对定时器强引用,定时器对block强引用,block对self弱引用,不产生循环引用。运行代码,从当前VC返回,timer定时器不打印了,说明上面代码有效。
CADisplayLink
#import "ViewController.h"
#import "MJProxy.h"
@interface ViewController ()
@property (strong, nonatomic) CADisplayLink *link;
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- (void)linkTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self.link invalidate]; //让定时器停止工作
}
@end
- CADisplayLink这个定时器不能设置时间,保证调用频率和屏幕刷帧频率一致。屏幕刷帧频率大概是60FPS,所以这个定时器一般一秒钟调用60次。
- CADisplayLink、对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用。
运行上面代码,从当前VC返回,但是两个定时器还是一直在打印,说明上面代码的确有循环引用问题。
当前VC返回
上面代码的确有循环引用问题。
上面,我们使用了block 加 __weak typeof(self) weakSelf = self;
的方式解决了NSTimer循环引用的问题。我们也可以用中间对象解决。
在没使用中间对象之前,引用关系是,self里面的timer强引用着定时器,定时器里面的target强引用着self,产生循环引用。
添加中间对象之后,如下图:
创建一个中间层,让NSTimer
强引用这个中间层,中间层弱引用ViewController
,就打破了之前的循环引用关系:控制器中的timer强引用着定时器,定时器中的target强引用着中间对象,中间对象的target弱引用着控制器,这样就不会产生循环引用了。
我们需要做的就是当定时器找到中间对象,想要调用中间对象的timerTest
方法时,我们让中间对象调用控制器的timerTest
方法。
创建中间对象
//------------------------ MJProxy.h -------------
#import
@interface MJProxy : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target; //用弱引用
@end
//------------------------ MJProxy.m -------------
#import "MJProxy.h"
@implementation MJProxy
+ (instancetype)proxyWithTarget:(id)target
{
MJProxy *proxy = [[MJProxy alloc] init];
proxy.target = target;
return proxy;
}
//中间对象找不到timerTest方法,就通过消息转发,转发给控制器
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return self.target;
}
@end
//------------------------ ViewController.m -------------
#import "ViewController.h"
#import "MJProxy.h"
@interface ViewController ()
@property (strong, nonatomic) CADisplayLink *link;
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 保证调用频率和屏幕的刷帧频率一致,60FPS
self.link = [CADisplayLink displayLinkWithTarget:[MJProxy proxyWithTarget:self] selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
//
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MJProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- (void)linkTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self.link invalidate];
[self.timer invalidate];
}
@end
上面代码,中间对象弱引用着控制器。当定时器启动后,会从中间对象中寻找timerTest方法,中间对象中找不到timerTest方法,就通过消息转发,转发给控制器,最后调用控制器的timerTest方法。
需要注意的是
CADisplayLink
也需要手动调用invalidate
才能停止.运行代码,从当前VC返回,两个定时器都不打印了,说明使用中间对象有效。
NSProxy
以前我们说过,iOS中所有的类都继承于NSObject,但是有一个特殊的类:NSProxy(n. 代理人;委托书;代用品)
进入NSProxy的定义:
@interface NSProxy {
Class isa;
}
再看看NSObject的定义:
@interface NSObject {
Class isa ;
}
可以发现,NSProxy和NSObject是同一级别的,都遵守NSObject协议。他们都没有继承任何类,都实现了< NSObject >
协议.其实NSProxy
和NSObject
一样都是基类.只不过NSProxy
是专门用来做代理的类.
NSProxy的作用
那么NSProxy有什么用呢?
其实,NSProxy就是专门做消息转发的。
那么NSProxy比上面继承于NSObject的中间对象好在哪里呢?
如果调用的是继承于NSObject某个类的方法,那么它的方法寻找流程就是先查缓存,再走消息发送、动态方法解析、消息转发,效率低。
如果调用的是继承于NSProxy某个类的方法,那么它的方法寻找流程是,先看自己有没有这个方法,如果没有,就直接一步到位,来到methodSignatureForSelector方法,效率高。
NSProxy的使用
自定义MJProxy继承于NSProxy,使用如下:
//------------------------ MJProxy.h -------------
#import
@interface MJProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
//------------------------ MJProxy.m -------------
#import "MJProxy.h"
@implementation MJProxy
+ (instancetype)proxyWithTarget:(id)target
{
// NSProxy对象不需要调用init,因为它本来就没有init方法
MJProxy *proxy = [MJProxy alloc];
proxy.target = target;
return proxy;
}
//返回方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
return [self.target methodSignatureForSelector:sel];//
}
//NSInvocation封装了一个方法调用,包括:方法调用者、方法名、方法参数
- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
}
@end
当定时器启动时,会直接到MJProxy中寻找timerTest方法,MJProxy中没有timerTest方法,就会直接调用methodSignatureForSelector方法进行消息转发,转发给控制器后,最后调用控制器的timerTest方法。
NSProxy补充
int main(int argc, char * argv[]) {
@autoreleasepool {
ViewController *vc = [[ViewController alloc] init];
MJProxy *proxy = [MJProxy proxyWithTarget:vc]; //继承于NSProxy的类
MJProxy1 *proxy1 = [MJProxy1 proxyWithTarget:vc]; //继承于NSObject的类
NSLog(@"%d %d",
[proxy isKindOfClass:[ViewController class]],
[proxy1 isKindOfClass:[ViewController class]]);
//打印:1 0
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
RUN >
看到继承自
NSObject
的为false
,而继承自NSProxy
的为true
.这是因为NSProxy
直接把isKindOfClass
转发给了ViewController
处理,所以最后就是ViewController isKindOfClass [self class]
结果就为true
.在GUNstep的NSProxy.m文件中,找到isKindOfClass方法的实现:
- (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; }
这个方法直接进行了消息转发,直接转发给ViewController了,最后通过方法寻找流程找到的是ViewController的isKindOfClass方法,所以最后就是调用ViewController的isKindOfClass方法,所以上面会打印1。
特别备注
本系列文章总结自MJ老师在腾讯课堂iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!