老司機出品——源碼解析之從Block說開去

從Block說開去

近來把《iOS與OS X多線程和內存管理》這本書又掏出來看了一遍,這本書前前后后加起來看了能有三四遍了,每次看都有新的理解。現在就把個人對Block的一些理解記錄下來。

今天的內容中你會看到:

  • Block是什么
  • Block的實質
  • 關于Block對外部的賦值操作
  • Block類型及其存儲域
  • __block說明符
  • 關于Block引起的循環引用

Block是什么?

帶有自動變量的匿名函數。

————引自《iOS與OS X多線程和內存管理》

為什么這么說呢?
我們分別從匿名函數和帶自動變量兩個角度來說。

  • 1.匿名函數

首先,Blocks是C語言的擴充功能。

C語言中函數是這個樣子的:

void func() {
    printf("hello world");
}

如上,是一個C語言函數,而block是什么形式呢?

^void () {
    printf("hello world");
}

這種不帶函數名的函數就是所謂的匿名函數。

  • 2.帶自動變量

還是要從C語言函數中說起。

int a = 10;
void func (int b) {
    printf("%d + %d = %d",a,b,a+b);
}

int main(int argc, char * argv[]) {
    func(5);
    return 0;
}

上述的代碼主要想說明一件事,C語言函數中,函數體中使用的函數外部變量只有兩種:函數參數即全局變量。

我們再看看Block是如何使用的。

int main(int argc, char * argv[]) {
    int a = 10;
    void(^block)(int) = ^(int b) {
        printf("%d + %d = %d\n",a,b,a+b);
    };
    block(5);
    return 0;
}

我們看到,在此例中a已經不是全局變量了,而是一個局部變量,也就是自動變量。然而block卻可以正常使用,為什么呢?因為block內部維護了一個變量a的值,所以執行正確。這里你先不用糾結,下面會有源碼。由上我們就知道了什么叫做帶自動變量了。


Block的實質

想要看Block的實質我們還是Block的實現過程。我們還是要借助clang。

#include <stdio.h>

int main(int argc, char * argv[]) {
    int a = 10;
    void(^block)(int) = ^(int b) {
        printf("%d + %d = %d\n",a,b,a+b);
    };
    block(5);
    return 0;
}

還是這個簡單的函數,我們借助clang來轉換一下。這里為了稍后方便,我們盡量刪除其他無用頭文件,引入必要頭文件。

clang -rewrite-objc main.m

轉換完成后我們會發現main.m同文件夾下多了一個main.cpp的文件。
打開這個文件我們就會看到轉換后的源碼,而老司機讓同學們換頭文件的原因你也應該明白了,引入的頭文件中一些相關代碼也會在轉換的文件中。如果你跟老司機一樣只引入了stdio.h的話,那么現在command + L跳轉到第62行,復制62行至67行,command + 下調至文件底部粘貼,再跳至510行,command + shift + 上選中上面所有代碼,delete刪除后就剩下干貨了,大概是這個樣子的:

block實現

恩,我們看到這就是block的相關實現。

首先我們看block結構體中,三個成員變量,一個構造函數。

block

可以看到第一個成員變量是__block_impl的結構體,其中有指向block實現函數的函數指針,第二個成員變量是__main_block_desc_0,用來負責管理block的內存管理。第三個成員變量int型變量a。

老司機在這里解釋一下,int a這個成員變量就是上面提到的帶有的自動變量。因為block內部引用了外部的自動變量,所以在block結構體中多了一個同類型同名的成員變量。同樣,如果沒有引入外部的自動變量的話此處block結構體中也不會有這第三個成員變量。

現在我們將目光集中到main函數中。可以看到,第一行聲明了一個局域變量,第二行調用了block的構造函數,將block對應的函數指針Desc以及局部變量傳給了block。

然后我們看到第三行調用block結構體中的函數指針指向的函數,并把block自身參數傳給了函數指針指向的函數。

轉過來看block指向的函數,函數中首先從block自身中取出捕獲的自動變量a復制給一個臨時變量,同時執行原本block中的函數體

至此就完成了一次block的調用過程。

這里我們要注意一下捕獲的自動變量:

