033:strong和weak,(strong=retain+release)weak(self指標地址和weakSelf地址不一樣、 weakSelf沒有對引用計數+1)
問題
當前self
取地址 和weakSelf
取地址的值是不一樣的
weakSelf沒有對記憶體進行+1操作
目錄
預備
正文
1. ARC & MRC
Objective-C
提供了兩種記憶體管理機制
:
MRC
(Mannul Reference Counting
手動管理引用計數)ARC
(Automatic Reference Counting
自動管理引用計數)
MRC(手動管理引用計數)
- 通過
alloc
、new
、copy
、mutableCopy
生成的物件,持有時
,需要使用retain
、release
、autoRelease
管理引用計數
retain
:物件
的引用計數
+1release
:物件
的引用計數
-1autoRelease
:自動對作用域內
的物件
進行一次retain
和release
操作。
MRC模式
下,必須遵守:誰建立
,誰釋放
,誰引用
,誰管理
ARC(自動管理引用計數)
ARC
在WWDC2011
上公佈,iOS5
系統引入的自動管理機制
,是LLVM
和Runtime
配合的結果,在編譯期
和執行時
都會進行記憶體管理
。ARC
中禁止
手動呼叫retain
、release
、retainCount
、dealloc
,轉而使用weak
、strong
屬性關鍵字。
- 現在都是直接使用
ARC
,由系統
自動管理引用計數了。
2. strong & weak
- 關於
strong
weak
,可以在objc4原始碼中進行探索。 現在將流程圖
和總結
記錄一下:
2.1 weak
weak
是不處理
(物件)的引用計數
,而是使用
一個雜湊結構
的弱引用表
進行資訊
的儲存
。- 當
物件
本身的引用計數
為0
時,呼叫dealloc
函式,觸發weak
表的釋放
弱引用表
的儲存
細節
weak
使用weakTable弱引用表
進行儲存資訊
,是sideTable散列表
(雜湊表)結構。- 建立
weak_entry_t
,將referent
引用計數加入到weak_entry_t
的陣列inline_referrers
中。 - 支援
weak_table
擴容,把new_entry
加入到weak_table
中
2.2 strong
strong
修飾,實際是新值
的retain
和舊值
的release
:
weak
:不
處理引用計數
,使用弱引用表
進行資訊儲存
,dealloc
時移除記錄
。strong
:內部
使用retain
和release
進行引用計數
的管理
。
3. 強弱引用
- 以
NSTimer(計時器)
為切入點
,程式碼案例
:
- (void)createTimer { self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; } - (void)fireHome{ num++; NSLog(@"hello word - %d",num); } - (void)dealloc{ [self.timer invalidate]; self.timer = nil; NSLog(@"%s",__func__); }
NSTimer
建立後,需要手動加入
到Runloop
中才可以執行
,但timer
會使得當前控制器
不走dealloc
方法,導致timer
和控制器
都無法釋放。
下面,我們就來解決2個問題:
- 為什麼?(
timer
加入後,控制器無法釋放
) - 如何解決?
3.1 強持有
拓展:
無法釋放
,一般是迴圈引用
導致
(注意:self
作為引數
傳入,不會被【自動持有】
,除非內部
手動強引用
了self
。)
一般來說,
迴圈引用
可以通過加入弱引用
物件,打斷迴圈
:self -> timer ->加入weakself
-> self對,
原理沒錯
。但前提是:timer
�僅被self
持有,且timer
僅拷貝weakself
指標!很不巧:
- 當前
timer
除了被self持有
,還被加入了[NSRunLoop currentRunLoop]
中- 當前
timer
直接指向self
的記憶體空間
,是對記憶體
進行強持有
,而不是
簡單的指標拷貝
。
所以currentRunLoop
沒結束,timer
就不會釋放
,self
的記憶體空間
也不會釋放
。
block
捕獲外界變數:捕捉的是指標地址
。timer
捕捉的是物件本身(記憶體空間)
方法1:didMoveToParentViewController
手動打斷迴圈
- (void)didMoveToParentViewController:(UIViewController *)parent{ // 無論push 進來 還是 pop 出去 正常跑 // 就算繼續push 到下一層 pop 回去還是繼續 if (parent == nil) { [self.timer invalidate]; self.timer = nil; NSLog(@"timer 走了"); } }
方法2:不加入Runloop
,使用官方閉包API
- (void)createTimer{ self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"timer fire - %@",timer); }]; }
方法3:中介者模式(不使用self)
- 既然
timer
會強持有
物件(記憶體空間
),我們就給他一個中介者
的記憶體空間
,讓他碰不到self
,我們再對中介者
操作和釋放
。 HTTimer.h
檔案:
@interface HTTimer : NSObject + (instancetype)scheduledTimerWithTimeInterval:(NSTimeInterval)interval target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)repeats; - (void)invalidate; - (void)fire; @end
HTTimer.m
檔案:
@interface HTTimer () @property (nonatomic, strong) NSTimer * timer; @property (nonatomic, weak) id aTarget; @property (nonatomic, assign) SEL aSelector; @end @implementation HTTimer + (instancetype)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)repeats { HTTimer * timer = [HTTimer new]; timer.aTarget = aTarget; timer.aSelector = aSelector; timer.timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval target:timer selector:@selector(run) userInfo:userInfo repeats:repeats]; [[NSRunLoop currentRunLoop] addTimer:timer.timer forMode:NSRunLoopCommonModes]; return timer; } - (void)run { //如果崩在這裡,說明你沒有在使用Timer的VC裡面的deinit方法裡呼叫invalidate方法 if(![self.aTarget respondsToSelector:_aSelector]) return; // 消除警告 #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [self.aTarget performSelector:self.aSelector]; #pragma clang diagnostic pop } - (void)fire { [_timer fire]; } - (void)invalidate { [_timer invalidate]; _timer = nil; } - (void)dealloc { // release環境下注釋掉 NSLog(@"計時器已銷燬"); } @end
- 使用方法:
@interface TimerViewController () @property (nonatomic, strong) HTTimer * timer; @end @implementation TimerViewController - (void)viewDidLoad { [super viewDidLoad]; // 建立 self.timer = [HTTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES]; } - (void)fireHome{ NSLog(@"hello word" ); // 呼叫 } - (void)dealloc{ // 釋放 [self.timer invalidate]; NSLog(@"%s",__func__); } @end
方法4: NSProxy虛基類
- 與
NSObject
同級,但內部什麼都沒有
,但是可以持有物件
,並將訊息
全部轉發
給物件
。
(ps: 我啥也沒有
,但我也是物件
,我可以
把你需求
全部傳遞
給能辦事
的物件
)
這就是代理模式
,timer
持有代理
,代理
weak弱引用
持有self
,再把所有訊息轉發
給self
。
HTProxy.h
檔案
@interface HTProxy : NSProxy /// 麻煩把訊息轉發給`object` + (instancetype)proxyWithTransformObject:(id)object; @end
HTProxy.m
檔案
#import "HTProxy.h" @interface HTProxy () @property (nonatomic, weak) id object; // 弱引用object @end @implementation HTProxy /// 麻煩把訊息轉發給`object` + (instancetype)proxyWithTransformObject:(id)object { HTProxy * proxy = [HTProxy alloc]; proxy.object = object; return proxy; } // 訊息轉發。 (所有訊息,都轉發給object去處理) - (id)forwardingTargetForSelector:(SEL)aSelector { return self.object; } // 訊息轉發 self.object(可以利用虛基類,進行資料收集) //- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{ // // if (self.object) { // }else{ // NSLog(@"麻煩收集 stack111"); // } // return [self.object methodSignatureForSelector:sel]; // //} // //- (void)forwardInvocation:(NSInvocation *)invocation{ // // if (self.object) { // [invocation invokeWithTarget:self.object]; // }else{ // NSLog(@"麻煩收集 stack"); // } // //} -(void)dealloc { NSLog(@"%s",__func__); } @end
- 使用方法:
@interface TimerViewController () @property (nonatomic, strong) HTProxy * proxy; @property (nonatomic, strong) NSTimer * timer; @end @implementation TimerViewController - (void)viewDidLoad { [super viewDidLoad]; // 建立虛基類代理 self.proxy = [HTProxy proxyWithTransformObject: self]; self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES]; } - (void)fireHome{ NSLog(@"hello word" ); // 呼叫 } - (void)dealloc{ // 釋放 [self.timer invalidate]; NSLog(@"%s",__func__); } @end
虛基類
的代理模式
使用非常方便
,使用場景
也很多
。(注意proxy
中是weak
弱引用object
)
這樣做的主要目的是將強引用的注意力轉移成了訊息轉發
。虛基類只負責訊息轉發,即使用NSProxy
作為中間代理、中間者
這裡有個疑問,定義的proxy
物件,在dealloc釋放時,還存在嗎?
proxy
物件會正常釋放,因為vc
正常釋放了,所以可以釋放其持有者,即timer和proxy
,timer
的釋放也打破了runLoop對proxy的強持有
。完美的達到了兩層釋放
,即vc -×-> proxy <-×- runloop
,解釋如下:-
vc釋放,導致了
proxy
的釋放 -
dealloc方法中,timer進行了釋放,所以runloop強引用也釋放了
-
這樣做的主要目的是將強引用的注意力轉移成了訊息轉發
。虛基類只負責訊息轉發,即使用NSProxy
作為中間代理、中間者
這裡有個疑問,定義的proxy
物件,在dealloc釋放時,還存在嗎?
proxy
物件會正常釋放,因為vc
正常釋放了,所以可以釋放其持有者,即timer和proxy
,timer
的釋放也打破了runLoop對proxy的強持有
。完美的達到了兩層釋放
,即vc -×-> proxy <-×- runloop
,解釋如下:-
vc釋放,導致了
proxy
的釋放 -
dealloc方法中,timer進行了釋放,所以runloop強引用也釋放了
-
5: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));因此可以得出一個結論:
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的臨時變數的指標地址