iOS底層原理 - 內(nèi)存管理 之 定時器(一)

面試題引發(fā)的思考:

Q: 使用CADisplayLink抑诸、NSTimer有什么注意點谍椅?

  • 循環(huán)引用:
    CADisplayLink丸升、NSTimer會對target產(chǎn)生強引用勉盅,如果target又對自身產(chǎn)生強引用佑颇,那么就會引發(fā) 循環(huán)引用。
  • 不準時:
    CADisplayLink草娜、NSTimer依賴于RunLoop挑胸,如果RunLoop的任務過于繁重,可能會導致CADisplayLink宰闰、NSTimer不準時茬贵。

Q: 使用CADisplayLinkNSTimer如何避免循環(huán)引用移袍?

  • 使用scheduledTimerWithTimeInterval: repeats: block:方法解藻;
  • 使用代理對象。

Q: 簡述NSProxy葡盗?

  • NSProxy是專門用來做消息轉(zhuǎn)發(fā)的類螟左,相比NSObject類來說NSProxy更輕量級。
  • 通過NSProxy可以幫助Objective-C間接的實現(xiàn)多重繼承的功能。

1. 實例:

(1) NSTimer使用時產(chǎn)生循環(huán)引用

// TODO: -----------------  ViewController類  -----------------
@interface ViewController ()
// 循環(huán)引用問題:self對NSTimer對象產(chǎn)生強引用
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    //  循環(huán)引用問題:NSTimer對象對self產(chǎn)生強引用
    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
循環(huán)引用

由以上分析可知:

selfNSTimer對象產(chǎn)生強引用胶背,而NSTimer對象又會對self產(chǎn)生強引用巷嚣,此時會造成循環(huán)引用問題,導致無法NSTimer對象無法隨著ViewController的釋放而釋放钳吟。


(2) CADisplayLink使用時產(chǎn)生循環(huán)引用

// TODO: -----------------  ViewController類  -----------------
@interface ViewController ()
// 循環(huán)引用問題:self對CADisplayLink對象產(chǎn)生強引用
@property (nonatomic, strong) CADisplayLink *link;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // 保證調(diào)用頻率和屏幕的刷幀頻率一致廷粒,60FPS
    //  循環(huán)引用問題:CADisplayLink對象對self產(chǎn)生強引用
    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
循環(huán)引用

由以上分析可知:

selfCADisplayLink對象產(chǎn)生強引用,而CADisplayLink對象又會對self產(chǎn)生強引用砸抛,此時會造成循環(huán)引用問題评雌,導致無法CADisplayLink對象無法隨著ViewController的釋放而釋放树枫。


2. 解決方法探究:

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

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

    // weakSelf - block內(nèi)部用的是弱指針直焙,對外面的對象產(chǎn)生弱引用
    // self - block內(nèi)部用的是強指針,對外面的對象產(chǎn)生強引用
    __weak typeof(self) weakSelf = self;

    // weakSelf只是把地址賦值給target砂轻,而target在NSTimer內(nèi)部是強引用
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target: weakSelf selector:@selector(timerTest) userInfo:nil repeats:YES];
}
解決循環(huán)引用方案

由以上分析可知:

直接使用弱指針是無法解決循環(huán)引用問題的奔誓,因為weakSelf只是把地址賦值給target,而targetNSTimer內(nèi)部是強引用搔涝。而NSTimer是不開源的厨喂,無法修改成弱指針。

弱指針是針對block的方案庄呈,block內(nèi)部用的是弱指針蜕煌,對外面的對象產(chǎn)生弱引用。

所以可以使用以下方法避免循環(huán)引用:

// TODO: -----------------  ViewController類  -----------------
- (void)viewDidLoad {
    [super viewDidLoad];
    // 弱指針是針對block的方案
    __weak typeof(self) weakSelf = self;
 
    // NSTimer對象對block產(chǎn)生強引用
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        // block對self產(chǎn)生弱引用
        [weakSelf timerTest];
    }];
}

(2) 解決方案二:使用代理對象

1> 方案分析
解決循環(huán)引用方案

由以上分析可知:

直接使用弱指針是無法解決循環(huán)引用問題的诬留,因為weakSelf只是把地址賦值給target斜纪,而targetNSTimer內(nèi)部是強引用。而NSTimer是不開源的文兑,無法修改成弱指針盒刚。

如上圖使用代理對象:

