探究Block原理(下篇)-捕獲變量分析及__block原理

主要內容:
1.分析Block捕獲外部變量的過程
2.理解Block修改外部變量的限制
3.分析__block存儲域類說明符的原理
4.理解__block變量的存儲域
5.探究Block對對象的捕獲過程
6.Block的循環引用問題

一、分析Block捕獲外部變量的過程

爲了保證block內部能夠正常訪問外部的變量,Block有一個變量捕獲機制,即Block語法表達式所使用變量可以被保存到Block的結構體實例(Block自身)中。

關於捕獲,Block對不同的外部變量的處理有所不同,根據OC中使用變量的分類,大概包括以下幾種情況:

  • 函數參數(這裏研究Block捕獲,所以此處不涉及)
  • 自動變量(常簡稱,局部變量)
  • 靜態局部變量(常簡稱,靜態變量)
  • 靜態全局變量
  • 全局變量

那麼,現在對Block捕獲外部變量的四種情況進行測試,相關代碼如下:

#import <Foundation/Foundation.h>

//使用如下的命令,可將OC代碼編譯爲C++代碼
//clang -rewrite-objc main.m

int global_val = 1;                  //全局變量
static int static_global_val = 1;    //靜態全局變量

int main(int argc, char * argv[]) {
    int val = 1;                     //自動變量
    static int static_val = 1;       //局部靜態變量
    
    void (^myBlock)(void) = ^{
        global_val ++;
        static_global_val ++;
        static_val ++;
        //val++//直接修改會報錯(Variable is not assignable (missing __block type specifier)
        
        NSLog(@"\nBlock內:\nglobal_val = %d,\nstatic_global_val = %d,\nval = %d,\nstatic_val= %d",global_val,static_global_val,val,static_val);
    };
    
    global_val ++;
    static_global_val ++;
    val ++;
    static_val ++;
    
    NSLog(@"\nBlock外:\nglobal_val = %d,\nstatic_global_val = %d,\nval = %d,\nstatic_val= %d",global_val,static_global_val,val,static_val);
    myBlock();
    return 0;
}

運行的結果如下:

Block外:
global_val = 2,
static_global_val = 2,
val = 2,
static_val= 2

Block內:
global_val = 3,
static_global_val = 3,
val = 1,
static_val= 3

觀察代碼運行結果,我們會發現四種情況下,只有靜態局部變量、靜態全局變量、全局變量可以在Block裏被修改,而且直接修改自動變量就會報錯;所以此時需要考慮以下兩個問題:
1.爲什麼在Block裏不允許更改自動變量?
2.Block捕獲不同的變量並修改時,有什麼區別嗎?

現在將上述代碼轉化爲C++源碼來具體分析,轉換後的代碼如下:

int global_val = 1;
static int static_global_val = 1;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *static_val;  //對應靜態局部變量
  int val;          //對應自動變量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int _val, int flags=0) : static_val(_static_val), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *static_val = __cself->static_val; // bound by copy
  int val = __cself->val; // bound by copy

        global_val ++;
        static_global_val ++;
        (*static_val) ++;

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_78fd5a_mi_0,global_val,static_global_val,val,(*static_val));
    }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(int argc, char * argv[]) {
    int val = 1;
    static int static_val = 1;

    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val, val));

    global_val ++;
    static_global_val ++;
    val ++;
    static_val ++;

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_78fd5a_mi_1,global_val,static_global_val,val,static_val);

    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

    return 0;
}

在代碼分析之前,我們有必要對程序中的內存區域劃分有所瞭解,其大致的分類如下:

內存區域 具體說明
棧區 存放局部變量的值,系統自動分配和釋放;
特點:容量小,速度快,有序
堆區 存放通過malloc系列函數或new操作符分配的內存,如對象;
一般由程序員分配和釋放,如果不釋放,則出現內存泄露;
特點:容量大,速度慢,無序;
靜態區 存放全局變量和靜態變量(包括靜態局部變量和靜態全局變量);
當程序結束時,系統回收;
常量區 存放常量的內存區域;
程序結束時,系統回收;
代碼區 存放二進制代碼的區域

瞭解了這些之後,我們再來具體分析代碼和執行結果:

1.全局變量和靜態全局變量

