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];
    }];
       
}
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 >协议.其实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 > 🚗🚗🚗🚙🚙🚙

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容