今天看啥  ›  专栏  ›  蒋斌文

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

蒋斌文  · 简书  ·  · 2021-05-24 15:57

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];
    }];
       
}
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返回

image-20210524152412491

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

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

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

image-20210524153152631

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

中间对象

创建一个中间层,让 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方法。

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 > 协议.其实 NSProxy NSObject 一样都是基类.只不过 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 > 🚗🚗🚗🚙🚙🚙

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




原文地址:访问原文地址
快照地址: 访问文章快照