Block的本質

當需要執行異步操作,或同步多個操作時,塊(Block)會非常有用。這一篇文章將介紹 Block 的本質。如果你對 block 還不瞭解,推薦先查看Block的用法

1. Block的本質

Block 是封裝了函數調用及函數調用環境的 Objective-C 對象,內部也有一個 isa 指針。即 Block 本質上也是一個 Objective-C 對象。

下面寫一個簡單的 block:

        int age = 10;
        void(^myblock)(void) = ^{
            NSLog(@"age: %d", age);
        };
        myblock();

使用 clang 命令將上述代碼轉化爲 C++,方便查看 block 內部結構:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

轉化後如下:

        int age = 10;
        // 定義block變量
        void(*myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
        // 調用block
        ((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);

1.1 聲明 block

1.1.1 __main_block_impl_0

通過轉化的 C++ 代碼可以看到,block定義中調用了__main_block_impl_0函數,並將其地址賦值給myblock。進一步查看__main_block_impl_0函數:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  
  // 該構造函數最終返回__main_block_impl_0。會將傳入的_age賦值給成員age。
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__main_block_impl_0結構體內的構造函數對變量進行賦值,最終返回__main_block_impl_0結構體,也就是最終返回給myblock變量的是__main_block_impl_0結構體。

1.1.2 __main_block_func_0

__main_block_impl_0函數的第一個參數是__main_block_func_0,其定義如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int age = __cself->age; // bound by copy
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_05_pj1lwvjs50j3gx6vjtvxcvf80000gn_T_main_63c9df_mi_0, age);
}

__main_block_func_0函數內存儲着 block 內代碼。其函數內部先取出局部變量 age,後面調用NSLog

也就是將 block 內的代碼封裝到__main_block_func_0函數,將__main_block_func_0函數地址傳遞給__main_block_impl_0

1.1.3 __main_block_desc_0

__main_block_impl_0函數的第二個參數是__main_block_desc_0,其定義如下:

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)};

__main_block_desc_0中存儲着兩個成員,reservedBlock_size。並且爲reserved賦值0,爲Block_size賦值sizeof(struct __main_block_impl_0),即block的大小。

最終,將__main_block_desc_0結構體傳給__main_block_impl_0中,賦值給desc。

1.1.4 age

__main_block_impl_0函數的第三個參數是age,即定義的局部變量。

如果在 block 中使用了局部變量,block 聲明的時候會將 age 作爲參數傳入,即 block 會捕獲(capture)age。如果 block 中沒有使用 age,則只會給__main_block_impl_0函數傳入__main_block_func_0__main_block_desc_0_DATA參數。

由於 block 在聲明時捕獲了局部變量,在聲明後、調用前修改局部變量值,不會影響 block 內捕獲到的局部變量值。如下所示:

        int age = 10;
        void(^myblock)(void) = ^{
            NSLog(@"age: %d", age);
        };
        age = 11;
        myblock();

執行後,控制檯打印如下:

age: 10

block 在定義之後已將局部變量age值存儲在__main_block_impl_0結構體,調用時直接從結構體中取出。聲明後修改局部變量的值不會影響__main_block_impl_0結構體捕獲的值。

1.1.5 __block_impl

__main_block_impl_0結構體第一個成員是__block_impl結構體,__block_impl結構體如下:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

isa指針指向類對象,FuncPtr指針存儲着__main_block_func_0函數地址,即 block 內代碼地址。

__block_impl結構體第一個成員就是 isa 指針。Objective-C對象本質上也是結構體,第一個成員也是 isa 指針。因此,block 本質上也是一個 OC 對象。__main_block_impl_0函數的構造函數將傳入 block 的值存儲到__main_block_impl_0結構體中,最終將__main_block_impl_0結構體地址賦值給myblock。

分析__main_block_impl_0構造函數,特點如下:

  • __main_block_func_0封裝了函數地址,其中先取出局部變量,再調用 block 內代碼。
  • __main_block_desc_0_DATA封裝 block 大小。
  • age是 block 捕獲的局部變量。
  • __main_block_impl_0結構體中的__block_impl結構體包含了isa指針、FuncPtr。

