日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

iOS之深入解析内存管理NSTimer的强引用问题

發(fā)布時間:2024/5/21 编程问答 44 豆豆
生活随笔 收集整理的這篇文章主要介紹了 iOS之深入解析内存管理NSTimer的强引用问题 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

一、強(qiáng)引用問題分析

  • 現(xiàn)在有兩個控制器 A、B,從 A push 到 B 控制器,在 B 控制器中有如下代碼:
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(popHome) userInfo:nil repeats:YES];[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
  • 當(dāng)從控制器 B pop 回到控制器 A 時,我們發(fā)現(xiàn)定時器沒有停止,其 popHome 方法仍然在執(zhí)行,這是為什么呢?
  • 在控制器 B 的 dealloc 方法打上斷點(diǎn),可以看到程序并沒有執(zhí)行。因此可以得出,控制器 B 沒有被釋放,即控制器 B 沒有執(zhí)行 dealloc 方法,從而導(dǎo)致 timer 也無法停止運(yùn)行和釋放。
  • 重寫 didMoveToParentViewController 方法,可以看到:當(dāng)控制器 B 退出到上層控制器的時候消除了引用,dealloc 方法被調(diào)用,timer 被銷毀:
- (void)didMoveToParentViewController:(UIViewController *)parent {if (parent == nil) {[self.timer invalidate];self.timer = nil;NSLog(@"timer 被釋放");}}
  • 定義 timer 時,可以采用閉包的形式,不需要指定 target,就不會產(chǎn)生 timer 無法被釋放的問題:
- (void)blockTimer {self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {NSLog(@"timer pop - %@", timer);}];}
  • 經(jīng)過上面的兩種方式,都可以正常處理 timer 釋放的問題,那么這又是為什么呢?
  • 通過查看官方文檔對 timerWithTimeInterval:target:selector:userInfo:repeats: 方法中對 target 的描述:
The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to this object until it (the timer) is invalidated.timer強(qiáng)引用了target,直接對target所指向的內(nèi)存地址強(qiáng)引用
  • 從文檔中描述可以看出,timer 對傳入的 target 具有強(qiáng)持有,即 timer 持有 self,又由于 timer 是定義在控制器 B 中,所以 self 也持有 timer,因此 self -> timer -> self 構(gòu)成了循環(huán)引用。
  • 我們知道:循環(huán)引用可以通過 __weak 即弱引用來解決,那么我們代碼修改如下:
__weak typeof(self) weakSelf = self;self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(popHome) userInfo:nil repeats:YES];[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
  • 再次運(yùn)行程序,進(jìn)行 push-pop 跳轉(zhuǎn),卻發(fā)現(xiàn)問題還是存在,即定時器方法仍然在執(zhí)行,并沒有執(zhí)行 B 的 dealloc 方法,這是為什么呢?
  • 使用 __weak 雖然打破了 self -> timer -> self 之前的循環(huán)引用,即引用鏈變成了 self -> timer -> weakSelf -> self,但是我們遺漏了一個點(diǎn),Runloop 對 timer 也強(qiáng)持有,因?yàn)?Runloop 的生命周期比控制器 B 更長,所以導(dǎo)致了 timer 無法被釋放,同時也導(dǎo)致了控制器 B 的 self 也無法被釋放。
  • 沒有添加 weakSelf 之前的引用鏈如下:

  • 添加 weakSelf 之后的引用鏈變成了如下所示:

二、weakSelf 與 self

  • 對于 weakSelf 和 self,我們關(guān)心的是:
    • weakSelf 會對引用計(jì)數(shù)進(jìn)行 +1 操作嗎?
    • weakSelf 和 self 的指針地址相同嗎,是指向同一片內(nèi)存嗎?
  • 在添加 weakSelf 前后打印 self 的引用計(jì)數(shù):
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));__weak typeof(self) weakSelf = self;NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
  • 運(yùn)行程序,可以看到前后 self 的引用計(jì)數(shù)都是 8,因此可以判定 weakSelf 沒有對內(nèi)存進(jìn)行 +1 操作。
  • 繼續(xù)打印 weakSelf 和 self 對象,以及指針地址:
