你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 深入研究Block捕獲外部變量和__block實現原理

深入研究Block捕獲外部變量和__block實現原理

編輯:IOS開發基礎

前言

Blocks是C語言的擴充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了這個新功能“Blocks”。從那開始,Block就出現在iOS和Mac系統各個API中,並被大家廣泛使用。一句話來形容Blocks,帶有自動變量(局部變量)的匿名函數。

Block在OC中的實現如下:

struct Block_layout {    void *isa;    int flags;    int reserved;    void (*invoke)(void *, ...);    struct Block_descriptor *descriptor;    /* Imported variables. */};struct Block_descriptor {    unsigned long int reserved;    unsigned long int size;    void (*copy)(void *dst, void *src);    void (*dispose)(void *);
};

從結構圖中很容易看到isa,所以OC處理Block是按照對象來處理的。在iOS中,isa常見的就是_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock這3種(另外只在GC環境下還有3種使用的_NSConcreteFinalizingBlock,_NSConcreteAutoBlock,_NSConcreteWeakBlockVariable,本文暫不談論這3種,有興趣的看看官方文檔)

以上介紹是Block的簡要實現,接下來我們來仔細研究一下Block的捕獲外部變量的特性以及__block的實現原理。

研究工具:clang
為了研究編譯器的實現原理,我們需要使用 clang 命令。clang 命令可以將 Objetive-C 的源碼改寫成 C / C++ 語言的,借此可以研究 block 中各個特性的源碼實現方式。該命令是

clang -rewrite-objc block.c

目錄

  • 1.Block捕獲外部變量實質

  • 2.Block的copy和release

  • 3.Block中__block實現原理

一.Block捕獲外部變量實質


拿起我們的Block一起來捕捉外部變量吧。

說到外部變量,我們要先說一下C語言中變量有哪幾種。一般可以分為一下5種:

  • 自動變量

  • 函數參數

  • 靜態變量

  • 靜態全局變量

  • 全局變量

研究Block的捕獲外部變量就要除去函數參數這一項,下面一一根據這4種變量類型的捕獲情況進行分析。

我們先根據這4種類型

  • 自動變量

  • 靜態變量

  • 靜態全局變量

  • 全局變量

寫出Block測試代碼。


這裡很快就出現了一個錯誤,提示說自動變量沒有加__block,由於__block有點復雜,我們先實驗靜態變量,靜態全局變量,全局變量這3類。測試代碼如下:

#import int global_i = 1;static int static_global_j = 2;int main(int argc, const char * argv[]) {    static int static_k = 3;    int val = 4;    void (^myBlock)(void) = ^{
        global_i ++;
        static_global_j ++;
        static_k ++;        NSLog(@"Block中 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);
    };

    global_i ++;
    static_global_j ++;
    static_k ++;
    val ++;    NSLog(@"Block外 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);

    myBlock();    return 0;
}

運行結果

Block 外  global_i = 2,static_global_j = 3,static_k = 4,val = 5Block 中  global_i = 3,static_global_j = 4,static_k = 5,val = 4

這裡就有2點需要弄清楚了
1.為什麼在Block裡面不加__bolck不允許更改變量?
2.為什麼自動變量的值沒有增加,而其他幾個變量的值是增加的?自動變量是什麼狀態下被block捕獲進去的?

為了弄清楚這2點,我們用clang轉換一下源碼出來分析分析。

(main.m代碼行37行,文件大小832bype, 經過clang轉換成main.cpp以後,代碼行數飙升至104810行,文件大小也變成了3.1MB)

源碼如下

int global_i = 1;static int static_global_j = 2;struct __main_block_impl_0 {  struct __block_impl impl;  struct __main_block_desc_0* Desc;  int *static_k;  int val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val, int flags=0) : static_k(_static_k), 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_k = __cself->static_k; // bound by copy
  int val = __cself->val; // bound by copy

        global_i ++;
        static_global_j ++;
        (*static_k) ++;        NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_0,global_i,static_global_j,(*static_k),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, const char * argv[]) {    static int static_k = 3;    int val = 4;    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val));

    global_i ++;
    static_global_j ++;
    static_k ++;
    val ++;    NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_1,global_i,static_global_j,static_k,val);

    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);    return 0;
}

首先全局變量global_i和靜態全局變量static_global_j的值增加,以及它們被Block捕獲進去,這一點很好理解,因為是全局的,作用域很廣,所以Block捕獲了它們進去之後,在Block裡面進行++操作,Block結束之後,它們的值依舊可以得以保存下來。