1.2 調用 block

        // 調用block
        ((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);

將上述代碼中的強制轉換移除後,變爲下面的代碼:

        (myblock->FuncPtr)(myblock);

調用myblock就是通過myblock找到FuncPtr指針,然後進行調用。

myblock是指向__main_block_impl_0結構體的指針,內部並沒有FuncPtr指針,爲什麼這裏可以直接訪問?這是因爲__main_block_impl_0結構體第一個成員是__block_impl,而__block_impl也是一個結構體,即__main_block_impl_0可以改爲以下內容:

struct __main_block_impl_0 {
  // 使用__block_impl直接替換
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
  
  struct __main_block_desc_0* Desc;
  int age;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

另一方面,__main_block_impl_0結構體第一個成員是__block_impl__main_block_impl_0結構體地址就是__block_impl的地址,這樣也可以查找到FuncPtr指針。

block 底層的數據結構也可以使用下面圖片表示:

2. 變量捕獲

除了包含可執行代碼,塊還具有捕獲塊以外值的能力。如果在一個方法內聲明瞭一個塊,該塊可以獲取方法內任何變量,也就是可以捕獲局部變量。

2.1 局部變量

2.1.1 auto變量

局部變量默認是 automatic variable 類型,簡寫爲auto,一般省略不寫。當程序進入、離開局部變量作用域時,會自動分配、釋放內存。

auto會自動捕獲到 block 內,__main_block_impl_0結構體內增加了存儲局部變量的成員。block 內訪問auto變量的方式是值傳遞,即直接將auto變量傳遞給__main_block_impl_0函數。

2.1.2 static變量

static變量會一直存儲在內存中。block 會捕獲static修飾的局部變量,訪問時使用指針訪問。

下面分別添加使用autostatic修飾的局部變量:

        auto int age = 10;
        static int weight = 125;
        void(^myblock)(void) = ^{
            NSLog(@"age: %d, weight: %d", age, weight);
        };
        myblock();

生成C++後如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // 捕獲了age、weight。
  int age;
  int *weight;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_weight, int flags=0) : age(_age), weight(_weight) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
  int *weight = __cself->weight; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_05_pj1lwvjs50j3gx6vjtvxcvf80000gn_T_main_e2f202_mi_0, age, (*weight));
        }

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[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        auto int age = 10;
        static int weight = 125;
        // age直接傳遞值,weight傳遞指針。
        void(*myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &weight));

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

可以看到,__main_block_impl_0捕獲了age、weight,並且給__main_block_impl_0函數傳遞age時直接傳遞值,傳遞weight時傳遞的是指針。

2.2 全局變量

block 是否會捕獲全局變量?以及如何使用?

添加以下全局變量:

int height = 170;
static int number = 11;

生成C++代碼如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  int *weight;
  // 並沒有捕獲全局變量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_weight, int flags=0) : age(_age), weight(_weight) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
  int *weight = __cself->weight; // bound by copy

            // 直接使用height、number
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_05_pj1lwvjs50j3gx6vjtvxcvf80000gn_T_main_964e22_mi_0, age, (*weight), height, number);
        }

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[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        auto int age = 10;
        static int weight = 125;
        void(*myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &weight));

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

可以看到__main_block_func_0並沒有添加任何全局變量,而是直接使用。這是因爲全局變量會一直存放在內存中,全局都可以使用。

3. Block 的類型

既然 block 也是 OC 對象,那麼 block 是什麼類型呢?

聲明一個 block,並打印其父類,如下所示:

        void(^myblock)(void) = ^{
            NSLog(@"github.com/pro648");
        };
        NSLog(@"%@", [myblock class]);
        NSLog(@"%@", [[myblock class] superclass]);
        NSLog(@"%@", [[[myblock class] superclass] superclass]);
        NSLog(@"%@", [[[[myblock class] superclass] superclass] superclass]);

