Block迴圈引用的三種解決方式

穿山甲到底說了什麼發表於2018-12-19

今天 抽空看了下 *Objective-C高階程式設計iOS與OSX多執行緒和記憶體管理*,發現自己之前所理解的為什麼block會發生迴圈引用?`有些理解是錯誤的,還好看了這個書,最後弄清楚了,希望寫出來,既能算是一種總結,又能讓其他小夥伴避免再遇到這個坑!下面 讓我們一起來看幾個場景!

專案簡單的類結構

import "ViewController.h"
#import "DetailViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    DetailViewController *detailVC = [DetailViewController new];
    detailVC.testMemoryLeaksBLock = ^(DetailViewController *d) {
        
        NSLog(@"d: %@--", d);
    };
    [self presentViewController:d animated:YES completion:nil];
}

//---------------------DetailViewController-----------------------------
@interface DetailViewController : UIViewController

@property (nonatomic, copy) void(^testMemoryLeaksBLock)(DetailViewController *detailVC);

@end

@implementation DetailViewController

-(void)dealloc {
    
    NSLog(@"dealloc");
}

- (void)viewDidDisappear:(BOOL)animated {
    
    [super viewDidDisappear:animated];
    !_testMemoryLeaksBLock ?: _testMemoryLeaksBLock(self);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self dismissViewControllerAnimated:YES completion:nil];
}

複製程式碼

場景一

正如專案結構那樣,當detailVC disappear時候,回撥block是否會有記憶體洩漏呢?

 detailVC.testMemoryLeaksBLock = ^(DetailViewController *d) {
        
        NSLog(@"d: %@--", d);
    };
複製程式碼

區域性變數不會造成,執行完自動釋放,不會造成記憶體洩漏 - 1

sow 正如上面分析圖,一般情況下,是有記憶體洩漏的,但就是由於detailVC.testMemoryLeaksBLock所持有的d指標是一個區域性變數,當block執行完之後,這個區域性變數引用計數就為0,就被釋放了,因此d 就不再持有detailVCdetailVC.testMemoryLeaksBLockdetailVC就不再持有, 迴圈引用被打破,還是會走 -[DetailViewController dealloc] 的.

區域性變數不會造成,執行完自動釋放,不會造成記憶體洩漏 - 2
####更正:block不會強引用 block內部的區域性變數weak弱指標,只會強引用 block 外部strong指標,並不是 block結束之後就會釋放掉區域性變數,所以不會引起迴圈,因為如果像那樣說的話,假如block不執行,那區域性變數豈不是就釋放不掉了。 具體看一下例1:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Student *student = [[Student alloc]init];
    student.name = @"Hello World";
    
    __weak typeof(student) weakStu = student;
    Student *strongStu = weakStu;
    student.study = ^{
        
        //第1種寫法
        //Student *strongStu = weakStu;
        //第2種寫法
        //__strong typeof(weakStu) strongStu = weakStu;
        //第3種寫法
        //typeof(student) strongStu = weakStu;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
            NSLog(@"my name is = %@", strongStu.name);
        });
    };
}
複製程式碼

例1是有記憶體洩漏的沒有走-[Student dealloc],因為未執行student.study, 所以dispatch_after block也不會走,但是dispatch_after bLock卻強引用了strongStu還是發生了迴圈引用。這個比較好理解。但是下面例2,就改了一行程式碼 我怎麼也想不通為什麼 沒有發生迴圈引用,走了-[Student dealloc] .

例2:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Student *student = [[Student alloc]init];
    student.name = @"Hello World";
    
    __weak typeof(student) weakStu = student;
    student.study = ^{

        /**
         * 三種寫法是一樣的效果,都是為了防止區域性變數`student`在`viewDidLoad `之後銷燬。如果不這樣寫的話,
         * 由於`student`區域性變數被銷燬,所以為nil,再走到`dispatch_after block`時候,由於weakStu是弱指標,
         * 所以不會強引用,最後列印為null,這不是我們想要的效果,`AFNetWorking`好多第三方庫也是這麼寫的
         * 第1種寫法
         * Student *strongStu = weakStu;
         * 第2種寫法
         * __strong typeof(weakStu) strongStu = weakStu;
         * 第3種寫法
         * typeof(student) strongStu = weakStu;
         */
        //隨便取哪一種寫法,這裡取第一種寫法
         Student *strongStu = weakStu;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
            NSLog(@"my name is = %@", strongStu.name);
        });
    };
}
複製程式碼

dispatch_after block 強引用了外部變數strongStu ,這種不呼叫student.study()的寫法為什麼沒有迴圈引用呢,但是如果在ViwDidLoad結尾呼叫student.study(),那麼會在2秒後執行完dispatch_after block才會走-[Student dealloc],不就說明dispatch_after block持有這個student,走完才回銷燬,那如果不執行student.study()的話,按道理講,應該也會被dispatch_after block持有這個student,為什麼 不會產生迴圈引用呢。

匪夷所思如果有哪個大神路過,麻煩給個思路,我真想不明白。

場景二

block程式碼改成下面這樣,當detailVC disappear時候,回撥block是否又會有記憶體洩漏呢?

DetailViewController *detailVC = [DetailViewController new]; 
id  tmp = detailVC;
 detailVC.testMemoryLeaksBLock = ^(DetailViewController *d) {
        
     NSLog(@"d: %@--\nd: %@",  d,  tmp); 
   };
複製程式碼

答案:的確有記憶體洩漏 ,因為迴圈引用的問題,具體看下圖:

迴圈引用造成記憶體洩漏

上面情況如果懂的話,下面這種寫法是一樣的,經典的迴圈引用 detailVC持有testMemoryLeaksBLocktestMemoryLeaksBLock 持有 detailVC,導致兩個引用計數都無法減一,最後誰也釋放不了誰!!

DetailViewController * detailVC = [DetailViewController new]; 
detailVC.testMemoryLeaksBLock = ^(DetailViewController *d) {
        
     NSLog(@"detailVC: %@",  detailVC); 
   };
複製程式碼

##如何解決記憶體洩漏 原因我們瞭解了,現在是該如何解決了,下面根據之前場景逐一給出不同的思路,注意思路很重要,因為有很多種解決思路,都要搞清楚,舉一反三,因為場景一沒有記憶體洩漏,因此主要針對場景二 ###針對場景二的解決方案1: 有問題的程式碼:

DetailViewController *detailVC = [DetailViewController new]; 
id  tmp = detailVC;
detailVC.testMemoryLeaksBLock = ^(DetailViewController *d) {

    NSLog(@"tmp: %@",  tmp); 
  };
複製程式碼

方案一的程式碼:

DetailViewController * detailVC = [DetailViewController new];
 __block id tmp = detailVC;
 detailVC.testMemoryLeaksBLock = ^(DetailViewController *d) {

     NSLog(@"tmp: %@",  tmp); 
     tmp = nil;
 };
複製程式碼

雖然解決了記憶體洩漏,但是細心的看客姥爺肯定發現了這樣寫的一個弊端,沒錯,那就是 如果 detailVC.testMemoryLeaksBLock ()沒有呼叫的話,還是會造成記憶體洩漏的,因為testMemoryLeaksBLock還是間接地強引用了detailVC, 算是一個思路吧,畢竟思路要廣才能 想的更多,學的更多,不是嗎!

###針對場景二的解決方案2: 有問題的程式碼:

DetailViewController *detailVC = [DetailViewController new]; 
id  tmp = detailVC;
detailVC.testMemoryLeaksBLock = ^(DetailViewController *d) {

    NSLog(@"tmp: %@",  tmp); 
  };
複製程式碼

方案二的程式碼:

DetailViewController * detailVC = [DetailViewController new];
__weak id weakTmp = detailVC;
 detailVC.testMemoryLeaksBLock = ^(DetailViewController *d) {

     NSLog(@"weakTmp: %@",  weakTmp); 
 };
複製程式碼

這也是平常開發中用得最多的一種解決迴圈引用的方法。來給小總結吧:方案一和方案二都是斷掉testMemoryLeaksBLockdetailVC的強引用,自然可以;其實開發中還有一種方案也是可以的,那就是 斷掉detailVCtestMemoryLeaksBLock 的強引用。

###針對場景二的解決方案3: 有問題的程式碼:

DetailViewController *detailVC = [DetailViewController new]; 
id  tmp = detailVC;
detailVC.testMemoryLeaksBLock = ^(DetailViewController *d) {

    NSLog(@"tmp: %@",  tmp); 
  };
複製程式碼

方案三的程式碼:

@implementation DetailViewController

-(void)dealloc {
    
    NSLog(@"dealloc");
}

- (void)viewDidDisappear:(BOOL)animated {
    
    [super viewDidDisappear:animated];
    !_ testMemoryLeaksBLock ?: _testMemoryLeaksBLock(self);
    //就加了下面一行程式碼也是可以的,因為一旦手動把 _testMemoryLeaksBLock置為空,  那麼這個block就沒有任何物件持有它,
    //換一句話說就是沒有物件強引用這個block, 那麼如果這個block之前在堆裡,它就會被廢棄掉,
    _testMemoryLeaksBLock= nil;
}
@end
複製程式碼

每一次執行完block之後都手動置nil,斷掉detailVCtestMemoryLeaksBLock 的強引用也不失為一種方法。

####更正:block不會強引用 block內部的區域性變數和 弱指標,只會強引用 block 外部strong指標,並不是 block結束之後就會釋放掉區域性變數,所以不會引起迴圈,因為如果像那樣說的話,假如block不執行,那區域性變數豈不是就釋放不掉了。 ####總結:這也是平常開發中三種解決迴圈引用的方法。希望大家週末都玩得開心,麼麼噠,下週準備寫 GCD,有好多 執行緒同步的 知識,望 共同努力,共勉,手動!

相關文章