接下來仔細看看自動變量和靜態變量的問題。
在__main_block_impl_0中,可以看到靜態變量static_k和自動變量val,被Block從外面捕獲進來,成為__main_block_impl_0這個結構體的成員變量了。

接著看構造函數,

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val, int flags=0) : static_k(_static_k), val(_val)

這個構造函數中,自動變量和靜態變量被捕獲為成員變量追加到了構造函數中。

main裡面的myBlock閉包中的__main_block_impl_0結構體,初始化如下

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


impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.FuncPtr = __main_block_impl_0; 
Desc = &__main_block_desc_0_DATA;
*_static_k = 4;
val = 4;

到此,__main_block_impl_0結構體就是這樣把自動變量捕獲進來的。也就是說,在執行Block語法的時候,Block語法表達式所使用的自動變量的值是被保存進了Block的結構體實例中,也就是Block自身中。

這裡值得說明的一點是,如果Block外面還有很多自動變量,靜態變量,等等,這些變量在Block裡面並不會被使用到。那麼這些變量並不會被Block捕獲進來,也就是說並不會在構造函數裡面傳入它們的值。

Block捕獲外部變量僅僅只捕獲Block閉包裡面會用到的值,其他用不到的值,它並不會去捕獲。

再研究一下源碼,我們注意到__main_block_func_0這個函數的實現

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

        global_i ++;
        static_global_j ++;
        (*static_k) ++;        NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_0,global_i,static_global_j,(*static_k),val);
    }

我們可以發現,系統自動給我們加上的注釋,bound by copy,自動變量val雖然被捕獲進來了,但是是用 __cself->val來訪問的。Block僅僅捕獲了val的值,並沒有捕獲val的內存地址。所以在__main_block_func_0這個函數中即使我們重寫這個自動變量val的值,依舊沒法去改變Block外面自動變量val的值。

OC可能是基於這一點,在編譯的層面就防止開發者可能犯的錯誤,因為自動變量沒法在Block中改變外部變量的值,所以編譯過程中就報編譯錯誤。錯誤就是最開始的那張截圖。

Variable is not assignable(missing __block type specifier)

小結一下:
到此為止,上面提出的第二個問題就解開答案了。自動變量是以值傳遞方式傳遞到Block的構造函數裡面去的。Block只捕獲Block中會用到的變量。由於只捕獲了自動變量的值,並非內存地址,所以Block內部不能改變自動變量的值。Block捕獲的外部變量可以改變值的是靜態變量,靜態全局變量,全局變量。上面例子也都證明過了。

剩下問題一我們還沒有解決。

回到上面的例子上面來,4種變量裡面只有靜態變量,靜態全局變量,全局變量這3種是可以在Block裡面被改變值的。仔細觀看源碼,我們能看出這3個變量可以改變值的原因。

  1. 靜態全局變量,全局變量由於作用域的原因,於是可以直接在Block裡面被改變。他們也都存儲在全局區。


  2. 靜態變量傳遞給Block是內存地址值,所以能在Block裡面直接改變值。

根據官方文檔我們可以了解到,蘋果要求我們在自動變量前加入 __block關鍵字(__block storage-class-specifier存儲域類說明符),就可以在Block裡面改變外部自動變量的值了。

總結一下在Block中改變變量值有2種方式,一是傳遞內存地址指針到Block中,二是改變存儲區方式(__block)。

先來實驗一下第一種方式,傳遞內存地址到Block中,改變變量的值。

#import int main(int argc, const char * argv[]) {  NSMutableString * str = [[NSMutableString alloc]initWithString:@"Hello,"];        void (^myBlock)(void) = ^{
            [str appendString:@"World!"];            NSLog(@"Block中 str = %@",str);
        };    NSLog(@"Block外 str = %@",str);

    myBlock();    return 0;
}

控制台輸出:

Block 外  str = Hello,Block 中  str = Hello,World!