這兩種變量都存儲在靜態區,在任何時候都可以訪問,所以Block無所謂捕獲,而是採用了直接訪問的方式成功的修改了它們的值;這一點從Block對應的構造函數中就可以看出來:

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_var, int _var, int flags=0) : static_var(_static_var), var(_var);

我們看到,用於創建Block的構造函數使用到了靜態局部變量和自動變量作爲參數,並沒有涉及到全局變量和靜態全局變量。而且我們也在Block的結構體中只發現了對應的靜態變量和自動變量的屬性,這進一步說明Block是直接使用全局變量和靜態全局變量,而非捕獲;

int *static_val;  //對應靜態局部變量
int val;          //對應自動變量
2.自動變量與靜態局部變量

雖然自動變量與靜態局部變量都被Block捕獲,但是隻有靜態局部變量纔可以被修改成功;通過Block中對應的函數__main_block_func_0,可以觀察到Block對外部變量的修改過程,相關代碼如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *static_var = __cself->static_var; // bound by copy
  int var = __cself->var; // bound by copy
            global_var ++;
            static_global_var ++;
            (*static_var) ++;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_TestBlock_b539f1_mi_0,global_var,static_global_var,var,(*static_var));
}

我們發現,Block爲了訪問到對應的自動變量和靜態局部變量都使用了__cself,這些操作其實都是針對Block自身屬性的,但不同的是:
外部靜態局部變量,由於是指針傳遞,所以修改的是同一個變量,可以修改成功;
外部自動變量,由於是值傳遞,所以即使修改成功,也無法改變外部自動變量的值;

因此,也許是出於安全的目的,在編譯階段我們就會收到錯誤提示:Block不能修改其捕獲的外部自動變量,即

Variable is not assignable(missing __block type specifier)

這裏還有兩個問題值得我們思考:
1.爲什麼靜態局部變量的存儲域也在靜態區,卻不可以像全局變量一樣直接修改呢?
關鍵原因還是"局部"兩個字,我們看到C++代碼中的函數__main_block_func_0被設置在了包含Block語法的函數(main函數,靜態局部變量在此處聲明定義)之外,所以__main_block_func_0和靜態局部變量和作用域不同,自然不能像全局變量一樣隨時訪問它,所以採用捕獲和指針傳遞的方式來修改靜態變量;

2.爲什麼自動變量不能像靜態變量一樣指針傳遞呢?
其實,這主要還是因爲自動變量和靜態變量的存儲域的不同,自動變量存在棧上被銷燬的時間不定,這很有可能導致Block執行的時候自動變量已經被銷燬,那麼此時訪問被銷燬的地址就會產生野指針錯誤。

二、理解Block修改外部變量的限制

通過以上的代碼示例,我們可以將Block修改外部變量成功的情況分爲兩種:
第一種:Block直接訪問全局性的變量,如全局變量、靜態全局變量;
第二種:Block間接訪問靜態局部變量,捕獲外部變量並使用指針傳遞的方式;

Block中不允許修改外部變量的值的問題,變成了不允許修改自動變量的值的問題;但這也並非最終答案,其實最根本的原因還是Block不允許修改棧中指針的內容
下面的一段代碼,可以從側面來驗證我們的想法:

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
  NSMutableString *mStr = @"mStr".mutableCopy;
    void (^myBlock)(void) = ^{
        //mStr = @"newMstr".mutableCopy; //代碼1:直接修改了mStr指針內容;
        [mStr appendString:@"-ExtraStr"]; //代碼2:修改mStr指向的堆中內容;
        NSLog(@"Block內:mStr:%@",mStr);
    };
    NSLog(@"Block外:%@",mStr);
    myBlock();   
    return 0;
}
//打印結果:
//Block外:mStr
//Block內:mStr:mStr-ExtraStr

上述代碼是操作一個自動變量的可變字符串,經過測試mStr不可以直接賦值,卻可以通過appendString修改字符串,這其中的原因是什麼呢?
首先還是將代碼轉化爲C++源碼,具體如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSMutableString *mStr;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableString *_mStr, int flags=0) : mStr(_mStr) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself){
  NSMutableString *mStr = __cself->mStr; // bound by copy

        ((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)mStr, sel_registerName("appendString:"), (NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_fe0cca_mi_1);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_fe0cca_mi_2,mStr);
    }
    
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->mStr, (void*)src->mStr, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->mStr, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

