iOS底层原理 - 内存管理 之 定时器(一)

面试题引发的思考:

Q: 使用CADisplayLinkNSTimer有什么注意点?

  • 循环引用:
    CADisplayLink、NSTimer会对target产生强引用,如果target又对自身产生强引用,那么就会引发 循环引用。
  • 不准时:
    CADisplayLink、NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致CADisplayLinkNSTimer不准时。

Q: 使用CADisplayLinkNSTimer如何避免循环引用?

  • 使用scheduledTimerWithTimeInterval: repeats: block:方法;
  • 使用代理对象。

Q: 简述NSProxy

  • NSProxy是专门用来做消息转发的类,相比NSObject类来说NSProxy更轻量级。
  • 通过NSProxy可以帮助Objective-C间接的实现多重继承的功能。

1. 实例:

(1) NSTimer使用时产生循环引用

// TODO: -----------------  ViewController类  -----------------
@interface ViewController ()
// 循环引用问题:self对NSTimer对象产生强引用
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    //  循环引用问题:NSTimer对象对self产生强引用
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
    NSLog(@"%s", __func__);
}
- (void)dealloc {
    NSLog(@"%s", __func__);
    [self.timer invalidate];
}
@end
循环引用

由以上分析可知:

selfNSTimer对象产生强引用,而NSTimer对象又会对self产生强引用,此时会造成循环引用问题,导致无法NSTimer对象无法随着ViewController的释放而释放。


(2) CADisplayLink使用时产生循环引用

// TODO: -----------------  ViewController类  -----------------
@interface ViewController ()
// 循环引用问题:self对CADisplayLink对象产生强引用
@property (nonatomic, strong) CADisplayLink *link;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // 保证调用频率和屏幕的刷帧频率一致,60FPS
    //  循环引用问题:CADisplayLink对象对self产生强引用
    self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)linkTest {
    NSLog(@"%s", __func__);
}
- (void)dealloc {
    NSLog(@"%s", __func__);
    [self.link invalidate];
}
@end
循环引用

由以上分析可知:

selfCADisplayLink对象产生强引用,而CADisplayLink对象又会对self产生强引用,此时会造成循环引用问题,导致无法CADisplayLink对象无法随着ViewController的释放而释放。


2. 解决方法探究:

(1) 解决方案一:使用block

// TODO: -----------------  ViewController类  -----------------
- (void)viewDidLoad {
    [super viewDidLoad];

    // weakSelf - block内部用的是弱指针,对外面的对象产生弱引用
    // self - block内部用的是强指针,对外面的对象产生强引用
    __weak typeof(self) weakSelf = self;

    // weakSelf只是把地址赋值给target,而target在NSTimer内部是强引用
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target: weakSelf selector:@selector(timerTest) userInfo:nil repeats:YES];
}
解决循环引用方案

由以上分析可知:

直接使用弱指针是无法解决循环引用问题的,因为weakSelf只是把地址赋值给target,而targetNSTimer内部是强引用。而NSTimer是不开源的,无法修改成弱指针。

弱指针是针对block的方案,block内部用的是弱指针,对外面的对象产生弱引用。

所以可以使用以下方法避免循环引用:

// TODO: -----------------  ViewController类  -----------------
- (void)viewDidLoad {
    [super viewDidLoad];
    // 弱指针是针对block的方案
    __weak typeof(self) weakSelf = self;
 
    // NSTimer对象对block产生强引用
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        // block对self产生弱引用
        [weakSelf timerTest];
    }];
}

(2) 解决方案二:使用代理对象

1> 方案分析
解决循环引用方案

由以上分析可知:

直接使用弱指针是无法解决循环引用问题的,因为weakSelf只是把地址赋值给target,而targetNSTimer内部是强引用。而NSTimer是不开源的,无法修改成弱指针。

如上图使用代理对象:

selfNSTimer对象产生强引用,NSTimer对象对OtherObject对象产生强引用,而OtherObject对象对self产生弱引用,此时会避免循环引用,NSTimer对象会随着ViewController的释放而释放。

a> 对NSTimer使用代码如下:
// TODO: -----------------  ViewController类  -----------------
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad]; 
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MYObjectProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
    NSLog(@"%s", __func__);
}
- (void)dealloc {
    NSLog(@"%s", __func__);
    [self.timer invalidate];
}
@end