看結果是成功改變了變量的值了,轉換一下源碼。

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

            ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)str, sel_registerName("appendString:"), (NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_1);            NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_2,str);
        }static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->str, (void*)src->str, 3/*BLOCK_FIELD_IS_OBJECT*/);}static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->str, 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 * str = ((NSMutableString *(*)(id, SEL, NSString *))(void *)objc_msgSend)((id)((NSMutableString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableString"), sel_registerName("alloc")), sel_registerName("initWithString:"), (NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_0);        void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, str, 570425344));    NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_3,str);

    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);    return 0;
}

在__main_block_func_0裡面可以看到傳遞的是指針。所以成功改變了變量的值。

至於源碼裡面的copy和dispose下一節會講到。

改變外部變量值的第二種方式是加 __block這個放在第三章裡面討論,接下來我們先討論一下Block的copy的問題,因為這個問題會關系到 __block存儲域的問題。

二.Block的copy和dispose


OC中,一般Block就分為以下3種,_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock。

先來說明一下3者的區別。

1.從捕獲外部變量的角度上來看
  • _NSConcreteStackBlock:
    只用到外部局部變量、成員屬性變量,且沒有強指針引用的block都是StackBlock。
    StackBlock的生命周期由系統控制的,一旦返回之後,就被系統銷毀了。

  • _NSConcreteMallocBlock:
    有強指針引用或copy修飾的成員屬性引用的block會被復制一份到堆中成為MallocBlock,沒有強指針引用即銷毀,生命周期由程序員控制

  • _NSConcreteGlobalBlock:
    沒有用到外界變量或只用到全局變量、靜態變量的block為_NSConcreteGlobalBlock,生命周期從創建到應用程序結束。

沒有用到外部變量肯定是_NSConcreteGlobalBlock,這點很好理解。不過只用到全局變量、靜態變量的block也是_NSConcreteGlobalBlock。舉例如下:

#import int global_i = 1;static int static_global_j = 2;int main(int argc, const char * argv[]) {    static int static_k = 3;    void (^myBlock)(void) = ^{            NSLog(@"Block中 變量 = %d %d %d",static_global_j ,static_k, global_i);
        };    NSLog(@"%@",myBlock);

    myBlock();    return 0;
}

輸出:

<__NSGlobalBlock__: 0x100001050>
Block中 變量 = 2 3 1

可見,只用到全局變量、靜態變量的block也可以是_NSConcreteGlobalBlock。

所以在ARC環境下,3種類型都可以捕獲外部變量。

2.從持有對象的角度上來看:
  • _NSConcreteStackBlock是不持有對象的。

//以下是在MRC下執行的
    NSObject * obj = [[NSObject alloc]init];    NSLog(@"1.Block外 obj = %lu",(unsigned long)obj.retainCount);    void (^myBlock)(void) = ^{        NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
    };    NSLog(@"2.Block外 obj = %lu",(unsigned long)obj.retainCount);

    myBlock();

輸出:

1.Block外 obj = 12.Block外 obj = 1Block中 obj = 1
  • _NSConcreteMallocBlock是持有對象的。

//以下是在MRC下執行的
    NSObject * obj = [[NSObject alloc]init];    NSLog(@"1.Block外 obj = %lu",(unsigned long)obj.retainCount);    void (^myBlock)(void) = [^{        NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
    }copy];    NSLog(@"2.Block外 obj = %lu",(unsigned long)obj.retainCount);

    myBlock();

    [myBlock release];    NSLog(@"3.Block外 obj = %lu",(unsigned long)obj.retainCount);

輸出:

1.Block外 obj = 12.Block外 obj = 2Block中 obj = 23.Block外 obj = 1
  • _NSConcreteGlobalBlock也不持有對象

//以下是在MRC下執行的
    void (^myBlock)(void) = ^{        NSObject * obj = [[NSObject alloc]init];        NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
    };

    myBlock();

輸出:

Block 中 obj = 1

由於_NSConcreteStackBlock所屬的變量域一旦結束,那麼該Block就會被銷毀。在ARC環境下,編譯器會自動的判斷,把Block自動的從棧copy到堆。比如當Block作為函數返回值的時候,肯定會copy到堆上。

1.手動調用copy
2.Block是函數的返回值
3.Block被強引用,Block被賦值給__strong或者id類型
4.調用系統API入參中含有usingBlcok的方法

以上4種情況,系統都會默認調用copy方法把Block賦復制

但是當Block為函數參數的時候,就需要我們手動的copy一份到堆上了。這裡除去系統的API我們不需要管,比如GCD等方法中本身帶usingBlock的方法,其他我們自定義的方法傳遞Block為參數的時候都需要手動copy一份到堆上。

copy函數把Block從棧上拷貝到堆上,dispose函數是把堆上的函數在廢棄的時候銷毀掉。