輸出如下:

__NSGlobalBlock__
NSBlock
NSObject
(null)

即 block 的繼承關係是:__NSGlobalBlock__NSBlockNSObject。進一步證實了 block 本質上也是一個 OC 對象。

定義三個不同的 block,分別打印其類型:

        // 沒有調用外部變量的block
        void(^myblock1)(void) = ^{
            NSLog(@"github.com/pro648");
        };
        
        // 訪問auto變量
        int age = 10;
        void(^myblock2)(void) = ^{
            NSLog(@"age: %d", age);
        };
        
        // 直接調用block的 class
        NSLog(@"%@ %@ %@", [myblock1 class], [myblock2 class], [^{
            NSLog(@"%d", age);
        } class]);

打印如下:

__NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__

將上述代碼轉換爲C++,可以看到三個 block 類型都是_NSConcreteStackBlock類型。這可能是 runtime 運行時進行了某種轉換,使用 clang 生成的C++代碼僅供參考,不能保證和運行時完全一致。

三種類型的block在內存中的位置如下:

__NSGlobalBlock____NSStackBlock____NSMallocBlock__三種類型的block是按照以下規則產生的:

block 類型 環境
__NSGlobalBlock__ 沒有訪問auto變量
__NSStackBlock__ 訪問了auto變量
__NSMallocBlock__ __NSStackBlock__調用了copy方法

3.1 __NSGlobalBlock__

當 block 內沒有訪問auto變量時,block 爲__NSGlobalBlock__類型,__NSGlobalBlock__存在數據段中,程序結束纔會回收內存。但因爲其與普通函數沒有區別,很少使用__NSGlobalBlock__類型的 block。

3.2 __NSStackBlock__

在 block 內訪問了auto變量爲__NSStackBlock__類型。

__NSStackBlock__類型的 block 存放在棧中。棧的內存由系統自動分配和釋放,超出變量作用域後自動釋放。由於棧中代碼超出作用域之後,內存就會被銷燬,而有可能內存銷燬之後纔去調用它,此時就會出現問題。

ARC 自動管理內存時會幫助我們做很多事情,爲了方便理解其本質,先關閉 ARC 使用 MRC 管理內存。進入TARGETS > Build Settings > Objective-C Automatic Reference Counting,修改其值爲 NO。

關閉 ARC 後,使用以下代碼驗證問題:

void (^myblock)(void);

void test() {
    // __NSStackBlock__
    int age = 10;
    myblock = ^{
        NSLog(@"age: %d", age);
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        myblock();
    }
    return 0;
}

執行後控制檯輸出如下:

age: -272632840

這是因爲myblock是在棧中的,即__NSStackBlock__類型的。當test函數執行完畢後,棧內存中 block 已經被系統回收。

3.3 __NSMallocBlock__

爲了避免函數執行完畢棧內存立即被回收,可以將__NSStackBlock__block copy 到堆中。以下是修改後的代碼:

void (^myblock)(void);

void test() {
    // __NSMallocBlock__
    int age = 10;
    // 將 block 從棧中複製到堆中。
    myblock = [^{
        NSLog(@"age: %d", age);
    } copy];
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        myblock();
    }
    return 0;
}

執行後控制檯輸出如下:

age: 10

block 調用 copy 後,類型改變如下所示:

block類型 內存區域 調用copy的效果
__NSGlobalBlock__ 數據段 什麼都不做,類型不變。
__NSStackBlock__ 從棧複製到堆,類型變爲__NSMallocBlock__
__NSMallocBlock__ 引用計數加一,類型不變。

使用 MRC 管理內存時,經常需要使用 copy 保存 block,將棧上的 block 複製到堆上,超出作用域時 block 不會被釋放,後續需調用 release 銷燬 block。ARC 環境下,系統會自動調用 copy 操作,使 block 不被銷燬;不再使用時,自動調用 release 引用計數減一。