// TODO: -----------------  MYObjectProxy类  -----------------
@interface MYObjectProxy : NSObject
@property (nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end

@implementation MYObjectProxy
+ (instancetype)proxyWithTarget:(id)target {
    MYObjectProxy *proxy = [[MYObjectProxy alloc] init];
    proxy.target = target;
    return proxy;
}
// 消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
    // objc_msgSend(self.target, aSelector);
    return self.target;
}
@end
b> 对CADisplayLink使用代码如下:
// TODO: -----------------  ViewController类  -----------------
@interface ViewController ()
// 没有block方案
@property (nonatomic, strong) CADisplayLink *link;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad]; 
    self.link = [CADisplayLink displayLinkWithTarget:[MYObjectProxy proxyWithTarget:self] selector:@selector(linkTest)];
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)timerTest {
    NSLog(@"%s", __func__);
}
- (void)dealloc {
    NSLog(@"%s", __func__);
    [self.timer invalidate];
}
@end

// TODO: -----------------  MYObjectProxy类  -----------------
@interface MYObjectProxy : NSObject
@property (nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end

@implementation MYObjectProxy
+ (instancetype)proxyWithTarget:(id)target {
    MYObjectProxy *proxy = [[MYObjectProxy alloc] init];
    proxy.target = target;
    return proxy;
}
// 消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
    // objc_msgSend(self.target, aSelector);
    return self.target;
}
@end
2> 方案优化:使用NSProxy

源码如下:

// NSProxy声明
@interface NSProxy <NSObject> { 
    Class isa;
}
// NSObject声明
@interface NSObject <NSObject> {  
    Class isa;
}

由源码可知:

  • NSProxy、NSObject两者都是基类;
  • 两者区别在与方法调用执行流程不同。

MYObjectProxy继承自NSObject其方法调用执行流程:

objc_msgSend()的执行流程可以分为三个阶段:

  • 消息发送阶段:负责从类及父类的缓存列表及方法列表查找方法;
  • 动态解析阶段:如果消息发送阶段没有找到方法,则会进入动态解析阶段,负责动态的添加方法实现;
  • 消息转发阶段:如果也没有实现动态解析方法,则会进行消息转发阶段,将消息转发给可以处理消息的接收者来处理;
  • 报错:如果也没有实现消息转发方法,会报错unrecognzied selector sent to instance

MYProxy继承自NSProxy其方法调用执行流程:

  • 直接进入消息转发阶段:将消息转发给可以处理消息的接收者来处理;
  • 报错:如果也没有实现消息转发方法,会报错unrecognzied selector sent to instance。

由以上结论可知:

  • NSProxy是专门用来做消息转发的类,相比NSObject类来说NSProxy更轻量级。
  • 通过NSProxy可以帮助Objective-C间接的实现多重继承的功能。
NSProxy使用代码如下:
// TODO: -----------------  ViewController类  -----------------
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad]; 
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MYProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
    NSLog(@"%s", __func__);
}
- (void)dealloc {
    NSLog(@"%s", __func__);
    [self.timer invalidate];
}
@end

// TODO: -----------------  MYProxy类  -----------------
@interface MYProxy : NSProxy
@property (nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end

@implementation MYProxy
+ (instancetype)proxyWithTarget:(id)target {
    // NSProxy对象不需要调用init,没有init方法
    MYProxy *proxy = [MYProxy alloc];
    proxy.target = target;
    return proxy;
}
// 消息转发 - 效率高
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}
@end

3. 延伸:

Q: 以下代码输出为何为 “0 - 1” ?

// TODO: -----------------  ViewController类  -----------------
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    ViewController *vc = [[ViewController alloc] init];

    MYObjectProxy *objectProxy = [MYObjectProxy proxyWithTarget:vc];
    MYProxy *proxy = [MYProxy proxyWithTarget:vc];

    NSLog(@"%d - %d",
          [objectProxy isKindOfClass:[ViewController class]],
          [proxy isKindOfClass:[ViewController class]]);
}

objectProxy对象是MYObjectProxy类型,继承自NSObject;
MYObjectProxy不是UIViewController类型及其子类,输出结果为“0”。

proxy对象是MYProxy类型,继承自NSProxy;
proxy对象调用isKindOfClass方法时进行消息转发,即调用target进行转发;
那么[proxy isKindOfClass:[ViewController class]]相当于[vc isKindOfClass:[ViewController class]];
proxyUIViewController类型及其子类,输出结果为“1”。

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

推荐阅读更多精彩内容