#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))#define Block_release(...) _Block_release((const void *)(__VA_ARGS__))// Create a heap based copy of a Block or simply add a reference to an existing one.// This must be paired with Block_release to recover memory, even when running// under Objective-C Garbage Collection.BLOCK_EXPORT void *_Block_copy(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);// Lose the reference, and if heap based and last reference, recover the memoryBLOCK_EXPORT void _Block_release(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);// Used by the compiler. Do not call this function yourself.BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);// Used by the compiler. Do not call this function yourself.BLOCK_EXPORT void _Block_object_dispose(const void *, const int)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

上面是源碼中2個常用的宏定義和4個常用的方法,一會我們就會看到這4個方法。

static void *_Block_copy_internal(const void *arg, const int flags) {    struct Block_layout *aBlock;    const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;    // 1
    if (!arg) return NULL;    // 2
    aBlock = (struct Block_layout *)arg;    // 3
    if (aBlock->flags & BLOCK_NEEDS_FREE) {        // latches on high
        latching_incr_int(&aBlock->flags);        return aBlock;
    }    // 4
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {        return aBlock;
    }    // 5
    struct Block_layout *result = malloc(aBlock->descriptor->size);    if (!result) return (void *)0;    // 6
    memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first

    // 7
    result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
    result->flags |= BLOCK_NEEDS_FREE | 1;    // 8
    result->isa = _NSConcreteMallocBlock;    // 9
    if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
        (*aBlock->descriptor->copy)(result, aBlock); // do fixup
    }    return result;
}

上面這一段是Block_copy的一個實現,實現了從_NSConcreteStackBlock復制到_NSConcreteMallocBlock的過程。對應有9個步驟。

void _Block_release(void *arg) {    // 1
    struct Block_layout *aBlock = (struct Block_layout *)arg;    if (!aBlock) return;    // 2
    int32_t newCount;
    newCount = latching_decr_int(&aBlock->flags) & BLOCK_REFCOUNT_MASK;    // 3
    if (newCount > 0) return;    // 4
    if (aBlock->flags & BLOCK_NEEDS_FREE) {        if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)(*aBlock->descriptor->dispose)(aBlock);
        _Block_deallocator(aBlock);
    }    // 5
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        ;
    }    // 6
    else {
        printf("Block_release called upon a stack Block: %p, ignored\\\\n", (void *)aBlock);
    }
}

上面這一段是Block_release的一個實現,實現了怎麼釋放一個Block。對應有6個步驟。

上述2個方法的詳細解析可以看這篇文章

回到上一章節中最後的例子,字符串的例子中來,轉換源碼之後,我們會發現多了一個copy和dispose方法。

因為在C語言的結構體中,編譯器沒法很好的進行初始化和銷毀操作。這樣對內存管理來說是很不方便的。所以就在 __main_block_desc_0結構體中間增加成員變量 void (*copy)(struct  __main_block_impl_0*, struct __main_block_impl_0*)和void (*dispose)(struct __main_block_impl_0*),利用OC的Runtime進行內存管理。

相應的增加了2個方法。

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

這裡的_Block_object_assign和_Block_object_dispose就對應著retain和release方法。

BLOCK_FIELD_IS_OBJECT 是Block截獲對象時候的特殊標示,如果是截獲的__block,那麼是BLOCK_FIELD_IS_BYREF。

三.Block中__block實現原理

我們繼續研究一下__block實現原理。

1.普通非對象的變量

先來看看普通變量的情況。

#import int main(int argc, const char * argv[]) {

    __block int i = 0;    void (^myBlock)(void) = ^{
        i ++;        NSLog(@"%d",i);
    };

    myBlock();    return 0;
}

把上述代碼用clang轉換成源碼。

struct __Block_byref_i_0 {  void *__isa;
__Block_byref_i_0 *__forwarding; int __flags; int __size; int i;
};struct __main_block_impl_0 {  struct __block_impl impl;  struct __main_block_desc_0* Desc;
  __Block_byref_i_0 *i; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__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_i_0 *i = __cself->i; // bound by ref

        (i->__forwarding->i) ++;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_3b0837_mi_0,(i->__forwarding->i));
    }static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 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_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));

    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);    return 0;
}

從源碼我們能發現,帶有 __block的變量也被轉化成了一個結構體__Block_byref_i_0,這個結構體有5個成員變量。第一個是isa指針,第二個是指向自身類型的__forwarding指針,第三個是一個標記flag,第四個是它的大小,第五個是變量值,名字和變量名同名。

__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};

源碼中是這樣初始化的。__forwarding指針初始化傳遞的是自己的地址。然而這裡__forwarding指針真的永遠指向自己麼?我們來做一個實驗。