4. ARC 在某些情況下會對 block 自動進行一次 copy 操作,將其從棧區移動到堆區

出現以下情況時,ARC 會自動對 block 執行一次 copy 操作,將其從棧區移動到堆區:

  1. 當 block 作爲函數返回值時。
  2. 當 block 被強指針引用時。
  3. 當 Cocoa API 方法名包含usingBlock,且 block 作爲參數時,或 block 作爲 GCD API 方法參數。

4.1 當 block 作爲函數返回值時

typedef void (^MyBlock)(void);

MyBlock test() {
    int age = 10;
    // myblock 作爲函數返回值,ARC 會自動進行copy。
    MyBlock myblock = ^{
        NSLog(@"age: %d", age);
    };
    return myblock;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyBlock myblock = test();
        NSLog(@"%@", [myblock class]);
    }
    return 0;
}

在 ARC 環境下,參數返回值爲 block 類型時,系統會對 ARC 自動執行一次 copy 操作,使其變爲__NSMallocBlock__類型。在 MRC 環境下,超出作用域後 block 會被銷燬,此時再調用會引起閃退。

4.2 當 block 被強指針引用時

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        MyBlock myblock = ^{
            NSLog(@"age: %d", age);
        };
        NSLog(@"%@",[myblock class]);
    }
    return 0;
}

由於 block 訪問了auto變量,其是__NSStackBlock__類型。在 MRC 環境中,不會自動進行 copy 操作,輸出是__NSStackBlock__;在 ARC 環境中,有強指針引用時會自動執行 copy 操作,將 block 從棧中移動到堆中。

修改上述代碼如下,即取消強指針對 block 的引用:

        int age = 10;
        // 取消強指針的引用
        NSLog(@"%@",[^{
            NSLog(@"age: %d", age);
        } class]);

可以看到輸出爲:

__NSStackBlock__

手動調用 copy,如下所示:

        int age = 10;
        NSLog(@"%@", [[^{
            NSLog(@"age: %d", age);
        } copy] class]);

輸出爲:

__NSMallocBlock__

這也進一步證明了 ARC 環境下,有強指針引用 block 時會自動調用 copy 方法。

4.3 當 Cocoa API 方法名包含usingBlock,且 block 作爲參數時,或 block 作爲 GCD API 的方法參數

當 Cocoa API 方法名包含usingBlock,且 block 作爲參數時,或 block 作爲 GCD API 的方法參數。ARC 會根據情況自動將棧上的 block copy到堆上。

        // Cocoa API
        NSArray *arr = @[@1];
        [arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            // 這個 block 在堆上
        }];

        // GCD API
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            // 這個 block 在堆上
        });

block 作爲屬性時與其它屬性類似,但 MRC 環境下,只能使用copy修飾。因爲,block 訪問auto變量時,block 是__NSStackBlock__類型,超出作用域 block 會被自動銷燬。如果想要在外部繼續訪問、調用 block,就需要將 block 從棧中複製到堆中,因此需用copy修飾。

在 ARC 環境下,系統會在需要時自動進行 copy 操作。此時屬性可以使用strong,但copy更能表明用意。

5. Block 內引用對象

之前 block 內只引用過基本數據類型,這一部分介紹 block 內引用對象類型。如下所示:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            ^{
                NSLog(@"person.age = %d", person.age);
            }();
        }
        NSLog(@"--------");
    }
    return 0;
}

執行後控制檯輸出如下:

person.age = 10
-[Person dealloc]
--------

可以看到在打印虛線前person已經釋放。此時,block 是棧類型 block,即__NSStackBlock__。棧區 block 即便引用了對象,也會在超出作用域時一起釋放。

更新上述代碼如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyBlock myblock;
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            myblock = ^{
                NSLog(@"person.age = %d", person.age);
            };
            myblock();
        }
        NSLog(@"--------");
    }
    return 0;
}

執行後輸出如下:

person.age = 10
--------
-[Person dealloc]