selfNSTimer對象產(chǎn)生強引用,NSTimer對象對OtherObject對象產(chǎn)生強引用绿贞,而OtherObject對象對self產(chǎn)生弱引用因块,此時會避免循環(huán)引用,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;
}
// 消息轉(zhuǎn)發(fā)
- (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;
}
// 消息轉(zhuǎn)發(fā)
- (id)forwardingTargetForSelector:(SEL)aSelector {
    // objc_msgSend(self.target, aSelector);
    return self.target;
}
@end
2> 方案優(yōu)化:使用NSProxy

源碼如下:

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

由源碼可知:

  • NSProxy涡上、NSObject兩者都是基類;
  • 兩者區(qū)別在與方法調(diào)用執(zhí)行流程不同拒名。

MYObjectProxy繼承自NSObject其方法調(diào)用執(zhí)行流程:

objc_msgSend()的執(zhí)行流程可以分為三個階段:

  • 消息發(fā)送階段:負責從類及父類的緩存列表及方法列表查找方法吩愧;
  • 動態(tài)解析階段:如果消息發(fā)送階段沒有找到方法,則會進入動態(tài)解析階段靡狞,負責動態(tài)的添加方法實現(xiàn)耻警;
  • 消息轉(zhuǎn)發(fā)階段:如果也沒有實現(xiàn)動態(tài)解析方法,則會進行消息轉(zhuǎn)發(fā)階段,將消息轉(zhuǎn)發(fā)給可以處理消息的接收者來處理甘穿;
  • 報錯:如果也沒有實現(xiàn)消息轉(zhuǎn)發(fā)方法腮恩,會報錯unrecognzied selector sent to instance

MYProxy繼承自NSProxy其方法調(diào)用執(zhí)行流程:

  • 直接進入消息轉(zhuǎn)發(fā)階段:將消息轉(zhuǎn)發(fā)給可以處理消息的接收者來處理温兼;
  • 報錯:如果也沒有實現(xiàn)消息轉(zhuǎn)發(fā)方法秸滴,會報錯unrecognzied selector sent to instance

由以上結論可知:

  • NSProxy是專門用來做消息轉(zhuǎn)發(fā)的類募判,相比NSObject類來說NSProxy更輕量級荡含。
  • 通過NSProxy可以幫助Objective-C間接的實現(xiàn)多重繼承的功能。
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對象不需要調(diào)用init届垫,沒有init方法
    MYProxy *proxy = [MYProxy alloc];
    proxy.target = target;
    return proxy;
}
// 消息轉(zhuǎn)發(fā) - 效率高
- (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對象調(diào)用isKindOfClass方法時進行消息轉(zhuǎn)發(fā),即調(diào)用target進行轉(zhuǎn)發(fā)登淘;
那么[proxy isKindOfClass:[ViewController class]]相當于[vc isKindOfClass:[ViewController class]]箫老;
proxyUIViewController類型及其子類,輸出結果為“1”黔州。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末耍鬓,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子辩撑,更是在濱河造成了極大的恐慌界斜,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件合冀,死亡現(xiàn)場離奇詭異各薇,居然都是意外死亡,警方通過查閱死者的電腦和手機君躺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門峭判,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捆愁,“玉大人啡直,你說我怎么就攤上這事甥捺〔杓” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵茧泪,是天一觀的道長清笨。 經(jīng)常有香客問我业稼,道長,這世上最難降的妖魔是什么横漏? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任谨设,我火速辦了婚禮,結果婚禮上缎浇,老公的妹妹穿的比我還像新娘扎拣。我一直安慰自己,他們只是感情好素跺,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布二蓝。 她就那樣靜靜地躺著,像睡著了一般指厌。 火紅的嫁衣襯著肌膚如雪刊愚。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天仑乌,我揣著相機與錄音百拓,去河邊找鬼。 笑死晰甚,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的决帖。 我是一名探鬼主播厕九,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼地回!你這毒婦竟也來了扁远?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤刻像,失蹤者是張志新(化名)和其女友劉穎畅买,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體细睡,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡谷羞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了溜徙。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片湃缎。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蠢壹,靈堂內(nèi)的尸體忽然破棺而出嗓违,到底是詐尸還是另有隱情,我是刑警寧澤图贸,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布蹂季,位于F島的核電站冕广,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏偿洁。R本人自食惡果不足惜佳窑,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望父能。 院中可真熱鬧神凑,春花似錦、人聲如沸何吝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽爱榕。三九已至瓣喊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間黔酥,已是汗流浹背藻三。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留跪者,地道東北人棵帽。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像渣玲,于是被迫代替她去往敵國和親逗概。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容