《Objective-C高級編程》三篇總結(jié)之二:Block篇

這本書作者幾乎通篇都在用 C、C++ 語言分析講解 Block 的實現(xiàn),初次看真的很吃力。這里推薦一篇文章:《Objective-C 高級編程》干貨三部曲(二):Blocks篇??偨Y(jié)的非常精簡到位。這篇博客作者有三篇文章分別是:

Objective-C高級編程.jpg

C 語言相關(guān)

Objective-C 轉(zhuǎn) C++ 的方法

Block 是帶有自動變量的匿名函數(shù),實質(zhì)上就是對 C 語言的擴充,最終通過支持 C 語言的編輯器,將目標代碼轉(zhuǎn)化為 C 語言代碼被編譯,可通過下面命令行轉(zhuǎn)化:

clang -rewrite-objc 源代碼文件名

例如:

  1. 創(chuàng)建 OC 源文件 block.m 并編輯代碼。
  2. 打開終端,cd 到 block.m 所在的文件夾。
  3. 輸入 clang -rewrite-objc block.m,就會在當前文件夾生成 block.cpp 文件。

C 語言幾種變量特點

C 語言函數(shù)可能使用的變量:

  • 自動變量(局部變量)
  • 函數(shù)的參數(shù)
  • 靜態(tài)變量(局部靜態(tài)變量)
  • 靜態(tài)全局變量
  • 全局變量

其中,在函數(shù)的多次調(diào)用之間能夠傳遞值的變量有:

  • 靜態(tài)變量(局部靜態(tài)變量)
  • 靜態(tài)全局變量
  • 全局變量

Block 實質(zhì)

先說結(jié)論:Block 即為 Objective-C 的對象。

Block 的構(gòu)成

以下是 block 語法的 BN 范式:

Blokc_literal_expression ::= ^block_decl compound_statement_body

block_decl ::=

block_decl ::= parameter_list

block_decl ::= type_expression

如下所示:

^ 返回值類型 參數(shù)列表 表達式

表層分析 Block 實質(zhì):它是一個類型

Block 是一種類型,一旦使用了 Block 就相當于生成了可以賦值給 Block 類型變量的類型。舉個例子:

int (^blk)(int) = ^(int count) {
    return count + 1;
};
  • 等號左側(cè)代碼定義這個 Block 類型:它接受一個 int 參數(shù),返回一個 int 值。
  • 等號右側(cè)代碼表示這個 Block 的值:它是左側(cè)定義的 Block 的一種實現(xiàn)。

如果我們項目中經(jīng)常使用某個類型的 Block,可以通過 typedef 來抽象出這種類型的 Block,如下:

// 抽象成一種類型
typedef int (^AddOneBlock)(int count);

AddOneBlock block = ^(int count) {
    // 具體實現(xiàn)代碼
    return  count + 1;
};

這樣一來,Block 的賦值和傳遞就和普通類型一樣方便了。

深層分析 Block 的實質(zhì):它是 Objective-C 對象

Block 其實就是 Objective-C 對象。它的結(jié)構(gòu)體內(nèi)含有 ISA 指針。

下面將 Objective-C 的代碼轉(zhuǎn)化為 C++ 來看下實現(xiàn),這里我把方法命名為 test:

#include <stdio.h>
int test()
{
    void (^blk)(void) = ^{
        printf("Block\n");
    };
    blk();
    return 0;
}

C++ 核心代碼,這里并不是全部代碼,抽取了關(guān)鍵部分來說:

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

// block 結(jié)構(gòu)體
struct __test_block_impl_0 {
  struct __block_impl impl;
  struct __test_block_desc_0* Desc;
  __test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// Block 被調(diào)用的代碼,也是Block被轉(zhuǎn)化為 C 語言的代碼
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
    printf("Block\n");
}

// 方法的描述
static struct __test_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0)};

// 自己定義的 test 方法
int test()
{
    void (*blk)(void) = ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

先來看下 OC 中源代碼 block 語法 ^{printf("Block\n")}; 變化成 C++ 后:

static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
    printf("Block\n");
}

該函數(shù)的參數(shù) __cself 相當于 C++ 實例方法中指向自身實例方法的 this,或是 OC 實例方法中指向?qū)ο笞陨淼淖兞?self,即參數(shù) __cself 為指向 Block 值的變量,也是 __test_block_impl_0 結(jié)構(gòu)體的指針。