可以看到執行到虛線位置時,person對象並沒有釋放。這是因爲 block 內部對person對象進行了強引用,block 又被 myblock 強指針引用,即 block 是堆類型。堆類型的 block 會對外部對象強引用。

使用以下命令生成 C++ 代碼,查看其底層實現:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 main.m

查看 C++ 代碼,block 定義如下:

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

__main_block_desc_0定義如下:

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兩個參數,用於管理對象內存。

copy操作調用的是__main_block_copy_0,如下所示:

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

最終調用_Block_object_assign函數,_Block_object_assign會對引用的對象person進行引用計數操作。如果引用的對象是__strong修飾(默認是__strong,即忽略時就是__strong),則引用計數加一;如果使用的__weak修飾,則引用計數不變。

當 block 執行完畢,會調用dispose方法,dispose底層會調用以下方法:

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

__main_block_dispose_0內部會調用_Block_object_dispose方法。如果之前 copy 時使用了強引用,此時引用計數減一;如果之前使用了弱引用,直接取消對原來對象的弱引用。

6. Block 內修改外部變量

如果外部變量是auto類型,block 通過值傳遞的方式捕獲變量。由於是值傳遞的方式進行的,其不能修改外部變量。如果需要外部變量,可以通過以下兩種方式:

  • 使用 static 修飾外部變量。
  • 使用__block修飾外部變量。

6.1 使用 static 修飾外部變量

使用 static 修飾的變量會一直存在內存中,程序結束前不會被釋放。block 捕獲時通過引用方式進行,即傳遞地址。因此,使用 static 修飾的外部變量可以直接修改值。

6.2 使用__block修飾外部變量

使用 static 修飾的變量會一直存放在內存中,直到程序結束,這不利於性能優化。

使用__block修飾外部變量,也可以達到在 block 內修改成員變量的目的,那__block底層是如何實現的呢?

__block不能修飾全局變量、靜態變量。

下面代碼使用__block修飾局部變量:

        __block int age = 10;
        MyBlock myblock = ^{
            age = 20;
            NSLog(@"age: %d", age);
        };
        myblock();

使用以下命令將其轉換爲 C++:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 main.m

轉換後的 block 定義如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // age 被封裝成了對象。
  __Block_byref_age_0 *age; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到使用__block修飾的外部變量被封裝成了__Block_byref_age_0對象類型,__Block_byref_age_0聲明如下:

struct __Block_byref_age_0 {
    // 也有isa指針,即也是對象類型。
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 // 值
 int age;
};

__Block_byref_age_0結構體也有isa指針,即也是對象類型。

使用__block修飾的age被轉換爲:

        __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {
            (void*)0,(__Block_byref_age_0 *)&age,
            0,
            sizeof(__Block_byref_age_0),
            10
        };

__main_block_func_0函數被轉換爲:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_age_0 *age = __cself->age; // bound by ref
    
    // 使用age的forwarding指向age。
    (age->__forwarding->age) = 20;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_05_pj1lwvjs50j3gx6vjtvxcvf80000gn_T_main_d88942_mi_0, (age->__forwarding->age));
}

使用age__forwarding取出變量地址,這樣即使 block 從棧移動到了堆上,也可以正確修改變量值。

7. 對象類型的auto變量、__block變量

在 block 內訪問了使用auto__block修飾的對象類型的變量:

  • 如果 block 在棧上,將不會對變量產生強引用。

  • 如果 block 被拷貝到堆上

    • 會調用 block 內部的copy函數。

    • copy函數內部會調用_Block_object_assign函數。

    • _Block_object_assign函數會根據變量修飾符__strong__weak__unsafe_unretained做出相應操作,類似於 retain(形成強引用、弱引用)。

      使用__block修飾的變量只有在 ARC 環境中會根據__strong__weak__unsafe_unretained修飾符進行強引用,在 MRC 環境中不會進行強引用。

  • 如果 block 從堆上移除:

    • 會調用 block 內部的 dispose 函數。
    • dispose 函數內部會調用_Block_object_dispose函數。
    • _Block_object_dispose函數會自動釋放引用的變量,類似於 release。

