本文主要是通過定時器
來梳理強引用
的幾種解決方案
強應用(強持有)
假設此時有兩個界面A、B,從A push
到B界面,在B界面中有如下定時器代碼。當從B pop
回到A界面[圖片上傳中...(E70D3F5D-8815-4138-BFDD-017B1BFCE0E7.png-6861f8-1609331145410-0)]
時,發現定時器沒有停止,其方法仍然在執行,為什么?
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
其主要原因是B界面沒有釋放
,即沒有執行dealloc
方法,導致timer也無法停止和釋放
解決方式一
- 重寫
didMoveToParentViewController
方法
- (void)didMoveToParentViewController:(UIViewController *)parent{
// 無論push 進來 還是 pop 出去 正常跑
// 就算繼續push 到下一層 pop 回去還是繼續
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);
}];
}
現在,我們從底層來深入研究,為什么B
界面有了timer
之后,導致B界面釋放不掉,即不會走到dealloc
方法。我們可以通過官方文檔查看timerWithTimeInterval:target:selector:userInfo:repeats:
方法中對target的描述
從文檔中可以看出,timer對傳入的target具有強持有,即timer
持有self
。由于timer是定義在B界面中,所以self也持有timer
,因此 self -> timer -> self
構成了循環引用
在iOS-底層原理 30:Block底層原理文章中,針對循環應用提供了幾種解決方式。我們我們嘗試通過__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跳轉。發現問題還是存在,即定時器方法仍然在執行,并沒有執行B的dealloc方法,為什么呢?
- 我們使用
__weak
雖然打破了self -> timer -> self
之前的循環引用,即引用鏈變成了self -> timer -> weakSelf -> self
。但是在這里我們的分析并不全面,此時還有一個Runloop對timer的強持有
,因為Runloop
的生命周期
比B
界面更長
,所以導致了timer無法釋放
,同時也導致了B界面的self也無法釋放
。所以,最初引用鏈
應該是這樣的
引用鏈-1
加上weakSelf
之后,變成了這樣
引用鏈-2
weakSelf 與 self
對于weakSelf
和 self
,主要有以下兩個疑問
1、
weakSelf
會對引用計數進行+1
操作嗎?2、
weakSelf
和self
的指針地址相同嗎,是指向同一片內存嗎?帶著疑問,我們在
weakSelf
前后打印self
的引用計數
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
__weak typeof(self) weakSelf = self;
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
運行結果如下,發現前后self
的引用計數都是8
因此可以得出一個結論:weakSelf沒有對內存進行+1操作
- 繼續打印
weakSelf
和self
對象,以及指針地址
po weakSelf
po self
po &weakSelf
po &self
結果如下
從打印結果可以看出,當前
self
取地址 和 weakSelf
取地址的值是不一樣的。意味著有兩個指針地址,指向的是同一片內存空間
,即weakSelf 和 self 的內存地址是不一樣,都指向同一片內存空間
的從上面打印可以看出,此時
timer
捕獲的是<LGTimerViewController: 0x7f890741f5b0>
,是一個對象
,所以無法通過weakSelf來解決強持有
。即引用鏈關系為:NSRunLoop -> timer -> weakSelf(<LGTimerViewController: 0x7f890741f5b0>)
。所以RunLoop對整個 對象的空間有強持有
,runloop沒停,timer 和 weakSelf是無法釋放的而我們在
Block
原理中提及的block的循環引用
,與timer
的是有區別的。通過block底層原理的方法__Block_object_assign
可知,block
捕獲的是對象的指針地址
,即weakself 是 臨時變量的指針地址
,跟self
沒有關系,因為weakSelf是新的地址空間
。所以此時的weakSelf相當于中間值
。其引用關系鏈為self -> block -> weakSelf(臨時變量的指針地址)
,可以通過地址
拿到指針
所以在這里,我們需要區別下block
和timer
循環引用的模型
timer模型:
self -> timer -> weakSelf -> self
,當前的timer
捕獲的是B界面的內存,即vc對象的內存
,即weakSelf
表示的是vc對象
Block模型:
self -> block -> weakSelf -> self
,當前的block捕獲的是指針地址
,即weakSelf
表示的是指向self的臨時變量的指針地址
解決 強引用(強持有)
以下幾種方法的思路均是:依賴中介者模式
,打破強持有
,其中推薦思路四
思路一:pop時在其他方法中銷毀timer
根據前面的解釋,我們知道由于Runloop對timer的強持有
,導致了Runloop間接的強持有了self
(因為timer中捕獲的是vc對象
)。所以導致dealloc
方法無法執行。需要查看在pop
時,是否還有其他方法可以銷毀timer
。這個方法就是didMoveToParentViewController
didMoveToParentViewController
方法,是用于當一個視圖控制器中添加或者移除viewController后,必須調用的方法。目的是為了告訴iOS,已經完成添加/刪除子控制器的操作。在B界面中重寫
didMoveToParentViewController
方法
- (void)didMoveToParentViewController:(UIViewController *)parent{
// 無論push 進來 還是 pop 出去 正常跑
// 就算繼續push 到下一層 pop 回去還是繼續
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 走了");
}
}
思路二:中介者模式,即不使用self,依賴于其他對象
在timer模式中,我們重點關注的是fireHome
能執行,并不關心timer捕獲的target
是誰,由于這里不方便使用self
(因為會有強持有問題),所以可以將target換成其他對象
,例如將target換成NSObject對象
,將fireHome
交給target
執行
- 將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);
}
運行結果如下
運行發現執行
dealloc
之后,timer還是會繼續執行
。原因是解決了中介者的釋放
,但是沒有解決中介者的回收
,即self.target
的回收。所以這種方式有缺陷
可以通過在dealloc
方法中,取消定時器來解決,代碼如下
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}
運行結果如下,發現pop之后,timer釋放,從而中介者也會進行回收釋放
思路三:自定義封裝timer
這種方式是根據思路二的原理,自定義封裝timer,其步驟如下
- 自定義timerWapper
- 在初始化方法中,定義一個timer,其target是自己。即
timerWapper
中的timer
,一直監聽自己,判斷selector
,此時的selector已交給了傳入的target(即vc對象),此時有一個方法fireHomeWapper
,在方法中,判斷target是否存在如果
target存在
,則需要讓vc知道,即向傳入的target發送selector消息,并將此時的timer參數也一并傳入,所以vc就可以得知fireHome
方法,就這事這種方式定時器方法能夠執行的原因如果
target不存在
,已經釋放了,則釋放當前的timerWrapper,即打破了RunLoop對timeWrapper的強持有 (timeWrapper <-×- RunLoop
)
- 自定義
cjl_invalidate
方法中釋放timer。這個方法在vc的dealloc方法中調用,即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,即監聽自己
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發送selector消息,并將此時的timer參數也一并傳入,所以vc就可以得知`fireHome`方法,就這事這種方式定時器方法能夠執行的原因
//objc_msgSend發送消息,執行定時器方法
void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
lg_msgSend((__bridge void *)(wapper.target), wapper.aSelector,wapper.timer);
}else{
//如果target不存在,已經釋放了,則釋放當前的timerWrapper
[wapper.timer invalidate];
wapper.timer = nil;
}
}
//在vc的dealloc方法中調用,通過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
虛基類,可以交給其子類實現,NSProxy的介紹在iOS-底層原理 30:Block底層原理已經介紹過了,這里不再重復
- 首先定義一個繼承自
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;
}
這樣做的主要目的是將強引用的注意力轉移成了消息轉發
。虛基類只負責消息轉發,即使用NSProxy
作為中間代理、中間者
這里有個疑問,定義的proxy
對象,在dealloc釋放時,還存在嗎?
-
proxy
對象會正常釋放,因為vc
正常釋放了,所以可以釋放其持有者,即timer和proxy
,timer
的釋放也打破了runLoop對proxy的強持有
。完美的達到了兩層釋放
,即vc -×-> proxy <-×- runloop
,解釋如下:vc釋放,導致了
proxy
的釋放dealloc方法中,timer進行了釋放,所以runloop強引用也釋放了