結(jié)合這個方法,可知 __test_block_impl_0 即為這個 Block 的結(jié)構(gòu)體。也就是說 blk 這個 Block就是通過 __test_block_impl_0 構(gòu)造出來的。

下面看下這個方法:

// block 結(jié)構(gòu)體
struct __test_block_impl_0 {
  struct __block_impl impl;
  struct __test_block_desc_0* Desc;
  // 構(gòu)造函數(shù)
  __test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到,該結(jié)構(gòu)體由三部分組成:

第一個成員變量是 impl,其 __block_impl 結(jié)構(gòu)體聲明為:

struct __block_impl {
  void *isa;            // 指針
  int Flags;            // 標志
  int Reserved;         // 今后版本升級所需要的區(qū)域
  void *FuncPtr;        // 函數(shù)指針
};

第二個成員變量是 Desc 指針,用來描述這個 block 的附加信息,__test_block_desc_0 結(jié)構(gòu)體如下:

// 方法的描述
static struct __test_block_desc_0 {
  size_t reserved;          // 今后版本升級所需要的區(qū)域
  size_t Block_size;        // block 大小
} __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0)};

第三部分是 __test_block_impl_0 結(jié)構(gòu)體的構(gòu)造函數(shù):

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

在這個結(jié)構(gòu)體的構(gòu)造函數(shù)里,ISA 指針保持著所屬類的結(jié)構(gòu)體的實例指針。__test_block_impl_0 結(jié)構(gòu)體相當于 Objective-C 類對象的結(jié)構(gòu)體,這里 _NSConcreteStackBlock 相當于 block 結(jié)構(gòu)體的實例。所以說 Block 的實質(zhì)即為 Objective-C 的對象。

Objective-C 類和對象的實質(zhì)

id 這一變量用于存儲 Objective-C 對象。源碼中面對 id 類型的聲明為:

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

id 為 objc_object 結(jié)構(gòu)體的指針類型。我們再看下 Class:

/// usr/include/objc/objc.h
typedef struct objc_class *Class;
/// usr/include/objc/runtime.h
struct objc_class {
    Class isa;
};

各類的結(jié)構(gòu)體就是基于 objc_class 的結(jié)構(gòu)體的 class_t 結(jié)構(gòu)體。class_t 在 objc4 運行時庫的 runtime/objc-runtime-new.h 中聲明如下:

typedef struct class_t {
    struct class_t *isa;
    struct class_t *superclass;
    Cache cache;
    IMP *vtable;
    class_rw_t *data;
} class_t;

這里要說一下,objc4-437 還有這段代碼,但是最新的 objc4-756.2 中已經(jīng)沒有了。查看一個 NSObject 對象的 C++ 源碼,依然可以找到:

struct _class_t {
    struct _class_t *isa;
    struct _class_t *superclass;
    void *cache;
    void *vtable;
    struct _class_ro_t *ro;
};

借用書中的一張圖,表達如下:

oc類與對象的實質(zhì).png

Block 截獲自動變量和對象

Block 截獲自動變量(局部變量)

使用 Block 時,不僅可以使用其內(nèi)部參數(shù),還能使用 Block 外部的局部或者全局變量。

當 Block 使用外部的局部變量時,這些變量就會被 Block 保存,即使在 Block 外部修改這些值,存在于 Block 內(nèi)部的這些變量也不會被修改,如下:

int a = 10;
int b = 20;
void (^printNumbers)(void) = ^{
    printf("a = %d\nb = %d\n", a, b);
};
printNumbers();
// a = 10  b = 20
a = 55;
b = 66;
printNumbers();
// a = 10  b = 20

如果想要在 Block 內(nèi)部修改外部局部變量值,會編譯錯誤,提示添加 __block。如果操作外部局部變量,則不會有這問題。如:

NSMutableArray *array = [[NSMutableArray alloc] init];
void (^printNumbers)(void) = ^{
    [array addObject:@1];
    NSLog(@"array1 = %@\n", array);
};
printNumbers();
NSLog(@"array2 = %@\n", array);
[array addObject:@2];
printNumbers();

// 輸出:
array1 = [1]
array2 = [1]
arrar1 = [1, 2, 1]

如果 Block 內(nèi)部執(zhí)行 array = [NSMutableArray array]; 則會編譯錯誤,提示加上 __block。