8. Block 的循環引用

使用 block 容易產生循環引用。如果類中定義了一個 block,在 block 內又訪問了類的屬性,就會導致循環引用。

Person類中聲明瞭屬性age和 myblock,main.m文件中爲 block 賦值,如下所示:

typedef void(^MyBlock)(void);
@interface Person : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, copy) MyBlock myblock;
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.age = 10;
        person.myblock = ^{
            NSLog(@"age: %d", 20);
        };
    }
    NSLog(@"-------");
    return 0;
}

執行後輸出如下:

-[Person dealloc]
-------

可以看到person先釋放,後打印虛線。

更新myblock賦值語句如下:

        person.myblock = ^{
            NSLog(@"age: %d", person.age);
        };

再次執行後,控制檯只輸出了虛線,person類沒有被釋放。

因爲person強引用了myblock,此時myblock在堆上;myblock內訪問了person對象,堆上的 block 會對對象進行強引用。此時person強引用myblockmyblock強引用person,形成了循環引用。

不止訪問person會產生循環引用,在person類裏的 block 內訪問成員變量也會產生循環引用,因爲訪問成員變量本質上是在調用self->instance,即仍然訪問了self

此外,OC 方法轉換爲 C 語言方法後,默認帶有兩個參數。第一個是 id 類型的self,第二個參數是SEL類型的_cmd,因此,平常訪問的self也是局部變量。

void test(id self, SEL _cmd) {
    
}

ARC 環境下有以下三種解決循環引用的方案:

  • 使用__weak修飾變量。
  • 使用__unsafe_unretained修飾變量。
  • 使用__block修飾變量,同時在 block 內將變量設置爲nil,最後確保調用 block。

下面詳細介紹解決循環引用的方案。

8.1 使用__weak修飾變量

使用__weak修飾變量,更新如下:

        __weak typeof(person) weakPerson = person;
        person.myblock = ^{
            NSLog(@"age: %d", weakPerson.age);
        };

將其轉換爲 C++ 代碼,__main_block_impl_0函數如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // 對捕獲的person進行弱引用。
  Person *__weak weakPerson;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

此時執行後,超出person作用域,person就會釋放。

8.2 使用__unsafe_unretained修飾變量

使用__unsafe_unretained修飾變量也可以解決循環引用問題。

__unsafe_unretained__weak區別在於:

  • __weak:不會產生強引用。指向的對象銷燬時,會自動讓指針置爲nil。
  • __unsafe_unretained:不會產生強引用,但沒有__weak安全。指向對象銷燬時,指針存儲地址不變,但內存已經被回收,再次訪問時產生野指針錯誤。

8.3 使用__block修飾變量,同時在 block 內將變量設置爲nil,最後確保調用 block

使用__block也可以解決循環引用問題:

        // 1.添加__block修飾符
        __block Person *person = [[Person alloc] init];
        person.age = 10;
        person.myblock = ^{
            NSLog(@"age: %d", person.age);
            // 2.置爲nil
            person = nil;
        };
        // 3.調用block()
        person.myblock();

使用__block解決循環引用問題時,上述三步缺一不可。其缺點就是必須調用 block,如果沒有調用 block,就無法在 block 執行完畢後將person置爲nil,就無法解決循環引用問題。

在 MRC 環境中,有以下兩種方案解決循環引用問題:

  • 使用__unsafe_unretained,MRC 不支持弱指針__weak
  • 直接使用__block。在 MRC 環境中,__block結構體不會對結構體內對象進行強引用,不會產生循環引用。

參考資料:

  1. Cocoa blocks as strong pointers vs copy
  2. A look inside blocks: Episode 3 (Block_copy)
  3. How blocks are implemented (and the consequences)
  4. Objective-C Blocks Ins And Outs

歡迎更多指正:https://github.com/pro648/tips

本文地址:https://github.com/pro648/tips/blob/master/sources/Block的本質.md

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