1. 程式人生 > >capturing self strongly in this block is likely to lead to a retain cycle 警告解決

capturing self strongly in this block is likely to lead to a retain cycle 警告解決

警告:

capturing self strongly in this block is likely to lead to a retain cycle

意思是block會retain一次,所以使用前最好

    __block ViewController *strongBlock = self;

    __block ViewController *strongBlock = self;

簡單程式碼如下:

    self.tableViewDelegate.didSelectedBlock = ^(UITableView *tableview, NSIndexPath *indexPath) {
        MessageViewController *messageVC = [[MessageViewController alloc]init];
        [self presentViewController:messageVC animated:YES completion:nil];
    };

修改後的程式碼:

    __block ViewController *strongBlock = self;
    self.tableViewDelegate.didSelectedBlock = ^(UITableView *tableview, NSIndexPath *indexPath) {
        MessageViewController *messageVC = [[MessageViewController alloc]init];
        [self presentViewController:messageVC animated:YES completion:nil];
    };

這樣就沒有警告了!!!

__block關鍵字:

根據記憶體地址變化可見,__block所起到的作用就是隻要觀察到該變數被 block 所持有,就將“外部變數”在棧中的記憶體地址放到了堆中。進而在block內部也可以修改外部變數的值。原先地址是否直接拋棄不用再繼續研究.

Block不允許修改外部變數的值,Apple這樣設計,應該是考慮到了block的特殊性,block也屬於“函式”的範疇,變數進入block,實際就是已經改變了作用域。在幾個作用域之間進行切換時,如果不加上這樣的限制,變數的可維護性將大大降低。又比如我想在block內聲明瞭一個與外部同名的變數,此時是允許呢還是不允許呢?只有加上了這樣的限制,這樣的情景才能實現。

編譯器做了什麼?

一般使用的話,到這個程度已經足夠了。我們已經知道了加上__block關鍵字之後,編譯器通過將外部變數同block一起copy到了堆區,並且將“外部變數”在棧中的記憶體地址改為了堆中的新地址。
如果多問一個為什麼?編譯器是怎麼做到這樣的呢?我們通過clang將 OC 程式碼轉換為 C++ 檔案:

clang -rewrite-objc 原始碼檔名

轉譯的時候遇到了幾個問題:

  1.  
#import <UIKit/UIKit.h>
**        ^**
1 error generated.

通過Objective-C編譯成C++程式碼報錯文中的方式可以轉譯,但是又出現了新的問題;
2.clang: warning: using sysroot for 'iPhoneSimulator' but targeting 'MacOSX'
這個問題沒能解決,然後換了個思路轉譯,
新程式碼如下

//坑爹的是NSLog都不能使用,不然會報NSLog錯誤。說白了還是工具不熟悉,為什麼會出現這個情況都不清楚。有機會再看吧
int main() {
        int  a = 1;
        void(^testBlock)(void) = ^(void){
            
        };
        testBlock();
    
        __block int b = 2;
        void(^testBlockb)(void) = ^(void){
            b = 3;
        };
        testBlockb();
        return 0;
}

轉譯後代碼如下

int main() {
        int a = 1;

        void(*testBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);





    __attribute__((__blocks__(byref))) __Block_byref_b_0 b = {(void*)0,(__Block_byref_b_0 *)&b, 0, sizeof(__Block_byref_b_0), 2};

        void(*testBlockb)(void) = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA, (__Block_byref_b_0 *)&b, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)testBlockb)->FuncPtr)((__block_impl *)testBlockb);

        return 0;
}

程式碼變的很長很長,我們的目的是研究__block加上去之後編譯器的操作,精簡下就是

//加__block前的宣告變數是這樣的
int a = 1;
//加__block後的宣告變數是這樣的
__attribute__((__blocks__(byref))) __Block_byref_b_0 b = {
  (void*)0,
  (__Block_byref_b_0 *)&b, 
  0,
   sizeof(__Block_byref_b_0), 
  2};