根據(jù)以上例子,可以暫時總結(jié)三點:

  • Block 可以截獲局部變量。
  • 修改 Block 外部的局部變量,Block 內(nèi)部被截獲的值不會改變。
  • 直接修改 Block 內(nèi)部的值,編譯錯誤。但是可以操作局部變量。

下面通過 C++ 代碼分析下為什么會出現(xiàn)這種現(xiàn)象, C 代碼:

#include <stdio.h>

int main() {
    int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    void (^blk)(void) = ^ {
        printf(fmt, val);
    };
    val = 2;
    fmt = "Value changed. Val = %d\n";
    blk();      // 輸出: val = 10
    return 0;
}

通過 Clang 將其轉(zhuǎn)化為 C++ 代碼,主要代碼如下:

// block 結(jié)構(gòu)體
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *fmt;
  int val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

/// block 函數(shù)體
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt; // bound by copy
  int val = __cself->val; // bound by copy
    printf(fmt, val);
}

/// block 描述
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 方法
int main() {
    int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
    val = 2;
    fmt = "Value changed. Val = %d\n";
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

這里加一點個人看法,前面說過,Block 是帶有自動變量的匿名函數(shù),我個人認為可以從上面 C++ 代碼來做驗證,Block 被轉(zhuǎn)化為 static void __main_block_func_0,即是一個函數(shù)。

單獨看 block 的構(gòu)成結(jié)構(gòu)體 __main_block_impl_0

  • 可以看到,在 block 內(nèi)部使用的局部變量 val、fmt 作為成員變量被追加到 __main_block_impl_0 結(jié)構(gòu)體中。而 block 中沒有使用的局部變量不會追加,如 dmy。
  • 初始化 block 結(jié)構(gòu)體實例時,即 void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));,截獲了局部變量 val 和 fmt 來初始化 block 的結(jié)構(gòu)體實例。

再看一下 block 函數(shù)體代碼:

/// block 函數(shù)體
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt; // bound by copy
  int val = __cself->val; // bound by copy
    printf(fmt, val);
}

可知, val 、fmt 都是從 cself 截獲的,而 cself 是指向 block 的對象。所以也能說,val 和 fmt 是屬于 block 的。而且從 Clang 生成的注釋 bound by copy ,表示對這兩個對象做了只讀值拷貝,而不是指針傳遞。所以即使改變了 block 外部局部變量的值,block 內(nèi)部拷貝的值,也不會發(fā)生改變。

不過可以使用 __block 修飾局部變量,來滿足 block 內(nèi)部修改的需求。后面會對 __block 做詳細分析。

改變存儲于特殊存儲區(qū)域的變量

在 Block 內(nèi)部,下列幾種值是可以直接訪問和修改的:

  • 全局變量
  • 靜態(tài)全局變量
  • 靜態(tài)變量

下面通過源碼來與自動變量對比來分析下:

#include <stdio.h>
int global_val = 1;     // 全局變量
static int static_global_val = 2;       // 全局靜態(tài)變量

int main() {
    // 局部靜態(tài)變量
    static int static_val = 3;
    void (^blk)(void) = ^ {
        printf("global_val = %d\nglobal_val = %d\nstatic_val = %d\n", global_val, static_global_val, static_val);
        global_val = 11;
        static_global_val = 12;
        static_val = 13;
        
    };
    global_val = 21;
    static_global_val = 22;
    static_val = 23;
    blk();
    printf("global_val2 = %d\nglobal_val2 = %d\nstatic_val2 = %d\n", global_val, static_global_val, static_val);
    return 0;
}

調(diào)用該方法,打印如下:

global_val = 21
global_val = 22
static_val = 23
global_val2 = 11
global_val2 = 12
static_val2 = 13

可知:

  • 這三種類型的值,可以在 Block 內(nèi)部修改,并且內(nèi)外修改相互影響。

轉(zhuǎn)化成 C++ 代碼:

/// 定義變量
int global_val = 1;
static int static_global_val = 2;

/// block 結(jié)構(gòu)體
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *static_val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_vtypedefal(_static_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

/// block  函數(shù)體
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *static_val = __cself->static_val; // bound by copy

        printf("global_val = %d\nglobal_val = %d\nstatic_val = %d\n", global_val, static_global_val, (*static_val));
        global_val = 11;
        static_global_val = 12;
        (*static_val) = 13;

    }

