本文主要是通過定時器
來梳理強引用
的幾種解決方案
強應用(強持有)
假設此時有兩個界面A灵奖、B美尸,從A push
到B界面尼啡,在B界面中有如下定時器代碼论颅。當從B pop
回到A界面[圖片上傳中...(E70D3F5D-8815-4138-BFDD-017B1BFCE0E7.png-6861f8-1609331145410-0)]
時,發(fā)現(xiàn)定時器沒有停止,其方法仍然在執(zhí)行显蝌,為什么?
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
其主要原因是B界面沒有釋放
订咸,即沒有執(zhí)行dealloc
方法曼尊,導致timer也無法停止和釋放
解決方式一
- 重寫
didMoveToParentViewController
方法
- (void)didMoveToParentViewController:(UIViewController *)parent{
// 無論push 進來 還是 pop 出去 正常跑
// 就算繼續(xù)push 到下一層 pop 回去還是繼續(xù)
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 走了");
}
}
解決方式二
- 定義timer時,采用
閉包
的形式脏嚷,因此不需要指定target
- (void)blockTimer{
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer fire - %@",timer);
}];
}
現(xiàn)在骆撇,我們從底層來深入研究,為什么B
界面有了timer
之后然眼,導致B界面釋放不掉艾船,即不會走到dealloc
方法葵腹。我們可以通過官方文檔查看timerWithTimeInterval:target:selector:userInfo:repeats:
方法中對target的描述
從文檔中可以看出,timer對傳入的target具有強持有屿岂,即timer
持有self
践宴。由于timer是定義在B界面中,所以self也持有timer
爷怀,因此 self -> timer -> self
構成了循環(huán)引用
在iOS-底層原理 30:Block底層原理文章中阻肩,針對循環(huán)應用提供了幾種解決方式。我們我們嘗試通過__weak
即弱引用
來解決运授,代碼修改如下
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
我們再次運行程序烤惊,進行push-pop跳轉(zhuǎn)。發(fā)現(xiàn)問題還是存在吁朦,即定時器方法仍然在執(zhí)行柒室,并沒有執(zhí)行B的dealloc方法,為什么呢逗宜?
- 我們使用
__weak
雖然打破了self -> timer -> self
之前的循環(huán)引用雄右,即引用鏈變成了self -> timer -> weakSelf -> self
。但是在這里我們的分析并不全面纺讲,此時還有一個Runloop對timer的強持有
擂仍,因為Runloop
的生命周期
比B
界面更長
,所以導致了timer無法釋放
熬甚,同時也導致了B界面的self也無法釋放
逢渔。所以,最初引用鏈
應該是這樣的
加上weakSelf
之后乡括,變成了這樣
weakSelf 與 self
對于weakSelf
和 self
肃廓,主要有以下兩個疑問
1、
weakSelf
會對引用計數(shù)進行+1
操作嗎诲泌?2亿昏、
weakSelf
和self
的指針地址相同嗎,是指向同一片內(nèi)存嗎档礁?帶著疑問,我們在
weakSelf
前后打印self
的引用計數(shù)
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
__weak typeof(self) weakSelf = self;
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
運行結果如下吝沫,發(fā)現(xiàn)前后self
的引用計數(shù)都是8
因此可以得出一個結論:weakSelf沒有對內(nèi)存進行+1操作
- 繼續(xù)打印
weakSelf
和self
對象呻澜,以及指針地址
po weakSelf
po self
po &weakSelf
po &self
結果如下
從打印結果可以看出,當前
self
取地址 和 weakSelf
取地址的值是不一樣的惨险。意味著有兩個指針地址羹幸,指向的是同一片內(nèi)存空間
,即weakSelf 和 self 的內(nèi)存地址是不一樣辫愉,都指向同一片內(nèi)存空間
的從上面打印可以看出栅受,此時
timer
捕獲的是<LGTimerViewController: 0x7f890741f5b0>
,是一個對象
,所以無法通過weakSelf來解決強持有
屏镊。即引用鏈關系為:NSRunLoop -> timer -> weakSelf(<LGTimerViewController: 0x7f890741f5b0>)
依疼。所以RunLoop對整個 對象的空間有強持有
,runloop沒停而芥,timer 和 weakSelf是無法釋放的而我們在
Block
原理中提及的block的循環(huán)引用
律罢,與timer
的是有區(qū)別的。通過block底層原理的方法__Block_object_assign
可知棍丐,block
捕獲的是對象的指針地址
误辑,即weakself 是 臨時變量的指針地址
,跟self
沒有關系歌逢,因為weakSelf是新的地址空間
巾钉。所以此時的weakSelf相當于中間值
。其引用關系鏈為self -> block -> weakSelf(臨時變量的指針地址)
秘案,可以通過地址
拿到指針
所以在這里砰苍,我們需要區(qū)別下block
和timer
循環(huán)引用的模型
timer模型:
self -> timer -> weakSelf -> self
,當前的timer
捕獲的是B界面的內(nèi)存,即vc對象的內(nèi)存
踏烙,即weakSelf
表示的是vc對象
Block模型:
self -> block -> weakSelf -> self
师骗,當前的block捕獲的是指針地址
,即weakSelf
表示的是指向self的臨時變量的指針地址
解決 強引用(強持有)
以下幾種方法的思路均是:依賴中介者模式
讨惩,打破強持有
辟癌,其中推薦思路四
思路一:pop時在其他方法中銷毀timer
根據(jù)前面的解釋,我們知道由于Runloop對timer的強持有
荐捻,導致了Runloop間接的強持有了self
(因為timer中捕獲的是vc對象
)黍少。所以導致dealloc
方法無法執(zhí)行。需要查看在pop
時处面,是否還有其他方法可以銷毀timer
厂置。這個方法就是didMoveToParentViewController
didMoveToParentViewController
方法,是用于當一個視圖控制器中添加或者移除viewController后魂角,必須調(diào)用的方法昵济。目的是為了告訴iOS,已經(jīng)完成添加/刪除子控制器的操作野揪。在B界面中重寫
didMoveToParentViewController
方法
- (void)didMoveToParentViewController:(UIViewController *)parent{
// 無論push 進來 還是 pop 出去 正常跑
// 就算繼續(xù)push 到下一層 pop 回去還是繼續(xù)
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 走了");
}
}
思路二:中介者模式访忿,即不使用self,依賴于其他對象
在timer模式中斯稳,我們重點關注的是fireHome
能執(zhí)行海铆,并不關心timer捕獲的target
是誰,由于這里不方便使用self
(因為會有強持有問題)挣惰,所以可以將target換成其他對象
卧斟,例如將target換成NSObject對象
殴边,將fireHome
交給target
執(zhí)行
- 將timer的target 由self改成objc
//**********1、定義其他對象**********
@property (nonatomic, strong) id target;
//**********1珍语、修改target**********
self.target = [[NSObject alloc] init];
class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:");
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(fireHome) userInfo:nil repeats:YES];
//**********3锤岸、imp**********
void fireHomeObjc(id obj){
NSLog(@"%s -- %@",__func__,obj);
}
運行結果如下
運行發(fā)現(xiàn)執(zhí)行
dealloc
之后,timer還是會繼續(xù)執(zhí)行
廊酣。原因是解決了中介者的釋放
能耻,但是沒有解決中介者的回收
,即self.target
的回收亡驰。所以這種方式有缺陷
可以通過在dealloc
方法中晓猛,取消定時器來解決,代碼如下
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}
運行結果如下凡辱,發(fā)現(xiàn)pop之后戒职,timer釋放,從而中介者也會進行回收釋放
思路三:自定義封裝timer
這種方式是根據(jù)思路二的原理透乾,自定義封裝timer洪燥,其步驟如下
- 自定義timerWapper
- 在初始化方法中,定義一個timer乳乌,其target是自己捧韵。即
timerWapper
中的timer
,一直監(jiān)聽自己汉操,判斷selector
再来,此時的selector已交給了傳入的target(即vc對象),此時有一個方法fireHomeWapper
磷瘤,在方法中芒篷,判斷target是否存在如果
target存在
,則需要讓vc知道采缚,即向傳入的target發(fā)送selector消息针炉,并將此時的timer參數(shù)也一并傳入,所以vc就可以得知fireHome
方法扳抽,就這事這種方式定時器方法能夠執(zhí)行的原因如果
target不存在
篡帕,已經(jīng)釋放了,則釋放當前的timerWrapper贸呢,即打破了RunLoop對timeWrapper的強持有 (timeWrapper <-×- RunLoop
)
- 自定義
cjl_invalidate
方法中釋放timer赂苗。這個方法在vc的dealloc方法中調(diào)用,即vc釋放,從而導致timerWapper釋放
贮尉,打破了vc
對timeWrapper
的的強持有(vc -×-> timeWrapper
)
- 在初始化方法中,定義一個timer乳乌,其target是自己捧韵。即
//*********** .h文件 ***********
@interface CJLTimerWapper : NSObject
- (instancetype)cjl_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
- (void)cjl_invalidate;
@end
//*********** .m文件 ***********
#import "CJLTimerWapper.h"
#import <objc/message.h>
@interface CJLTimerWapper ()
@property(nonatomic, weak) id target;
@property(nonatomic, assign) SEL aSelector;
@property(nonatomic, strong) NSTimer *timer;
@end
@implementation CJLTimerWapper
- (instancetype)cjl_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
if (self == [super init]) {
//傳入vc
self.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)fireHomeWapper, type);
//啟動一個timer,target是self朴沿,即監(jiān)聽自己
self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
}
}
return self;
}
//一直跑runloop
void fireHomeWapper(CJLTimerWapper *wapper){
//判斷target是否存在
if (wapper.target) {
//如果存在則需要讓vc知道猜谚,即向傳入的target發(fā)送selector消息败砂,并將此時的timer參數(shù)也一并傳入,所以vc就可以得知`fireHome`方法魏铅,就這事這種方式定時器方法能夠執(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)釋放了览芳,則釋放當前的timerWrapper
[wapper.timer invalidate];
wapper.timer = nil;
}
}
//在vc的dealloc方法中調(diào)用斜姥,通過vc釋放,從而讓timer釋放
- (void)cjl_invalidate{
[self.timer invalidate];
self.timer = nil;
}
- (void)dealloc
{
NSLog(@"%s",__func__);
}
@end
- timerWapper的使用
//定義
self.timerWapper = [[CJLTimerWapper alloc] cjl_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
//釋放
- (void)dealloc{
[self.timerWapper cjl_invalidate];
}
運行結果如下
這種方式看起來比較繁瑣沧竟,步驟很多铸敏,而且針對timerWapper
,需要不斷的添加method悟泵,需要進行一系列的處理杈笔。
思路四:利用NSProxy虛基類的子類
下面來介紹一種timer
強引用最常用
的處理方式:NSProxy子類
可以通過NSProxy
虛基類,可以交給其子類實現(xiàn)糕非,NSProxy的介紹在iOS-底層原理 30:Block底層原理已經(jīng)介紹過了蒙具,這里不再重復
- 首先定義一個繼承自
NSProxy
的子類
//************NSProxy子類************
@interface CJLProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end
@interface CJLProxy()
@property (nonatomic, weak) id object;
@end
@implementation CJLProxy
+ (instancetype)proxyWithTransformObject:(id)object{
CJLProxy *proxy = [CJLProxy alloc];
proxy.object = object;
return proxy;
}
-(id)forwardingTargetForSelector:(SEL)aSelector {
return self.object;
}
- 將
timer
中的target
傳入NSProxy子類對象
,即timer持有NSProxy子類對象
//************解決timer強持有問題************
self.proxy = [CJLProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
//在dealloc中將timer正常釋放
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
}
這樣做的主要目的是將強引用的注意力轉(zhuǎn)移成了消息轉(zhuǎn)發(fā)
朽肥。虛基類只負責消息轉(zhuǎn)發(fā)禁筏,即使用NSProxy
作為中間代理、中間者
這里有個疑問衡招,定義的proxy
對象篱昔,在dealloc釋放時,還存在嗎蚁吝?
-
proxy
對象會正常釋放旱爆,因為vc
正常釋放了,所以可以釋放其持有者窘茁,即timer和proxy
怀伦,timer
的釋放也打破了runLoop對proxy的強持有
。完美的達到了兩層釋放
山林,即vc -×-> proxy <-×- runloop
房待,解釋如下:vc釋放,導致了
proxy
的釋放dealloc方法中驼抹,timer進行了釋放桑孩,所以runloop強引用也釋放了