int main(int argc, const char * argv[]) {
    NSMutableString *mStr = ((id (*)(id, SEL))(void *)objc_msgSend)((id)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_fe0cca_mi_0, sel_registerName("mutableCopy"));
    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, mStr, 570425344));

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_fe0cca_mi_3,mStr);
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    return 0;
}

作爲對象的字符串會涉及到釋放的問題,所以此處轉換後的源碼與基本類型有所區別(但不影響此處分析,後續會講到),我們發現Block捕獲了mStr,而且採用了指針傳遞的方式,這與上面的靜態局部變量被捕獲的方式很相似,但是mStr依然不可以直接賦值新的字符串,其實弄清楚問題的關鍵是理解下面這句代碼究做了什麼?

mStr = @"newMstr".mutableCopy;

這句代碼的含義可以歸納爲:@"mStr".mutableCopy創建了新的字符串對象,並將新對象的地址返回,最後又賦值給了mStr;可我們知道mStr指針是在棧上的,它隨時可能被釋放,直接修改就有可能造成野指針錯誤,這剛好對應了先前自動變量不可修改的問題;

但通過appendString爲什麼又可以修改字符串呢?這主要因爲mStr通過指針傳遞被Block捕獲後,Block只是藉助其內部的指針(和mStr同名,且指向同一個地址),找到了可變字符串的位置,向這塊內存追加新的內容,但是並未改變mStr的內存地址;

重要總結:Block修改外部變量的限制,其實是指Block不允許修改棧中指針的內容

三、理解__block存儲域類說明符的原理

通過以上的分析,我們可以將Block理解爲"可以帶有自動變量值的匿名函數",但由於存儲域的關係,Block並不能直接修改捕獲的自動變量。爲了解決這個問題,總結起來有兩種方案:
1.使用存儲域在靜態區的變量(如全局變量、靜態全局變量、靜態局部變量);
2.使用存儲域類說明符__block;

第一種方案我們已經分析過了,現在重點來理解__block存儲域說明符的用法,其實C語言中的還有許多其他存儲域類說明符,如:
typedef
extern
static
auto
register
__block說明符就類似於static、auto、register它們可以用於指定變量值設置到哪個存儲域中。例如,auto表示自動變量存儲在棧中(默認),static表示靜態變量存儲在數據區中。

下面我們來實際使用__block,使用它來修改被Block捕獲的自動變量,具體的代碼如下:

//__block存儲域修飾符
int main(int argc, const char * argv[]) {
    __block int val = 10;
    void (^myBlock)(void) = ^{ val = 20;};

    val = 30;
    myBlock();
    NSLog(@"val: %@",val);
    return 0;
}

此處代碼在Block中修改自動變量卻沒有像之前那樣報錯,說明__block說明符是有效的,爲了探究其中原理,現在我們再次把上述代碼轉換C++代碼,具體如下:

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref
 (val->__forwarding->val) = 20;}
 
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

int main(int argc, const char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));

    (val.__forwarding->val) = 30;
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_wd_fhcn9bn91v56nlzv9mt5z8ym0000gn_T_main_a9f88e_mi_0,(val.__forwarding->val));
    return 0;
}

分析代碼,我們會發現__block變量的初始化已經發生了根本的變化,此時的自動變量val對應的是C++源碼中的__Block_byref_val_0結構體。該結構體包含了五個成員變量,具體定義如下:

struct __Block_byref_val_0 {
  void *__isa;                      //isa指針
__Block_byref_val_0 *__forwarding;  //初始化傳遞的是自身結構體實例的指針
 int __flags;                       //標記flag
 int __size;                        //大小
 int val;                           //對應原自動變量val的值
};

我們看到__block變量val的初始值爲10,而這個值也出現在了調用__Block_byref_val_0結構體構造方法的時候,總結__block變量被捕獲的過程如下:
1.自動變量__block int varl被封裝爲__Block_byref_val_0結構體;
2.__Block_byref_val_0結構體包含一個與__block變量同名的成員變量val,對應外部自動變量的值;
3.__Block_byref_val_0結構體包含一個__forwarding指針,初始化傳遞的是自己的地址;
4.在Block初始化的過程中,調用__main_block_impl_0結構體構造函數時,會將__block變量__Block_byref_val_0結構體實例的指針作爲參數;