// block 描述
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() {
    static int static_val = 3;
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));
    global_val = 21;
    static_global_val = 22;
    static_val = 23;
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    printf("global_val2 = %d\nglobal_val2 = %d\nstatic_val2 = %d\n", global_val, static_global_val, static_val);
    return 0;
}

通過 __main_block_impl_0 結(jié)構(gòu)體可以看到,全局變量、靜態(tài)全局變量并沒被截獲到 block 內(nèi)部,而局部靜態(tài)變量 static_val 也是通過指針訪問。所以他們在 block 內(nèi)外的改變都是相互影響的,用的是同一份值。

Block 截獲對象

下面看一下 Block 語法中使用 Array 對象的代碼,Array已經(jīng)超過其作用域:

void testBlock() {
    typedef void(^blk_t)(id obj);
    blk_t blk;
    {
        id array = [[NSMutableArray alloc] init];
        blk = [^(id obj) {
            [array addObject:obj];
            NSLog(@"array count = %ld", [array count]);
        } copy];
    }
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
}

調(diào)用 testBlock() 方法,控制臺打?。?/p>

ViewController.m:31 array count = 1
ViewController.m:31 array count = 2
ViewController.m:31 array count = 3

array 在超出其作用域后,并沒有被徹底銷毀,看一下其 C++ 代碼實現(xiàn):

