程序员社区

OC-内存管理(一)-定时器NSTimer NSProxy消息转发

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方法内部调用了timerinvalidate方法,并且手动把timer置为nil.

上述代码的dealloc是永远不会调用的,因为timerviewcontroller已经产生了循环引用.有人会想使用__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];
    }];
       
}
OC-内存管理(一)-定时器NSTimer NSProxy消息转发插图
image-20210524152043211

这样就能解决循环引用的问题,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
  1. CADisplayLink这个定时器不能设置时间,保证调用频率和屏幕刷帧频率一致。屏幕刷帧频率大概是60FPS,所以这个定时器一般一秒钟调用60次。
  2. CADisplayLink、对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用。

运行上面代码,从当前VC返回,但是两个定时器还是一直在打印,说明上面代码的确有循环引用问题。

当前VC返回

OC-内存管理(一)-定时器NSTimer NSProxy消息转发插图1
image-20210524152412491

上面代码的确有循环引用问题。

上面,我们使用了block 加 __weak typeof(self) weakSelf = self;的方式解决了NSTimer循环引用的问题。我们也可以用中间对象解决。

在没使用中间对象之前,引用关系是,self里面的timer强引用着定时器,定时器里面的target强引用着self,产生循环引用。

OC-内存管理(一)-定时器NSTimer NSProxy消息转发插图2
image-20210524153152631

添加中间对象之后,如下图:

OC-内存管理(一)-定时器NSTimer NSProxy消息转发插图3
中间对象

创建一个中间层,让NSTimer强引用这个中间层,中间层弱引用ViewController,就打破了之前的循环引用关系:控制器中的timer强引用着定时器,定时器中的target强引用着中间对象,中间对象的target弱引用着控制器,这样就不会产生循环引用了。

我们需要做的就是当定时器找到中间对象,想要调用中间对象的timerTest方法时,我们让中间对象调用控制器的timerTest方法。

创建中间对象

//------------------------??? MJProxy.h ??? -------------
#import <Foundation/Foundation.h>

@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方法。

OC-内存管理(一)-定时器NSTimer NSProxy消息转发插图4
image-20210524154028350

需要注意的是CADisplayLink也需要手动调用invalidate才能停止.

运行代码,从当前VC返回,两个定时器都不打印了,说明使用中间对象有效。

NSProxy

以前我们说过,iOS中所有的类都继承于NSObject,但是有一个特殊的类:NSProxy(n. 代理人;委托书;代用品)

进入NSProxy的定义:

@interface NSProxy <NSObject> {
    Class   isa;
}

再看看NSObject的定义:

@interface NSObject <NSObject> {
    Class isa ;
}

可以发现,NSProxy和NSObject是同一级别的,都遵守NSObject协议。他们都没有继承任何类,都实现了< NSObject >协议.其实NSProxyNSObject一样都是基类.只不过NSProxy是专门用来做代理的类.

NSProxy的作用

那么NSProxy有什么用呢?
其实,NSProxy就是专门做消息转发的

那么NSProxy比上面继承于NSObject的中间对象好在哪里呢?

如果调用的是继承于NSObject某个类的方法,那么它的方法寻找流程就是先查缓存,再走消息发送、动态方法解析、消息转发,效率低。
如果调用的是继承于NSProxy某个类的方法,那么它的方法寻找流程是,先看自己有没有这个方法,如果没有,就直接一步到位,来到methodSignatureForSelector方法,效率高。

NSProxy的使用

自定义MJProxy继承于NSProxy,使用如下:

//------------------------??? MJProxy.h ??? -------------
#import <Foundation/Foundation.h>

@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 > ??????

OC-内存管理(一)-定时器NSTimer NSProxy消息转发插图5
image-20210524155143745

看到继承自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对象/关联对象/多线程/内存管理/性能优化,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!

赞(0) 打赏
未经允许不得转载:IDEA激活码 » OC-内存管理(一)-定时器NSTimer NSProxy消息转发

一个分享Java & Python知识的社区