接下來分析給__block變量賦值的代碼,轉換後的源碼如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref
 (val->__forwarding->val) = 20;
}

在這裏,我們看到函數首先通過cself->val拿到了對應__block變量的結構體實例,然後又通過__Block_byref_val_0結構體實例的成員變量__forwarding,最終訪問到了結構體成員變量val;具體過程如下圖所示:
訪問__block變量
分析當前情況,我就會發現這裏有兩個很關鍵問題:
1.爲什麼要使用多餘的__forwarding指針來間接訪問變量?
2.當前__block說明符的作用僅僅體現在:將__block變量封裝爲__Block_byref_val_0結構體;這並未從根本上改變自動變量的性質,自動變量究竟是如何被修改的呢?

爲了理解上述問題,我們首先應該對下面的代碼有一個更加清晰的瞭解:

void (^myBlock)(void) = ^{ val = 10;};

代碼中創建後的Block直接賦值給了強指針,這其實滿足了ARC環境下編輯器對Block的優化:編譯器會自動將Block從棧拷貝到堆上,而Block中的用到的__block變量也會被一併拷貝,並且被堆上的Block持有。這樣即使Block語法所在的作用域結束,堆上的Block和__block變量依然繼續存在,自然也就不存在自動變量創建在棧上被釋放的問題了,藉助圖示理解如下:
在一個Block中使用__block變量
另外,當__block變量結構體實例在從棧上被拷貝到堆上時,會將成員變量的__forwarding的值替換爲複製目標堆上的__block變量結構體實例的地址。通過這種功能,無論是在Block語法中、Block語法外使用__block變量,還是__block變量配置在棧上或堆上,都可以順利訪問同__block變量。這就是__forwarding指針存在的意義。使用圖示理解如下:
在這裏插入圖片描述
重要總結:__block修飾的自動變量被封裝爲結構體,作爲一個對象隨着Block被拷貝到了堆上,解決了自動變量容易因作用域結束而釋放的問題。而__block變量結構體中的__forwarding則保證了無論在棧上還是堆上訪問的都是同一個__block變量;我們能夠成功修改__block變量的值,其實是修改了堆上被Block持有的__block變量的內部成員變量val。

其他問題:
1.ARC存在編譯器的自動優化,自動拷貝Block的情況還包含了很多種,這裏只是其中一種情況,上篇已分析過;
2.上述代碼中,__block說明符將基本類型的數據封裝爲結構體類型(其中包含了isa指針),這其實就說明__block變量已經是作爲了一個對象在使用,而對象類型被Block捕獲之後都會涉及一些釋放的問題,所以源碼也出現了許多與對象釋放相關的函數如:__main_block_copy_0__main_block_dispose_0等。這個問題後續會詳細分析;

四、__block變量的存儲域

Block的存儲域通常涉及到拷貝的操作,那麼對於__block變量又是如何處理的呢?使用__block變量的Block從棧上拷貝到堆上時,__block變量也會受到影響。

1.單個Block中使用__block變量

若一個Block中使用__block變量,則當該Block從棧拷貝到堆上時,使用的所有__block變量也全部被從棧上拷貝到堆上。使用圖示理解如下:
在這裏插入圖片描述

2.多個Block使用__block變量

多個Block使用__block變量時,任何一個Block從棧上拷貝到堆上,__block變量就會一併從棧上拷貝到堆上並被該Block所持有。當剩下的Block從棧拷貝到堆上時,被拷貝的Block持有__block變量,並增加__block變量的引用計數。使用圖示理解如下:
在這裏插入圖片描述

3.__block變量的釋放

如果拷貝到堆上的Block被釋放,那麼它使用的__block變量的引用計數會減一,如果引用計數爲0就會被釋放。使用圖示理解如下:
在這裏插入圖片描述
**重要總結:**無論是對基本類型還是對象使用__block修飾符,從轉化後的源碼來看,它們都會被轉化爲對應的結構體實例來使用,具有引用類型數據的特性。因此__block變量隨着Block被拷貝到堆上後,它們的內存管理與普通的OC對象引用計數內存管理模式完全相同。

五、理解Block對對象的捕獲

仔細觀察之前的源碼我們就會發現,Block捕獲對象類型和__block類型的變量(在底層被封裝爲結構體,也屬於對象)明顯比基本類型要複雜多,其實這裏主要是因爲對象類型還要涉及到釋放的問題。下面的代碼演示了Block對對象的捕獲的過程,具體如下:

typedef void(^AddBlock)(NSString *); //定義一種攜帶字符串參數的Block
int main(int argc, const char * argv[]) {
    AddBlock blk = nil;
    {
        NSMutableArray *mArr = @[].mutableCopy;
        blk = ^(NSString *string){
            [mArr addObject:string];
            NSLog(@"mArr count = %ld",[mArr count]);
        };
    }//NSMutableArray所在的作用域結束
    
    blk(@"A");
    blk(@"B");
    blk(@"C");
    return 0;
}

//打印結果:
mArr count = 1
mArr count = 2
mArr count = 3

分析代碼:當前爲ARC環境下,編譯器自動對訪問了自動變量的mArrblk進行了拷貝;所以mArr離開其所在的作用域結束時並沒有被釋放。雖然mArr指針已經不能使用,但是blk依然保留有對mArr的引用可以找到這塊內存。所以代碼也是運行正常的;

現在查看編譯器轉換後的源碼如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSMutableArray *mArr;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_mArr, int flags=0) : mArr(_mArr) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->mArr, (void*)src->mArr, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->mArr, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

由於代碼量較大,這裏只提供了與捕獲基本類型不同的部分;我們發現,當Block捕獲對象類型的變量時,此處的__main_block_desc_0結構體中多了copydispose兩個成員變量,而且它們的初始化分別使用了__main_block_copy_0__main_block_dispose_0的函數指針;

這裏主要的原因是,在Objective-C中,C語言結構體不能含有__strong、__weak修飾符的變量,因爲編譯器不知道應該如何進行C語言結構的初始化和廢棄操作,不能很好地管理內存。但是OC的運行時庫能夠準確把握Block從棧複製到堆以及堆上Block被廢棄的時機,所以這裏纔會增加與內存管理相關的變量和函數。

1.__main_block_copy_0函數

結構體__main_block_desc_0中的copy成員變量對應了__main_block_copy_0函數。

當Block從棧上拷貝到堆上時,__main_block_copy_0函數會被調用,然後再調用其內部的_Block_object_assign函數。_Block_object_assign函數就相當於retain操作,會自動根據__main_block_impl_0結構體內部的mArr是什麼類型的指針,對mArr對象產生強引用或者弱引用。如果mArr指針是__strong類型,則爲強引用,引用計數+1,如果mArr指針是__weak類型,則爲弱引用,引用計數不變。

2.__main_block_dispose_0函數

結構體__main_block_desc_0中的dispose成員變量對應了__main_block_dispose_0函數。
當Block被廢棄時,__main_block_dispose_0函數會被調用,__main_block_dispose_0函數就相當於release操作,將mArr對象的引用計數減1,如果此時引用計數爲0,那麼遵循引用計數的規則mArr也就被釋放了。

3.Block捕獲對象與__block變量的區別

其實Block捕獲對象與__block變量後,對於它們的內存管理的方式相同,也都是使用copy函數持有和disposde函數釋放;兩者體現在源碼上的不同,我們可以觀察下面的函數:

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->mArr, (void*)src->mArr, 3/*BLOCK_FIELD_IS_OBJECT*/);}

_Block_object_assign函數中的最後一個參數用於區分Block捕獲的是對象還是__block變量。

對象變量 __block變量
BLOCK_FIELD_IS_OBJECT BLOCK_FIELD_IS_BYREF

六、Block的循環引用問題

Block在從棧拷貝到堆上時,如果其中捕獲了強類型的對象,該對象就會被Block所持有。這樣很容易就會引起循環引用,我們來看下面的代碼:

typedef void(^MyBlock)(void);

@interface MyObject : NSObject
@property(nonatomic,copy) MyBlock block;
@end

@implementation MyObject
- (instancetype)init {
    self = [super init];
    return self;
}

- (void)dealloc {
    NSLog(@"MyObject dealloc!");
}
@end

int main(int argc, char * argv[]) {
    @autoreleasepool {
        MyObject *myObject = [[MyObject alloc] init];
        myObject.block = ^{
            //Capturing 'myObject' strongly in this block is likely to lead to a retain cycle
            NSLog(@"捕獲對象:%@", myObject );
        };
    }
    NSLog(@"myObject的作用域結束了");
    return 0;
}