struct __testBlock_block_impl_0 {
  struct __block_impl impl;
  struct __testBlock_block_desc_0* Desc;
  id array;     // 截獲的對象
  __testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

在 OC 中, C 語言的結(jié)構(gòu)體不能含有 __strong 修飾符的變量,因為編譯器不知道應(yīng)該何時進行 C 語言結(jié)構(gòu)體的初始化和廢棄操作,不能很好地管理內(nèi)存。

但是 OC 的運行時庫能夠準確的把握 Block 從棧復制到堆以及堆上的 Block 函數(shù)被廢棄的時機。在實現(xiàn)上是通過 __testBlock_block_copy_0__testBlock_block_dispose_0 兩個函數(shù)進行的:

static void __testBlock_block_copy_0(struct __testBlock_block_impl_0*dst, struct __testBlock_block_impl_0*src) {
    _Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __testBlock_block_dispose_0(struct __testBlock_block_impl_0*src) {
    _Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

其中,_Block_object_assign 函數(shù)相當于 retain 實例方法的函數(shù),將對象賦值在對象類型的結(jié)構(gòu)體成員變量中。 _Block_object_dispose 函數(shù)相當于調(diào)用 release 實例方法的函數(shù),釋放賦值在對象類型的結(jié)構(gòu)體成員變量中的對象。

這兩個函數(shù)在源碼中并沒有被調(diào)用,而是在 Block 從棧復制到堆上時以及堆上的 block 被廢棄時會調(diào)用這些函數(shù),如下表:調(diào)用 copy 函數(shù) 和 dispose 函數(shù)的時機

函數(shù) 調(diào)用時機
copy 函數(shù) 棧上的 Block 復制到堆時
dispose 函數(shù) 堆上的 Block 被廢棄時

什么時候棧上的 Block 會復制到堆上呢?

  • 調(diào)用 Block 的 copy 實例方法時
  • Block 作為函數(shù)的返回值時
  • 將 Block 賦值給附有 __strong 的修飾符 id 類型的類,或 Block 類型成員變量時
  • 在方法名中含有 usingBlock 的 Cocoa 框架方法或者 GCD 的 API 中傳遞 Block 時

上面四種情況,棧上的 Block 都會復制到堆上,但其實本質(zhì)都可以歸結(jié)為 block_copy 函數(shù)被調(diào)用時 Block 從棧復制到堆。

特殊說明:針對如下修改,書中說程序會強制結(jié)束:

void testBlock() {
    typedef void(^blk_t)(id obj);
    blk_t blk;
    {
        id array = [[NSMutableArray alloc] init];
        blk = ^(id obj) {
            [array addObject:obj];
            NSLog(@"array count = %ld", [array count]);
        };
    }
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
}

但是我測試時,調(diào)用 testBlock 函數(shù)依然輸出:

ViewController.m:34 array count = 1
ViewController.m:34 array count = 2
ViewController.m:34 array count = 3

不確定是 Apple 之后做了修改,還是我不應(yīng)該借用 OC 的機制這么寫。

另外, __testBlock_block_copy_0 函數(shù)是否存在,與是否調(diào)用了 copy 實例方法并沒有關(guān)系。

相對的,在釋放復制到堆上的 Block 后,誰都不持有 Block 而被廢棄時調(diào)用 dispose 函數(shù),相當于對象的 dealloc 方法。比如在使用了 __weak 關(guān)鍵字時:

void testBlock() {
    typedef void(^blk_t)(id obj);
    blk_t blk;
    {
        id array0 = [[NSMutableArray alloc] init];
        id __weak array = array0;
        blk = ^(id obj) {
            [array addObject:obj];
            NSLog(@"array count = %ld", [array count]);
        };
    }
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
}

輸出:

ViewController.m:34 array count = 0
ViewController.m:34 array count = 0
ViewController.m:34 array count = 0

這段代碼無法通過 Clang 轉(zhuǎn)化,會報錯:

 cannot create __weak reference in file using manual reference counting

__block 的實現(xiàn)原理

__block 修飾局部變量

__block 說明符 更精確的表達方式應(yīng)該是 __block 存儲域類說明符(__block storage-class-specifier)。C 語言有如下存儲域類說明符:

  • typedef
  • extern
  • static
  • auto
  • register

__block 說明符類似 static、auto和 register 說明符,用于指定將變量值放到哪個存儲域中。例如,auto 表示作為自動變量存儲在棧中,static 表示作為靜態(tài)變量存儲在數(shù)據(jù)區(qū)中。給自動變量加上 __block 說明符后,就會改變這個自動變量的存儲區(qū)域。

下面通過給局部變量添加 __block 關(guān)鍵字后效果來分析:

typedef void(^PrintNumbers)(void);
void lineBlock() {
    __block int a = 10;
    int b = 20;
    PrintNumbers printBlock = ^() {
        a -= 10;
        printf("a1 = %d, b1 = %d\n", a, b);
    };
    printBlock();
    a += 20;
    b += 20;
    printf("a2 = %d, b2 = %d\n", a, b);
    printBlock();
}

輸出:

a1 = 0, b1 = 20
a2 = 20, b2 = 40
a1 = 10, b1 = 20

附有 __block 修飾符的變量稱之為 __block 變量,可以看到,__block 變量在 Block 內(nèi)部可以被修改,且修改的值被同步到 Block 作用域外。

下面用 Clang 工具看一下 C++ 代碼:
OC 代碼:

void lineBlock() {
    __block int valNumber= 10;
    void (^blk)(void) = ^{
        valNumber = 1;
    };
}

C++ 代碼:

struct __Block_byref_valNumber_0 {
  void *__isa;
__Block_byref_valNumber_0 *__forwarding;
 int __flags;
 int __size;
 int valNumber;
};

struct __lineBlock_block_impl_0 {
  struct __block_impl impl;
  struct __lineBlock_block_desc_0* Desc;
  __Block_byref_valNumber_0 *valNumber; // by ref
  __lineBlock_block_impl_0(void *fp, struct __lineBlock_block_desc_0 *desc, __Block_byref_valNumber_0 *_valNumber, int flags=0) : valNumber(_valNumber->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __lineBlock_block_func_0(struct __lineBlock_block_impl_0 *__cself) {
  __Block_byref_valNumber_0 *valNumber = __cself->valNumber; // bound by ref

        (valNumber->__forwarding->valNumber) = 1;
    }
static void __lineBlock_block_copy_0(struct __lineBlock_block_impl_0*dst, struct __lineBlock_block_impl_0*src) {_Block_object_assign((void*)&dst->valNumber, (void*)src->valNumber, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __lineBlock_block_dispose_0(struct __lineBlock_block_impl_0*src) {_Block_object_dispose((void*)src->valNumber, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __lineBlock_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __lineBlock_block_impl_0*, struct __lineBlock_block_impl_0*);
  void (*dispose)(struct __lineBlock_block_impl_0*);
} __lineBlock_block_desc_0_DATA = { 0, sizeof(struct __lineBlock_block_impl_0), __lineBlock_block_copy_0, __lineBlock_block_dispose_0};
void lineBlock() {
    __attribute__((__blocks__(byref))) __Block_byref_valNumber_0 valNumber = {(void*)0,(__Block_byref_valNumber_0 *)&valNumber, 0, sizeof(__Block_byref_valNumber_0), 10};
    void (*blk)(void) = ((void (*)())&__lineBlock_block_impl_0((void *)__lineBlock_block_func_0, &__lineBlock_block_desc_0_DATA, (__Block_byref_valNumber_0 *)&valNumber, 570425344));
}

可以看到:

  • 被 __block 修飾的自動變量,被轉(zhuǎn)變?yōu)?__Block_byref_valNumber_0 結(jié)構(gòu)體存在,即棧上生成 __Block_byref_valNumber_0 結(jié)構(gòu)體實例。
  • block 的構(gòu)造結(jié)構(gòu)體 __lineBlock_block_impl_0 中可以看到,通過 __block 變量的結(jié)構(gòu)體指針訪問該變量。
  • block 函數(shù)實現(xiàn)方法 __lineBlock_block_func_0 中看到,block 內(nèi)部通過 valNumber->__forwarding->valNumbe 指針修改 __block 變量。

關(guān)于 __block 變量的結(jié)構(gòu)體實現(xiàn):

struct __Block_byref_valNumber_0 {
  void *__isa;
__Block_byref_valNumber_0 *__forwarding;
 int __flags;
 int __size;
 int valNumber;
};
  • 結(jié)構(gòu)體的成員變量 valNumber 持有原本變量的字面值。
  • __forwarding: 結(jié)構(gòu)體實例的成員變量 __forwarding 持有指向該實例自身的指針,可以通過成員變量 __forwarding 訪問成員變量 valNumber,即 valNumber->__forwarding->valNumbe。如下圖:
訪問__block變量.png

__Block_byref_valNumber_0 結(jié)構(gòu)體之所以不在 __lineBlock_block_impl_0 結(jié)構(gòu)體內(nèi)部定義,是為了在多個 block 中共用一個 __block 變量結(jié)構(gòu)體。

另外,當 Block 從棧復制到堆時,被 Block 持有的 __block 變量也會復制到堆上,注意,這里會增加 __block 變量的引用計數(shù)。同時,如果 Block 被廢棄,持有的 __block 變量也會被釋放。

不管 __block 變量配置在棧上還是堆上,都能夠正確的訪問該變量。通過下面這個圖可以更深刻的理解這句話:

復制__block變量.png

__block 修飾對象

__block 說明符可以用來指定任何類型的自動變量,下面用來指定 id 類型的自動變量:

void testBlock() {
    __block id obj = [[NSObject alloc] init];
    void (^blk)(void) = ^{
        obj = [NSObject new];
    };
}

ARC 有效時,id 類型以及對象類型變量必定附加所有權(quán)修飾符,缺省為附有 __strong 修飾符的變量。改代碼通過 clang 轉(zhuǎn)換。 __block 變量的結(jié)構(gòu)體如下:

struct __Block_byref_obj_0 {
  void *__isa;
__Block_byref_obj_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 id obj;
};

被 __strong 修飾的 id 類型或?qū)ο箢愋?,生成?copy 和 dispose 兩個方法:

static void __testBlock_block_copy_0(struct __testBlock_block_impl_0*dst, struct __testBlock_block_impl_0*src) {_Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __testBlock_block_dispose_0(struct __testBlock_block_impl_0*src) {_Block_object_dispose((void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);}
  • 如果 __block 對象變量從棧復制到堆,使用 _Block_object_assign 方法。
  • 當堆上的 __block 對象變量被廢棄時,使用 _Block_object_dispose 方法。

另外,__weak 與 __block 同時使用,clang 時報錯 cannot create __weak reference because the current deployment target does not support weak references

說明不支持 __weak。

Block 存儲域,即三種 Block

通過前面說明可知, Block 轉(zhuǎn)換為 Block 結(jié)構(gòu)體類型的自動變量, __block 變量轉(zhuǎn)換為 __block 變量的結(jié)構(gòu)體類型的自動變量。而所謂 結(jié)構(gòu)體類型的自動變量,即棧上生成的該結(jié)構(gòu)體的實例。

Block__block 變量的實質(zhì):

名稱 實質(zhì)
Block 棧上 Block 的結(jié)構(gòu)體實例
__block變量 棧上 __block 變量的結(jié)構(gòu)體實例

而 Block 根據(jù)存儲域的不同,可以歸結(jié)為三類:

Block 的類 存儲域 拷貝效果
_NSConcreteStackBlock 從??截惖蕉?/td>
_NSConcreteGlobalBlock 程序的數(shù)據(jù)區(qū)域(.data) 什么也不做
_NSConcreteMallocBlock 引用計數(shù)增加

全局 Block: _NSConcreteGlobalBlock

因為全局 Block 的結(jié)構(gòu)體實例設(shè)置在程序的數(shù)據(jù)存儲區(qū),可以在程序任意位置通知指針來訪問,只有兩種情況下會產(chǎn)生全局 Block:

  • 記述全局變量的地方有 Block 語法時。
  • Block 語法的表達式中不截獲自動變量時。

關(guān)于情況一,如下:

#include <stdio.h>

void(^blk_t)(void) = ^{
    printf("Global Block.\n");
};
void test() {
    blk_t();
}

通過 Clang 分析代碼,獲取 Block 的結(jié)構(gòu)體實例如下,可知確實是全局 Block:

struct __blk_t_block_impl_0 {
  struct __block_impl impl;
  struct __blk_t_block_desc_0* Desc;
  __blk_t_block_impl_0(void *fp, struct __blk_t_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

特別提示: 第二種情況我按照書中測試,即:

#include <stdio.h>
typedef int (^BlockNum) (int);
void test() {
    for(int rate = 0; rate < 5; ++rate) {
        BlockNum num = ^(int count) {
            return count;
        };
    }
}

該 Block 的結(jié)構(gòu)體如下

typedef int (*BlockNum) (int);

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

依然被認為存儲在棧上,這里暫時不確定原因。

棧 Block:_NSConcreteStackBlock

在生成 Block 后,如果這個 Block 不是全局 Block,那么它就是棧 Block,即 _NSConcreteStackBlock 對象。

配置在全局變量上的 Block,從變量作用域外也可通過指針安全的使用。但設(shè)置在棧上的 Block,如果其所屬變量作用域結(jié)束,該 Block 就被廢棄。由于 __block 變量也配置在棧上,同樣,其所屬的變量作用域結(jié)束,則該 __block 變量也被廢棄。

Blocks 機制提供了將 Block 和 __block 變量從棧上復制到堆上的方法來解決這個問題。將配置在棧上的 Block 復制到堆上,這樣即使 Block 語法記述的變量作用域結(jié)束,堆上的 Block 還可以繼續(xù)存在。

堆 Block:_NSConcreteMallocBlock

將棧 Block 復制到堆后,block結(jié)構(gòu)體的 ISA 成員變量變成了 _NSConcreteMallocBlock。

在對 Block 使用 copy 實例方法時:

Block 的類 副本源的配置存儲域 復制(copy)效果
_NSConcreteStackBlock 從棧拷貝到堆
_NSConcreteGlobalBlock 程序的數(shù)據(jù)區(qū)域 什么也不做
_NSConcreteMallocBlock 引用計數(shù)增加

不管 Block 配置之在何處,用 copy 方法都不會引起任何問題,在不確定時調(diào)用 copy 方法即可。

在 ARC 中不能顯示調(diào)用 release,多次調(diào)用 copy 方法依然沒有問題,如下:

typedef int (^BlockNum) (int);
void test(int rate) {
    BlockNum num = ^(int count) { return count * rate;};
    num = [[[[num copy] copy] copy] copy];
}

該源代碼可以解釋為:

{
    BlockNum tmp = [num copy];
    num = tmp;
}
{
    BlockNum tmp = [num copy];
    num = tmp;
}
{
    BlockNum tmp = [num copy];
    num = tmp;
}
{
    BlockNum tmp = [num copy];
    num = tmp;
}

內(nèi)存管理并不會存在問題。

大多數(shù)情況下,編譯器會自行判斷將 Block 從棧上復制到堆上:

  • block 作為函數(shù)返回值的時候。
  • 部分情況下向方法或函數(shù)中傳遞 Block 時。部分情況主要是下面兩種情況:
    • Cocoa 框架的方法且方法名中含有 usingBlock 時。
    • GCD 中的 API。

除了以上兩種情況,我們基本是都要手動 copy Block,才能將其從棧復制到堆。

備注,前文也提到過 Block 從棧復制到堆的情況,那里多了個將 Block 賦值給 __strong 類型的對象,這里卻沒有說,暫時沒有找到驗證的方式。

那么 __block 變量在 Block 執(zhí)行 copy 操作后會發(fā)生什么呢?

  • 任何一個 Block 被復制到堆上時,使用的 __block 變量也會復制到堆上,并被該 Block 持有。
  • 如果接著有其他 Block 復制到堆上,被復制的 Block 也持有 __block 變量,則會增加 __block 變量的引用計數(shù)。反過來如果堆上 Block 被廢棄,它所持有的 __block 變量也會被釋放。

Block 循環(huán)引用

如果在 Block 內(nèi)部使用附有 __strong 修飾符的對象類型自動變量,那么當 Block 從棧復制到堆時,該對象為 Block 所持有。這樣容易引起循環(huán)引用。如下:

typedef void(^blk_t)(void);

@interface Person : NSObject
{
    blk_t blk_;
}

@implementation Person

- (instancetype)init
{
    self = [super init];
    blk_ = ^{
        NSLog(@"self = %@",self);
    };
    return self;
}

@end

Block blk_t持有self,而self也同時持有作為成員變量的blk_t,導致循環(huán)引用,MyObject 的對象實例會無法釋放,造成內(nèi)存泄漏。

另外,編譯器有時候能夠及時的查處循環(huán)引用,進行編譯警告 Capturing 'self' strongly in this block is likely to lead to a retain cycle

__weak 修飾符

為避免循環(huán)引用,可聲明附有 __weak 修飾符的變量,并將 self 賦值使用:

- (instancetype)init
{
    self = [super init];
    if (self) {
        id __weak tmp = self;
        blk_ = ^ {
            NSLog(@"self = %@", tmp);
        };
    }
    return self;
}

在低于 iOS4 時,使用 __unsafe_unretained 修飾符代替 __weak。

另外,以下源代碼中 Block 中沒有 self 但是同樣截獲了 self,引起循環(huán)引用:

typedef void(^blk_t) (void);

@interface MyObject : NSObject
{
    blk_t blk_;
    id obj_;
}

@end

@implementation MyObject

- (instancetype)init
{
    self = [super init];
    if (self) {
        blk_ = ^ {
            NSLog(@"obj_ = %@", obj_);
        };
    }
    return self;
}

@end

Block 語法內(nèi)部使用的 obj_ 實際上截獲了 self。對編譯器來說,obj_ 只是對象用結(jié)構(gòu)體的成員變量,即:

blk_ = ^ {
            NSLog(@"obj_ = %@", self->obj_);
        };

這里說個容易出錯的情況,如果某個屬性用 weak 關(guān)鍵字呢?

@interface Person()
@property (nonatomic, weak) NSArray *array;
@end

@implementation Person

- (instancetype)init
{
    self = [super init];
    blk_ = ^{
        NSLog(@"array = %@",_array);//循環(huán)引用警告
    };
    return self;
}

這里依然會有循環(huán)引用的警告,因為循環(huán)引用是 self 和 block 之間的事,和被這個 Block 持有的成員變量時strong、weak都沒有關(guān)系,即使是基本數(shù)據(jù)類型 assgin 一樣會有警告。

__block 修飾符

- (instancetype)init
{
    self = [super init];
    __block id temp = self;//temp持有self
    
    //self持有blk_
    blk_ = ^{
        NSLog(@"self = %@",temp);//blk_持有temp
        temp = nil;
    };
    return self;
}

- (void)execBlc
{
    blk_();
}

如果不調(diào)用 execBlc 實例方法,即不給成員變量 blk_ 賦值,便會循環(huán)引用并造成內(nèi)存泄漏。

使用 __block 避免循環(huán)引用的優(yōu)點:

  • 通過 __block 變量可控制對象的持有時間。
  • 為避免循環(huán)引用必須執(zhí)行 Block,否則循環(huán)引用一直存在。

說一下,使用 __block 來避免循環(huán)引用,感覺并不方便。不過也可以更具實際情況去具體選擇。

參考文檔

2018年 iOS 面試心得

后記

《Objective-C高級編程》三篇總結(jié)之一:引用計數(shù)篇

《Objective-C高級編程》三篇總結(jié)之二:Block篇

《Objective-C高級編程》三篇總結(jié)之三:GCD篇

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容