po weakSelf<ViewController: 0x7fea4f024200>po self<ViewController: 0x7fea4f024200>p &self(ViewController **) $4 = 0x00000001085a5fc8p &weakSelf(ViewController *const *) $5 = 0x00007ffeeb06b648
  • 可以看出,當(dāng)前 self 取地址和 weakSelf 取地址的值是不一樣的,意味著有兩個指針地址,指向的是同一片內(nèi)存空間,即 weakSelf 和 self 的內(nèi)存地址是不一樣,都指向同一片內(nèi)存空間的。
  • 此時 timer 捕獲的是 <ViewController: 0x7fea4f024200>,是一個對象,所以無法通過 weakSelf 來解決強(qiáng)持有,即引用鏈關(guān)系為:NSRunLoop -> timer -> weakSelf(<ViewController: 0x7fea4f024200>),所以 RunLoop 對整個對象的空間強(qiáng)持有,runloop 沒停,timer 和 weakSelf 就無法被釋放。
  • block 的循環(huán)引用,與 timer 的是有區(qū)別的,通過 block 底層原理的方法 __Block_object_assign 可知,block 捕獲的是對象的指針地址,即 weakself 是臨時變量的指針地址,與 self 無關(guān),因?yàn)?weakSelf 是新的地址空間,所以此時的 weakSelf 相當(dāng)于中間值,其引用關(guān)系鏈為 self -> block -> weakSelf(臨時變量的指針地址),可以通過地址拿到指針。
  • block 和 timer 循環(huán)引用的模型如下:
    • timer 模型:self -> timer -> weakSelf -> self,當(dāng)前的 timer 捕獲的是控制器 B 的內(nèi)存,即 vc 對象的內(nèi)存,即 weakSelf 表示的是 vc 對象;
    • Block 模型:self -> block -> weakSelf -> self,當(dāng)前的 block 捕獲的是指針地址,即 weakSelf 表示的是指向 self 的臨時變量的指針地址。

三、強(qiáng)引用的解決方案

① 當(dāng) controller 界面 pop 到上層界面的消除引用
  • 根據(jù)上文中的分析中,由于 Runloop 對 timer 的強(qiáng)持有,導(dǎo)致 Runloop 間接的強(qiáng)持有了self(因?yàn)?timer 中捕獲的是 vc 對象),所以導(dǎo)致 dealloc 方法無法執(zhí)行,需要查看在 pop 時,是否還有其他方法可以銷毀 timer,這個方法就是 didMoveToParentViewController。
  • didMoveToParentViewController 方法,是用于當(dāng)一個視圖控制器中添加或者移除 viewController 后,必須調(diào)用的方法,目的是為了告訴系統(tǒng),已經(jīng)完成添加/刪除子控制器的操作。
- (void)didMoveToParentViewController:(UIViewController *)parent {if (parent == nil) {[self.timer invalidate];self.timer = nil;NSLog(@"timer 被釋放");}}
② 中介者模式,不直接使用 self
  • 在 timer 模式中,主要是 popHome 能執(zhí)行,并不用管 timer 捕獲的 target 是誰,由于這里不能使用self(因?yàn)闀袕?qiáng)持有問題),所以可以將 target 換成其他對象,例如將 target 換成 NSObject 對象,將 popHome 交給 target 執(zhí)行:
// 定義其他對象@property (nonatomic, strong) id target;// 修改targetself.target = [[NSObject alloc] init];class_addMethod([NSObject class], @selector(popHome), (IMP)popHomeObjc, "v@:");self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(popHome) userInfo:nil repeats:YES];// impvoid popHomeObjc(id obj){NSLog(@"%s -- %@", __func__, obj);}
  • 運(yùn)行程序,發(fā)現(xiàn)程序執(zhí)行 dealloc 之后,timer 還是會繼續(xù)執(zhí)行,這是因?yàn)殡m然解決了中介者的釋放,但是沒有解決中介者的回收,即 self.target 的回收。
  • 繼續(xù)通過在 dealloc 方法中,取消定時器來解決,代碼如下:
- (void)dealloc{[self.timer invalidate];self.timer = nil;NSLog(@"%s", __func__);}
  • 再次運(yùn)行程序如下,發(fā)現(xiàn) pop 之后,timer 被釋放,從而中介者也會進(jìn)行回收釋放。
③ 自定義封裝 timer
  • 自定義 timerWapper:
    • 在初始化方法中,定義一個 timer,其 target 是自己,即 timerWapper 中的 timer,一直監(jiān)聽自己,判斷 selector,此時的 selector 已交給了傳入的 target(即 vc 對象),此時有一個方法 popHomeWapper,在方法中,判斷 target 是否存在;
      • 如果 target 存在,則需要讓 vc 知道,即向傳入的 target 發(fā)送 selector 消息,并將此時的 timer 參數(shù)也一并傳入,所以 vc 就可以得知 popHome 方法,就這事這種方式定時器方法能夠執(zhí)行的原因 ;
      • 如果 target 不存在,已經(jīng)釋放了,則釋放當(dāng)前的 timerWrapper,即打破了 RunLoop 對 timeWrapper 的強(qiáng)持有 (timeWrapper <-×- RunLoop);
    • 自定義 ydw_invalidate 方法中釋放 timer,這個方法在 vc 的 dealloc 方法中調(diào)用,即 vc 釋放,從而導(dǎo)致 timerWapper 釋放,打破了 vc 對 timeWrapper 的強(qiáng)持有( vc -×-> timeWrapper);
