1. 程式人生 > 實用技巧 >033:strong和weak,(strong=retain+release)weak(self指標地址和weakSelf地址不一樣、 weakSelf沒有對引用計數+1)

033:strong和weak,(strong=retain+release)weak(self指標地址和weakSelf地址不一樣、 weakSelf沒有對引用計數+1)

問題

當前self取地址 和weakSelf取地址的值是不一樣的

weakSelf沒有對記憶體進行+1操作

目錄

預備

正文

1. ARC & MRC

Objective-C提供了兩種記憶體管理機制

  • MRCMannul Reference Counting手動管理引用計數)
  • ARCAutomatic Reference Counting自動管理引用計數)

MRC(手動管理引用計數)

  1. 通過allocnewcopymutableCopy生成的物件,持有時,需要使用retainreleaseautoRelease管理引用計數
  • retain物件引用計數+1
  • release物件引用計數-1
  • autoRelease:自動對作用域內物件進行一次retainrelease操作。
  1. MRC模式下,必須遵守:誰建立誰釋放誰引用誰管理

ARC(自動管理引用計數)

  1. ARCWWDC2011上公佈,iOS5系統引入的自動管理機制,是LLVMRuntime配合的結果,在編譯期執行時都會進行記憶體管理
  2. ARC禁止手動呼叫retainreleaseretainCountdealloc,轉而使用weakstrong屬性關鍵字。
  • 現在都是直接使用ARC,由系統自動管理引用計數了。

2. strong & weak

  • 關於strong
    weak,可以在objc4原始碼中進行探索。 現在將流程圖總結記錄一下:

2.1 weak

  • weak不處理(物件)的引用計數,而是使用一個雜湊結構弱引用表進行資訊儲存
  • 物件本身的引用計數0時,呼叫dealloc函式,觸發weak表的釋放

  • 弱引用表儲存細節
  1. weak使用weakTable弱引用表進行儲存資訊,是sideTable散列表(雜湊表)結構。
  2. 建立weak_entry_t,將referent引用計數加入到weak_entry_t的陣列inline_referrers中。
  3. 支援weak_table擴容,把new_entry加入到weak_table

2.2 strong

strong修飾,實際是新值retain舊值release:

  1. weak處理引用計數,使用弱引用表進行資訊儲存dealloc移除記錄
  2. strong:內部使用retainrelease進行引用計數管理

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個問題:

  1. 為什麼?(timer加入後,控制器無法釋放)
  2. 如何解決?

3.1 強持有

拓展:

  無法釋放,一般是迴圈引用導致

  (注意:self作為引數傳入,不會被【自動持有】,除非內部手動強引用self。)

  • 一般來說,迴圈引用可以通過加入弱引用物件,打斷迴圈:self -> timer ->加入weakself-> self

  • 對,原理沒錯。但前提是:timer�僅被self持有,且timer僅拷貝weakself指標!

  • 很不巧:

  1. 當前timer除了被self持有,還被加入了[NSRunLoop currentRunLoop]
  2. 當前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和proxytimer的釋放也打破了runLoop對proxy的強持有。完美的達到了兩層釋放,即vc -×-> proxy <-×- runloop,解釋如下:
    • vc釋放,導致了proxy的釋放

    • dealloc方法中,timer進行了釋放,所以runloop強引用也釋放了

這樣做的主要目的是將強引用的注意力轉移成了訊息轉發。虛基類只負責訊息轉發,即使用NSProxy作為中間代理、中間者

這裡有個疑問,定義的proxy物件,在dealloc釋放時,還存在嗎?

  • proxy物件會正常釋放,因為vc正常釋放了,所以可以釋放其持有者,即timer和proxytimer的釋放也打破了runLoop對proxy的強持有。完美的達到了兩層釋放,即vc -×-> proxy <-×- runloop,解釋如下:
    • vc釋放,導致了proxy的釋放

    • dealloc方法中,timer進行了釋放,所以runloop強引用也釋放了

5:weakSelf 與 self

對於weakSelfself,主要有以下兩個疑問

  • 1、weakSelf會對引用計數進行+1操作嗎?

  • 2、weakSelfself的指標地址相同嗎,是指向同一片記憶體嗎?

  • 帶著疑問,我們在weakSelf前後列印self的引用計數

NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
__weak typeof(self) weakSelf = self;
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));

因此可以得出一個結論:weakSelf沒有對記憶體進行+1操作
  • 繼續列印weakSelfself物件,以及指標地址
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(臨時變數的指標地址),可以通過地址拿到指標

所以在這裡,我們需要區別下blocktimer迴圈引用的模型

  • timer模型self -> timer -> weakSelf -> self,當前的timer捕獲的是B介面的記憶體,即vc物件的記憶體,即weakSelf表示的是vc物件

  • Block模型self -> block -> weakSelf -> self,當前的block捕獲的是指標地址,即weakSelf表示的是指向self的臨時變數的指標地址

注意

引用