所謂捕獲的自動變量我們可以從兩方面來理解。

  • 1.我們看到在生成block的瞬間就將自動變量的值賦給了block。所以此時外界計時修改局部變量的值并不影響block中的值。

    int main(int argc, char * argv[]) {
    int a = 10;
    void(^block)(int) = ^(int b) {
        printf("%d + %d = %d\n",a,b,a+b);
    };
    a = 5;
    block(5);///執行結果為15
    return 0;
    

}
```
執行結果為15,上面的話正是最好的解釋。

  • 2.block中我們是不能對捕獲的變量進行賦值操作的,只要這么做編譯器就會警告。為什么蘋果會做出這樣的限制呢?因為在block里對捕獲的自動變量復制其實是有歧義的。因為通過看__main_block_func_0內部的實現我們知道,block內部使用的都是block捕獲到自動變量,當然這個自動變量是我們轉換代碼之前完全不知道的一個概念。也就是在編碼過程中我們在block中使用的變量與實際代碼運行過程中block內部操作的變量本就是兩個變量,所以在這里修改block捕獲的自動變量的值事實上跟開發者預期的結果完全是兩個結果。所以蘋果干脆在此就給出個警告來避免未知的錯誤。

  • 3.雖說不能對捕獲的自動變量進行賦值操作,但這并不影響我們使用他,否則的話這個自動變量捕獲到也沒有什么用了。這點很好理解,沒什么好解釋的。

說到這里,是時候來一個本節的扣題了,所以說block的實質事實上就是一個結構體,而且是一個可以根據自身捕獲的自動變量個數自動添加自身成員變量的結構體。更多情況下,其實你把它考慮成對象更好。


關于Block對外部的賦值操作

上文中老司機說到,Block不能對其捕獲的局部(非靜態)變量的值進行賦值操作。既然有這些限制,那么一定有可以Block中可以做賦值操作的變量,他們都有誰呢?

  • 靜態變量
  • 全局變量
  • __block說明符修飾的變量

還是針對帶有自動變量的匿名函數這句話來講。這一節我們來探討一下Block是如何使用外部變量的。我們知道Block截獲變量的意義在于想要使用Block作用于內無法使用的變量,所以他要截獲變量。接下來老司機圍繞著這句話從各種變量類型做深入的展開。

  • 1.僅使用參數的Block
int main(int argc, char * argv[]) {
    void(^block)(int) = ^(int a) {
        a = 10;
        printf("block : a = %d\n",a);
    };
    int a = 5;
    block(a);
    printf("a = %d",a);
    return 0;
}

///clang轉換后的形式
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a) {
    a = 10;
    printf("block : a = %d\n",a);
}

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[]) {
    void(*block)(int) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    int a = 5;
    ((void (*)(__block_impl *, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, a);
    printf("a = %d",a);
    return 0;
}

由于使用的是函數的參數,是在Block作用域內可以使用的,所以Block沒有對變量進行截獲。這個Block基本就是最簡單的函數。

  • 2.使用局部變量(非靜態)
int main(int argc, char * argv[]) {
    int a = 10;
    void(^block)() = ^() {
        printf("n = %d",a);
    };
    block();
    return 0;
}

///clang轉換后
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int a;
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int a = __cself->a; // bound by copy
    printf("n = %d",a);
}

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 a = 10;
    void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

我們看到,這里截獲了這個局部變量,具體原因在上述內容中有講到過,此處不再贅述。

  • 3.局部靜態變量

我們知道,靜態變量存儲在靜態區,只創建一次,隨后使用的同名變量均應指向同一地址。由靜態變量的特性我們應該知道,如果Block截獲了一個靜態局域變量,并在Block中對其值進行了更改,這個操作應該是有效的,他應該改變該變量的值。我們看下他是如何實現的?

int main(int argc, char * argv[]) {
    static int a = 10;
    void(^block)() = ^() {
        a = 20;
    };
    block();
    printf("a = %d",a);
    return 0;
}
///clang轉換后
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int *a;
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int *a = __cself->a; // bound by copy
    (*a) = 20;
}

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[]) {
    static int a = 10;
    void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    printf("a = %d",a);
    return 0;
}

我們看到了,Block截獲的是局部靜態變量的指針。這個思路跟C語言中函數一樣。C語言中我們想更改實參的值時也是通過傳址的方式實現的。形如:

void mySwap(int * a,int * b);
int main(int argc, char * argv[]) {
    int a = 1;
    int b = 2;
    mySwap(&a, &b);
    printf("a = %d",a);
    return 0;
}

void mySwap(int * a,int * b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

4.全局變量(靜態與非靜態)
老司機上面說過,Block捕獲變量是為了在Block中使用其作用域外的變量,那么全局變量本身作用在區域,Block可以使用,故不需要對全局變量進行捕獲。以下以全局靜態變量為例。

static int a = 10;
int main(int argc, char * argv[]) {
    void(^block)() =  ^{
        a = 20;
    };
    block();
    printf("a = %d",a);
    return 0;
}

///clang 轉換后
static int a = 10;

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        a = 20;
}

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[]) {
    void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    printf("a = %d",a);
    return 0;
}
  • 5.__block修飾的變量
    我們知道,被__block修飾的局部變量,在Block內部對其進行賦值操作是可以的,那么他是如何實現的呢?
int main(int argc, char * argv[]) {
    __block int a = 10;
    void(^block)() =  ^{
        a = 20;
    };
    block();
    printf("%d",a);
    return 0;
}
///clang 轉換后
struct __Block_byref_a_0 {
    void *__isa;
    __Block_byref_a_0 *__forwarding;
    int __flags;
    int __size;
    int a;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_a_0 *a; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__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_a_0 *a = __cself->a; // bound by ref
    (a->__forwarding->a) = 20;
}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 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, char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
    void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    printf("%d",(a.__forwarding->a));
    return 0;
}

我們看到__block修飾的變量會自動為其生成一個結構體,并在之后對變量的操作使用的都是結構體中持有的變量a。而后Block捕獲了結構體,Block中對變量的復制也映射成了對結構體內部變量的賦值。我們可以發現,在上面的例子中,不僅生成了一個__block變量的結構體,還多了__main_block_copy_0__main_block_dispose_0兩個函數,的具體作用我們稍后再表。


Block類型及其存儲域

首先應該了解一下Block的三種類型:

  • _NSConcreteStackBlock///棧區Block
  • _NSConcreteMallocBlock///堆區Block
  • _NSConcreteGlobalBlock///全局Block

我們設想這樣一種情況,上述的例子中,我們看到我們都是在main函數中聲明的Block,也就是說他其實block對象其實是一個局域變量,那么他一定會被存儲在棧上。也就是說當出了變量的作用于,也就是main函數結束,block對象就會被銷毀。這時我們的Block即為_NSConcreteStackBlock。我們可以從__block_impl結構體中的isa指針看到上述例子中的Block均為_NSConcreteStackBlock類型。

但是平時我們使用block的時候還有這么一種情況,即并不是在聲明的地方立即使用,而是在等待某個時機從而進行回調。而此時一般是已經出了block對象的作用域,如果跟之前一樣是棧區block的話顯然block已經被銷毀,此時進行回調只會引起crash。這時我們就需要_NSConcreteMallocBlock區的block了,即堆區Block。回想我們持有block的時候使用什么修飾符呢?copy對吧,而block對象執行copy操作就是將其按需復制到堆區

我們看下下面的例子:

#import <Foundation/Foundation.h>

int globalVar = 100;

void(^globalBlock)() = ^{
    NSLog(@"global block beyond main");
};

int main(int argc, char * argv[]) {
    int a = 10;
    NSLog(@"stack block: %@",^{NSLog(@"here a = %d",a);});
    NSLog(@"malloc block: %@",[^{NSLog(@"here a = %d",a);} copy]);
    NSLog(@"global block: %@",^{NSLog(@"I use nothing");});
    NSLog(@"global block beyong main: %@",globalBlock);
    NSLog(@"global block use globalVar: %@",^{NSLog(@"%d",globalVar);});
    return 0;
}

///輸出:
stack block: <__NSStackBlock__: 0x7fff5d7cd578>
malloc block: <__NSMallocBlock__: 0x60000004ef70>
global block: <__NSGlobalBlock__: 0x1024320d0>
global block beyong main: <__NSGlobalBlock__: 0x102432050>
global block use globalVar: <__NSGlobalBlock__: 0x102432110>

此處我們可以看到三種block類型。從源碼我們可以知道默認生成的block均為_NSConcreteStackBlock類型,而后執行了copy操作的block為_NSConcreteMallocBlock類型,后面三個均為_NSConcreteGlobalBlock類型。

這里我們先說_NSConcreteGlobalBlock類型的Block。在全局范圍內聲明的Block即為全局Block,并且沒有引入自動變量的也為全局Block

現在我們知道了,調用過copy方法的block會被復制到堆區,堆區的Block均為_NSConcreteMallocBlock類型。那么什么情況下block會執行copy方法呢?

其實我們可以從上述的分析中猜到,當block需要在其作用域外使用的我們應該將其復制到堆區。例如block作為函數返回值的時候,這時候編譯器會按需調用copy方法:

typedef void(^voidBlock)();
voidBlock func();

int main(int argc, char * argv[]) {
    NSLog(@"the return value of func:%@",func());
    return 0;
}


voidBlock func() {
    int a = 10;
    NSLog(@"the block in func:%@",^{NSLog(@"block in func : a = %d",a);});
    return ^{
        NSLog(@"block in func : a = %d",a);
    };
}

///輸出:
the block in func:<__NSStackBlock__: 0x7fff56877558>
the return value of func:<__NSMallocBlock__: 0x6080000486a0>

此例中我們看到函數體中,輸出了一個Block其為棧區Block,但是當將同樣的Block作為返回值返回到main函數中的時候,他變成了堆區Block

同學們不要說我這不是一個Block,我應該生成一個Block將其賦值,Log一下,在返回出去。這個真不是我不賦值,我不能啊,因為在ARC中賦值的時候如果不附加修飾符的話默認認為生成的變量是以__strong修飾符修飾的,而編譯器遇到__strong修飾符會自動copy。。。我怎么給你做例子啊。。。反正老司機這么寫雖然不是同一個block,但是應該是同一類型block,足以說明問題。另外老司機說過,編譯器會按需調用copy方法。也就是說棧區block會出作用域銷毀,全局block并不會,所以如果返回值是一個全局block的話,則不會調用copy方法

此外以下兩種情況也會由系統為我們調用copy方法:

  • Cocoa框架的方法且方法名中含有usingBlock等時
  • GCD的API

還有就是顯示調用copy方法的時候,另外如果將其賦值給有copy修飾符修飾的屬性的話也會調用copy方法。

然而什么時候應該調用copy方法呢?我們先來看下不同類型block調用copy方法會有什么行為。

Block類型 副本源的配置存儲域 復制效果
_NSConcreteStackBlock 從棧復制到堆
_NSConcreteGlobalBlock 程序的數據區域 什么也不做
_NSConcreteMallocBlock 引用計數增加

不管Block配置在何處,用copy方法復制都不會引起任何問題。在不確定時調用copy方法即可。

————引自《iOS與OS X多線程和內存管理》

但是在我們確定的時候,還是要根據需要調用copy方法,不要盲目調用copy方法,畢竟這個方法是十分占用CPU資源的。


__block說明符

上文中,老司機已經講述了block對象在調用copy方法時候的行為。然而__block說明符修飾的變量與block對象基本一致。

__block變量的配置存儲域 Block從棧復制到堆時的影響
從棧復制到堆并被Block持有
被Block持有

正如老司機在上文中提到的,被__block說明符的變量會自動生成一個結構體。
值得一提的是三個地方:

forwarding.png

老司機之前說過,只有被__block說明符修飾的變量,今后使用的均為其結構體中維護的同名成員變量,不過從源碼中我們看到,并不是簡單地使用了成員變量,而是a.__forwarding->a這樣一個引用方式,這是因為什么呢?

首先從__Block_byref_a_0中我們可以看到__forwarding是一個__Block_byref_a_0類型的結構體指針。

從main函數中第一行__block變量生成的代碼我們看出,在本例中生成__block變量a的同時將a的__forwarding指向了a自身。這樣a.__forwarding->a最終還是指向了__block變量a結構體中的成員變量a。

既然這樣,就一定存在__forwarding并不指向block變量自身的情況,故此才需要__forwarding存在來保證時刻能取到一個正確的值。而上文中提到的調用copy方法的時候,就會對__forwarding指針進行操作

__forwarding

由上圖我們可以看到,當調用copy方法后,__forwarding指針指向堆中的__block變量。而堆中的__block變量的__forwarding指針則指向自身。

同時我們知道,block其實是對c語言的擴充,然而OC中我們使用的是引用計數來管理對象生命周期,而不是GC。所以事實上Block需要自行管理內存。那么當我們的Block捕獲了一個對象時,他又是如何管理其引用計數的呢?
上文中老司機有提到過__main_block_copy__main_block_dispose兩個函數。當Block結構體中捕獲到的對象需要retain的時候則調用__main_block_copy方法增加引用計數,當其需要釋放的時候則調用__main_block_dispose釋放對象。所以當block從棧上復制到堆的時候會調用copy函數,而對上的block被釋放時調用dispose函數。


關于Block引起的循環引用

一直以來,Block引起的循環引用都讓不少初級工程師,甚至包括一些中級工程師(索性就叫他中級吧。。。)談虎色變。他們不知道Block是如何引起循環引用的,只知道__weak可以避免循環引用。知其然不知其所以然,鬧出一些笑話也是讓人無語。

首先說一下什么是循環引用?

引用計數機制不做展開,我們只需要知道,在OC中對象是在引用計數為0的時候進行銷毀的。一個對對象的強引用會造成一次引用計數的加一。釋放一個強引用會造成引用計數的減一。

強引用

上圖我們知道,對象A對對象B有一個強引用。當對象A銷毀的時候,會釋放對對象B的強引用。如果此時對象B的引用計數為0則對象B被銷毀

循環引用

但當出現上圖的情況,強引用出現了一個閉環的時候,就會造成逐個等待上一個強引用釋放信號,然而閉環導致任意一個對象都不會釋放對下一個對象的強引用,這就是循環引用

明確一點,造成循環引用的必要條件的閉環,所以循環引用不僅可以發生在兩個對象之間,可以是多個對象,甚至可以是一個對象。

循環引用

由此看來Block引起循環引用的原因就很明白了,Block對內部使用的自動變量造成一個強引用,而如果這個自動變量恰好對Block也有強引用的話就會造成循環引用

既然知道了循環引用的起因,那么我們只要打破引用的閉環就可以輕松解決。兩個思路,一個是從最開始就不讓強引用成為閉環,使用弱引用。另一個思路是找到一個合適的時機主動釋放一個強引用,打破閉環

打破閉環
  • 弱引用
__weak typeof(self)weakSelf = self;
self.block = ^{
    NSLog(@"%@",weakSelf);
};
self.block();

上述代碼中,使用__weak生成一個弱引用變量weakSelf,保持對self的弱引用。然后Block捕獲到weakSelf,對weakSelf也是弱引用,然而卻沒有造成閉環。故避免了循環引用。


weakSelf
  • 主動釋放
__block id blockSelf = self;
self.block = ^{
    NSLog(@"%@",blockSelf);
    blockSelf = nil;
};
NSLog(@"%@",self.block);
self.block();

上述代碼中,使用__block生成一個block對象blockSelf,保持對self的強引用。然后Block捕獲到blockSelf,強引用blockSelf,由于self對block還有一個強引用,此時形成了一個閉環。但當block調用的時候,內部最后將blockSelf對象置為nil。由于blockSelf置為nil,__block對象失去強引用被銷毀,同時釋放對self的強引用,從而打破閉環。


blockSelf

不過兩種避免循環引用的方式都有各自的缺點。

__weak 的弱引用形式的缺點在于,當block執行的時候,由于對self是弱引用,不能保證self對象是否已經被銷毀。事實上block執行前self被銷毀還好,頂多是不執行。但是如果在block執行過程中,self被銷毀就會造成不可預估的后果。所以當使用__weak的時候我們通常會看到如下結構:

__weak typeof(self)weakSelf = self;
self.block = ^{
    __strong typeof(weakSelf)strongSelf = weakSelf;
    NSLog(@"%@",strongSelf);
};
self.block();

這樣的結構可以保證在block執行過程中,不會因為self釋放引起問題,然而如果block執行前self被釋放后block也就沒有機會執行了,也算是對代碼的保護。更多的關于Weak-Strong-Dance的討論可以看下這篇文章:

Weak-Strong-Dance真的安全嗎?

__weak有這樣的缺點,為什么不適用__block等方式呢?

事實上__block同樣有著自己的煩惱,就是一定要在block體中對__block對象置為nil,且block一定要執行才可以解決循環引用。所以開發者要根據具體情況合理的選擇解決循環引用的方式。


至此,老司機今天的內容也就算結束了。

參考資料:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,740評論 3 420
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,856評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,931評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,321評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,533評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,082評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,891評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,319評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,794評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,076評論 2 375

推薦閱讀更多精彩內容