// .h文件@interface YDWTimerWapper : NSObject- (instancetype)ydw_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;- (void)ydw_invalidate;@end// .m文件#import "YDWTimerWapper.h"#import <objc/message.h>@interface YDWTimerWapper ()@property(nonatomic, weak) id target;@property(nonatomic, assign) SEL aSelector;@property(nonatomic, strong) NSTimer *timer;@end@implementation YDWTimerWapper- (instancetype)ydw_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo {if (self == [super init]) {// 傳入vcself.target = aTarget;// 傳入的定時器方法self.aSelector = aSelector;if ([self.target respondsToSelector:self.aSelector]) {Method method = class_getInstanceMethod([self.target class], aSelector);const char *type = method_getTypeEncoding(method);// 給timerWapper添加方法class_addMethod([self class], aSelector, (IMP)popHomeWapper, type);// 啟動一個timer,target是self,即監(jiān)聽自己self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];}}return self;}// 一直執(zhí)行 runloopvoid popHomeWapper(YDWTimerWapper *wapper){// 判斷target是否存在if (wapper.target) {// 如果存在則需要讓vc知道,即向傳入的target發(fā)送selector消息,并將此時的timer參數(shù)也一并傳入,所以vc就可以得知`popHome`方法,就這事這種方式定時器方法能夠執(zhí)行的原因// objc_msgSend發(fā)送消息,執(zhí)行定時器方法void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend;lg_msgSend((__bridge void *)(wapper.target), wapper.aSelector,wapper.timer);} else {// 如果target不存在,已經(jīng)釋放了,則釋放當(dāng)前的timerWrapper[wapper.timer invalidate];wapper.timer = nil;}}// 在vc的dealloc方法中調(diào)用,通過vc釋放,從而讓timer釋放- (void)ydw_invalidate {[self.timer invalidate];self.timer = nil;}- (void)dealloc {NSLog(@"%s",__func__);}@end
  • timerWapper 的使用:
// 定義self.timerWapper = [[YDWTimerWapper alloc] ydw_initWithTimeInterval:1 target:self selector:@selector(popHome) userInfo:nil repeats:YES];// 釋放- (void)dealloc {[self.timerWapper ydw_invalidate];}
④ 利用 NSProxy 虛基類的子類
  • 定義一個繼承自 NSProxy 的子類:
// NSProxy子類@interface YDWProxy : NSProxy+ (instancetype)proxyWithTransformObject:(id)object;@end@interface YDWProxy()@property (nonatomic, weak) id object;@end@implementation YDWProxy+ (instancetype)proxyWithTransformObject:(id)object{YDWProxy *proxy = [YDWProxy alloc];proxy.object = object;return proxy;}- (id)forwardingTargetForSelector:(SEL)aSelector {return self.object;}
  • 將 timer 中的 target 傳入 NSProxy 子類對象,即 timer 持有 NSProxy 子類對象:
// 解決timer強(qiáng)持有問題self.proxy = [YDWProxy proxyWithTransformObject:self];self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(popHome) userInfo:nil repeats:YES];// 在dealloc中將timer正常釋放- (void)dealloc {[self.timer invalidate];self.timer = nil;}
  • 這樣將強(qiáng)引用的注意力轉(zhuǎn)移成了消息轉(zhuǎn)發(fā),虛基類只負(fù)責(zé)消息轉(zhuǎn)發(fā),即使用 NSProxy 作為中間代理和中間者。
  • 那么定義的 proxy 對象,在 dealloc 釋放時,還存在嗎?其實(shí),proxy 對象會正常被釋放,因?yàn)?vc 被釋放,所以可以釋放其持有者,即 timer 和 proxy,timer 的釋放也打破了 runLoop 對 proxy 的強(qiáng)持有,完美的達(dá)到了兩層釋放,即 vc -×-> proxy <-×- runloop。

總結(jié)

以上是生活随笔為你收集整理的iOS之深入解析内存管理NSTimer的强引用问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。