可以看到增加了__block修飾之後,編譯器做了不少工作,修飾詞中有__Block_byref_b_0重複出現,這是一個與block一樣的結構體型別的自動變數例項!!!!
此時我們在block內部訪問val變數則需要通過一個叫__forwarding的成員變數來間接訪問val變數。
講__forwarding之前,需要先討論一下block的儲存域及copy操作。

1.Block的儲存域及copy操作

前面提到,block內部的作用域是在堆上的,並且呼叫變數時會將變數copy到堆上,那麼block本身是儲存在堆上還是棧上呢?
我們先來看看一個由C/C++/OBJC編譯的程式佔用記憶體分佈的結構:

實際上,block有三種類型,

  • 全域性塊(_NSConcreteGlobalBlock)
  • 棧塊(_NSConcreteStackBlock)
  • 堆塊(_NSConcreteMallocBlock)
    這三種block各自的儲存域如下圖:

    三種block各自的儲存域

     

    簡而言之,儲存在棧中的Block就是棧塊、儲存在堆中的就是堆塊、既不在棧中也不在堆中的塊就是全域性塊。(這聽起來似乎與文章上半部分的說明有衝突呢?其實並不然)

那麼,我們如何判斷這個block的儲存位置呢?
(1)Block不訪問外界變數(包括棧中和堆中的變數)
Block 既不在棧又不在堆中,在程式碼段中,ARC和MRC下都是如此。此時為全域性塊。(_NSConcreteGlobalBlock)
(2)Block訪問外界變數
MRC 環境下:訪問外界變數的 Block 預設儲存棧中。
ARC 環境下:訪問外界變數的 Block 預設儲存在堆中(實際是放在棧區,然後ARC情況下自動又拷貝到堆區),自動釋放。

ARC下,訪問外界變數的 Block為什麼要自動從棧區拷貝到堆區呢?
棧上的Block,如果其所屬的變數作用域結束,該Block就被廢棄,如同一般的自動變數。當然,Block中的__block變數也同時被廢棄。如下圖:

棧上的block的生命週期

為了解決棧塊在其變數作用域結束之後被廢棄(釋放)的問題,我們需要把Block複製到堆中,延長其生命週期。開啟ARC時,大多數情況下編譯器會恰當地進行判斷是否有需要將Block從棧複製到堆,如果有,自動生成將Block從棧上覆制到堆上的程式碼。Block的複製操作執行的是copy例項方法。Block只要呼叫了copy方法,棧塊就會變成堆塊。
如下圖:

block的copy操作原理

在非ARC情況下則需要開發者呼叫copy方法手動複製,由於開發中幾乎都是ARC模式,所以手動複製內容不再過多研究。
將Block從棧上覆制到堆上相當消耗CPU,所以當Block設定在棧上也能夠使用時,就不要複製了,因為此時的複製只是在浪費CPU資源。
Block的複製操作執行的是copy例項方法。不同型別的Block使用copy方法的效果如下表:


根據表得知,Block在堆中copy會造成引用計數增加,這與其他Objective-C物件是一樣的。雖然Block在棧中也是以物件的身份存在,但是棧塊沒有引用計數,因為不需要,我們都知道棧區的記憶體由編譯器自動分配釋放。
不管Block儲存域在何處,用copy方法複製都不會引起任何問題。在不確定時呼叫copy方法即可。

在ARC有效時,多次呼叫copy方法完全沒有問題:

blk = [[[[blk copy] copy] copy] copy];
// 經過多次複製,變數blk仍然持有Block的強引用,該Block不會被廢棄。

2.__block變數與__forwarding

在copy操作之後,既然__block變數也被copy到堆上去了, 那麼訪問該變數是訪問棧上的還是堆上的呢?__forwarding 終於要閃亮登場了,如下圖:

通過__forwarding, 無論是在block中還是 block外訪問__block變數, 也不管該變數在棧上或堆上, 都能順利地訪問同一個__block變數。
值得注意的是,在ARC下,使用 __block 也有可能帶來的迴圈引用,