//以下代碼在MRC中運行
    __block int i = 0;    NSLog(@"%p",&i);    void (^myBlock)(void) = [^{
        i ++;        NSLog(@"這是Block 裡面%p",&i);
    }copy];

我們把Block拷貝到了堆上,這個時候打印出來的2個i變量的地址就不同了。

0x7fff5fbff818<__NSMallocBlock__: 0x100203cc0>
這是Block 裡面 0x1002038a8

地址不同就可以很明顯的說明__forwarding指針並沒有指向之前的自己了。那__forwarding指針現在指向到哪裡了呢?

Block裡面的__block的地址和Block的地址就相差1052。我們可以很大膽的猜想,__block現在也在堆上了。

出現這個不同的原因在於這裡把Block拷貝到了堆上。

由第二章裡面詳細分析的,堆上的Block會持有對象。我們把Block通過copy到了堆上,堆上也會重新復制一份Block,並且該Block也會繼續持有該__block。當Block釋放的時候,__block沒有被任何對象引用,也會被釋放銷毀。

__forwarding指針這裡的作用就是針對堆的Block,把原來__forwarding指針指向自己,換成指向_NSConcreteMallocBlock上復制之後的__block自己。然後堆上的變量的__forwarding再指向自己。這樣不管__block怎麼復制到堆上,還是在棧上,都可以通過(i->__forwarding->i)來訪問到變量值。



所以在__main_block_func_0函數裡面就是寫的(i->__forwarding->i)。

這裡還有一個需要注意的地方。還是從例子說起:

//以下代碼在MRC中運行
    __block int i = 0;    NSLog(@"%p",&i);    void (^myBlock)(void) = ^{
        i ++;        NSLog(@"Block 裡面的%p",&i);
    };    NSLog(@"%@",myBlock);

    myBlock();

結果和之前copy的例子完全不同。

 0x7fff5fbff818<__NSStackBlock__: 0x7fff5fbff7c0>** 0x7fff5fbff818

Block在捕獲住__block變量之後,並不會復制到堆上,所以地址也一直都在棧上。這與ARC環境下的不一樣。

ARC環境下,不管有沒有copy,__block都會變copy到堆上,Block也是__NSMallocBlock。

感謝@酷酷的哀殿 指出錯誤,感謝@bestswifter 指點。上述說法有點不妥,詳細見文章末尾更新。

ARC環境下,一旦Block賦值就會觸發copy,__block就會copy到堆上,Block也是__NSMallocBlock。ARC環境下也是存在__NSStackBlock的時候,這種情況下,__block就在棧上。

MRC環境下,只有copy,__block才會被復制到堆上,否則,__block一直都在棧上,block也只是__NSStackBlock,這個時候__forwarding指針就只指向自己了。


至此,文章開頭提出的問題一,也解答了。__block的實現原理也已經明了。

2.對象的變量

還是先舉一個例子:

//以下代碼是在ARC下執行的#import int main(int argc, const char * argv[]) {

    __block id block_obj = [[NSObject alloc]init];    id obj = [[NSObject alloc]init];    NSLog(@"block_obj = [%@ , %p] , obj = [%@ , %p]",block_obj , &block_obj , obj , &obj);    void (^myBlock)(void) = ^{        NSLog(@"***Block中****block_obj = [%@ , %p] , obj = [%@ , %p]",block_obj , &block_obj , obj , &obj);
    };

    myBlock();    return 0;
}

輸出

block_obj = [ , 0x7fff5fbff7e8] , obj = [ , 0x7fff5fbff7b8]
Block****中********block_obj = [ , 0x100f000a8] , obj = [ , 0x100f00070]

我們把上面的代碼轉換成源碼研究一下:

struct __Block_byref_block_obj_0 {  void *__isa;
__Block_byref_block_obj_0 *__forwarding; int __flags; int __size; void (*__Block_byref_id_object_copy)(void*, void*); void (*__Block_byref_id_object_dispose)(void*); id block_obj;
};struct __main_block_impl_0 {  struct __block_impl impl;  struct __main_block_desc_0* Desc;  id obj;
  __Block_byref_block_obj_0 *block_obj; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _obj, __Block_byref_block_obj_0 *_block_obj, int flags=0) : obj(_obj), block_obj(_block_obj->__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_block_obj_0 *block_obj = __cself->block_obj; // bound by ref
  id obj = __cself->obj; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_e64910_mi_1,(block_obj->__forwarding->block_obj) , &(block_obj->__forwarding->block_obj) , obj , &obj);
    }static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->block_obj, (void*)src->block_obj, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->block_obj, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_dispose((void*)src->obj, 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[]) {

    __attribute__((__blocks__(byref))) __Block_byref_block_obj_0 block_obj = {(void*)0,(__Block_byref_block_obj_0 *)&block_obj, 33554432, sizeof(__Block_byref_block_obj_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))};    id obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));    NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_e64910_mi_0,(block_obj.__forwarding->block_obj) , &(block_obj.__forwarding->block_obj) , obj , &obj);    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, obj, (__Block_byref_block_obj_0 *)&block_obj, 570425344));

    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);    return 0;
}