不僅編譯器給出了內存泄漏的警告,而且測試結果也證實了MyObject的dealloc實例方法並沒有執行,這裏發生了循環引用。原因就在與myObjectblock在被自動拷貝到堆上的過程中持有了myObject,而myObject本身就持有了block,所以兩者相互持有就產生了問題。

現在就來總結類似情況下的Block循環引用的處理方法,可分爲ARC和MRC兩種情況:

1.解決ARC環境下的循環引用問題

方法1:使用弱引用修飾符__weak、和__unsafe_unretained修飾符;
使用__weak解決上述問題,需要改進的代碼如下:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        MyObject *myObject = [[MyObject alloc] init];
        __weak typeof(myObject) weakObject = myObject;
        myObject.block = ^{
            NSLog(@"捕獲對象:%@", weakObject );
        };
    }
    NSLog(@"myObject的作用域結束了");
    return 0;
}

上述代碼使用弱引用修飾符__weak ,在block內部對 myObject設置爲弱引用,弱引用不會導致Block捕獲對象的引用計數增加(這在上述分析中已經講過)。

注意__weak__unsafe_unretained的區別:
__weak:iOS4之後才提供使用,而且比__unsafe_unretained更加安全,因爲當它指向的對象銷燬時,會自動將指針置爲nil;推薦使用。
__unsafe_unretained:在__weak出現以前常用修飾符,其指向的對象銷燬時,指針存儲的地址值不變,所以沒有__weak安全。

方法2:使用__block說明符
回憶__block修飾基本類型的C++源碼,我們可以知道__block修飾對象時其實也會封裝一個結構體類型,而這個結構體中會持有自動變量對象,這樣就會造成下圖的情況:在這裏插入圖片描述
使用__block解決上述問題,需要改進的代碼如下:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        MyObject *myObject = [[MyObject alloc] init];
        __block MyObject *tempObject = myObject;
        myObject.block = ^{
            NSLog(@"捕獲對象:%@", tempObject );
            tempObject = nil;  //關鍵代碼1
        };
        myObject.block();      //關鍵代碼2:執行持有的block;
    }
    NSLog(@"myObject的作用域結束了");
    return 0;
}

上述代碼有兩句關鍵,已經通過註釋標註;在block中通過tempObject = nil這句代碼,__block變量tempObject對於MyObject類對象的強引用失效了,而這句代碼生效的前提又是block被調用了(關鍵代碼2);這種方式避免了循環引用的產生的過程如下圖:
在這裏插入圖片描述
**特別注意:**如果關鍵代碼2沒有被調用,同樣會造成循環引用。

使用__block變量相比弱引用修飾符的優缺點:
優點:
1.通過執行block的方式,可動態決定__block變量可以控制對象的持有時間;
2.在不能使用__weak修飾符的環境下,避免使用__unsafe_unretained(因爲要考慮野指針問題);
缺點:
爲了避免循環引用,必須執行Block;

2.解決MRC環境下的循環引用問題

方法1:使用弱引用修飾符__unsafe_unretained修飾符;
在MRC環境下不支持使用__weak,所以只能使用__unsafe_unretained;使用原理同ARC環境下相同,這裏不再贅述。

方法2:使用__block說明符
MRC環境下,__block說明符被用來避免循環引用。這是因爲當Block從棧拷貝到堆時,若Block使用的變量是附有__block說明符的id類型或者對象類型的自動變量,不會被retain,否則就會被retain。這一點和ARC環境是不同的。現在我們在MRC環境下改進代碼,具體如下:

int main(int argc, char * argv[]) {
    MyObject *myObject = [[MyObject alloc] init];
    __unsafe_unretained MyObject *tempObject = myObject;
    myObject.block = ^{
        NSLog(@"捕獲對象:%@", tempObject );
    };
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    [myObject autorelease];
    [pool drain];  //等同於[myObject release];
    return 0;
}
//打印結果:
//MyObject dealloc!

上述操作將代碼改爲了MRC下的自動釋放池,相比之前在ARC中使用__block,這裏沒有在Block內部置nil的操作,也沒有調用block,但同樣解決了循環引用的問題;

重要總結:__block說明符在ARC與MRC環境下的用途有很大區別,因此在編寫代碼時我們必須區分好這兩種環境。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章