首先需要說明的一點是對象在OC中,默認聲明自帶__strong所有權修飾符的,所以main開頭我們聲明的

__block id block_obj = [[NSObject alloc]init];id obj = [[NSObject alloc]init];

等價於

__block id __strong block_obj = [[NSObject alloc]init];id __strong obj = [[NSObject alloc]init];

在轉換出來的源碼中,我們也可以看到,Block捕獲了__block,並且強引用了,因為在__Block_byref_block_obj_0結構體中,有一個變量是id block_obj,這個默認也是帶__strong所有權修飾符的。

根據打印出來的結果來看,ARC環境下,Block捕獲外部對象變量,是都會copy一份的,地址都不同。只不過帶有__block修飾符的變量會被捕獲到Block內部持有。

我們再來看看MRC環境下的情況,還是將上述代碼的例子運行在MRC中。

輸出:

block_obj = [ , 0x7fff5fbff7e8] , obj = [ , 0x7fff5fbff7b8]
Block****中********block_obj = [ , 0x7fff5fbff7e8] , obj = [ , 0x7fff5fbff790]

這個時候block在棧上,__NSStackBlock__,可以打印出來retainCount值都是1。當把這個block copy一下,就變成__NSMallocBlock__,對象的retainCount值就會變成2了。

總結:

在MRC環境下,__block根本不會對指針所指向的對象執行copy操作,而只是把指針進行的復制。
而在ARC環境下,對於聲明為__block的外部對象,在block內部會進行retain,以至於在block環境內能安全的引用外部對象,所以才會產生循環引用的問題!

最後

關於Block捕獲外部變量有很多用途,用途也很廣,只有弄清了捕獲變量和持有的變量的概念以後,之後才能清楚的解決Block循環引用的問題。

再次回到文章開頭,5種變量,自動變量,函數參數 ,靜態變量,靜態全局變量,全局變量,如果嚴格的來說,捕獲是必須在Block結構體__main_block_impl_0裡面有成員變量的話,Block能捕獲的變量就只有帶有自動變量和靜態變量了。捕獲進Block的對象會被Block持有。

對於非對象的變量來說,

自動變量的值,被copy進了Block,不帶__block的自動變量只能在裡面被訪問,並不能改變值。


帶__block的自動變量 和 靜態變量 就是直接地址訪問。所以在Block裡面可以直接改變變量的值。


而剩下的靜態全局變量,全局變量,函數參數,也是可以在直接在Block中改變變量值的,但是他們並沒有變成Block結構體__main_block_impl_0的成員變量,因為他們的作用域大,所以可以直接更改他們的值。

值得注意的是,靜態全局變量,全局變量,函數參數他們並不會被Block持有,也就是說不會增加retainCount值。

對於對象來說,

在MRC環境下,__block根本不會對指針所指向的對象執行copy操作,而只是把指針進行的復制。
而在ARC環境下,對於聲明為__block的外部對象,在block內部會進行retain,以至於在block環境內能安全的引用外部對象。

請大家多多指點。

更新

在ARC環境下,Block也是存在__NSStackBlock的時候的,平時見到最多的是_NSConcreteMallocBlock,是因為我們會對Block有賦值操作,所以ARC下,block 類型通過=進行傳遞時,會導致調用objc_retainBlock->_Block_copy->_Block_copy_internal方法鏈。並導致 __NSStackBlock__ 類型的 block 轉換為 __NSMallocBlock__ 類型。

舉例如下:

#import int main(int argc, const char * argv[]) {

    __block int temp = 10;    NSLog(@"%@",^{NSLog(@"*******%d %p",temp ++,&temp);});    return 0;
}

輸出

<__NSStackBlock__: 0x7fff5fbff768>

這種情況就是ARC環境下Block是__NSStackBlock的類型。



  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved