《Objective-C高级编程》三篇总结之二:Block篇

这本书作者几乎通篇都在用 C、C++ 语言分析讲解 Block 的实现,初次看真的很吃力。这里推荐一篇文章:《Objective-C 高级编程》干货三部曲(二):Blocks篇。总结的非常精简到位。这篇博客作者有三篇文章分别是:

Objective-C高级编程.jpg

C 语言相关

Objective-C 转 C++ 的方法

Block 是带有自动变量的匿名函数,实质上就是对 C 语言的扩充,最终通过支持 C 语言的编辑器,将目标代码转化为 C 语言代码被编译,可通过下面命令行转化:

clang -rewrite-objc 源代码文件名

例如:

  1. 创建 OC 源文件 block.m 并编辑代码。
  2. 打开终端,cd 到 block.m 所在的文件夹。
  3. 输入 clang -rewrite-objc block.m,就会在当前文件夹生成 block.cpp 文件。

C 语言几种变量特点

C 语言函数可能使用的变量:

  • 自动变量(局部变量)
  • 函数的参数
  • 静态变量(局部静态变量)
  • 静态全局变量
  • 全局变量

其中,在函数的多次调用之间能够传递值的变量有:

  • 静态变量(局部静态变量)
  • 静态全局变量
  • 全局变量

Block 实质

先说结论:Block 即为 Objective-C 的对象。

Block 的构成

以下是 block 语法的 BN 范式:

Blokc_literal_expression ::= ^block_decl compound_statement_body

block_decl ::=

block_decl ::= parameter_list

block_decl ::= type_expression

如下所示:

^ 返回值类型 参数列表 表达式

表层分析 Block 实质:它是一个类型

Block 是一种类型,一旦使用了 Block 就相当于生成了可以赋值给 Block 类型变量的类型。举个例子:

int (^blk)(int) = ^(int count) {
    return count + 1;
};
  • 等号左侧代码定义这个 Block 类型:它接受一个 int 参数,返回一个 int 值。
  • 等号右侧代码表示这个 Block 的值:它是左侧定义的 Block 的一种实现。

如果我们项目中经常使用某个类型的 Block,可以通过 typedef 来抽象出这种类型的 Block,如下:

// 抽象成一种类型
typedef int (^AddOneBlock)(int count);

AddOneBlock block = ^(int count) {
    // 具体实现代码
    return  count + 1;
};

这样一来,Block 的赋值和传递就和普通类型一样方便了。

深层分析 Block 的实质:它是 Objective-C 对象

Block 其实就是 Objective-C 对象。它的结构体内含有 ISA 指针。

下面将 Objective-C 的代码转化为 C++ 来看下实现,这里我把方法命名为 test:

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

C++ 核心代码,这里并不是全部代码,抽取了关键部分来说:

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

// block 结构体
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 被调用的代码,也是Block被转化为 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");
}

该函数的参数 __cself 相当于 C++ 实例方法中指向自身实例方法的 this,或是 OC 实例方法中指向对象自身的变量 self,即参数 __cself 为指向 Block 值的变量,也是 __test_block_impl_0 结构体的指针。

结合这个方法,可知 __test_block_impl_0 即为这个 Block 的结构体。也就是说 blk 这个 Block就是通过 __test_block_impl_0 构造出来的。

下面看下这个方法:

// block 结构体
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;
  }
};

可以看到,该结构体由三部分组成:

第一个成员变量是 impl,其 __block_impl 结构体声明为:

struct __block_impl {
  void *isa;            // 指针
  int Flags;            // 标志
  int Reserved;         // 今后版本升级所需要的区域
  void *FuncPtr;        // 函数指针
};

第二个成员变量是 Desc 指针,用来描述这个 block 的附加信息,__test_block_desc_0 结构体如下:

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

第三部分是 __test_block_impl_0 结构体的构造函数:

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

在这个结构体的构造函数里,ISA 指针保持着所属类的结构体的实例指针。__test_block_impl_0 结构体相当于 Objective-C 类对象的结构体,这里 _NSConcreteStackBlock 相当于 block 结构体的实例。所以说 Block 的实质即为 Objective-C 的对象。

Objective-C 类和对象的实质

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 结构体的指针类型。我们再看下 Class:

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

各类的结构体就是基于 objc_class 的结构体的 class_t 结构体。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 中已经没有了。查看一个 NSObject 对象的 C++ 源码,依然可以找到:

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

借用书中的一张图,表达如下:

oc类与对象的实质.png

Block 截获自动变量和对象

Block 截获自动变量(局部变量)

使用 Block 时,不仅可以使用其内部参数,还能使用 Block 外部的局部或者全局变量。

当 Block 使用外部的局部变量时,这些变量就会被 Block 保存,即使在 Block 外部修改这些值,存在于 Block 内部的这些变量也不会被修改,如下:

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 内部修改外部局部变量值,会编译错误,提示添加 __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 内部执行 array = [NSMutableArray array]; 则会编译错误,提示加上 __block。

根据以上例子,可以暂时总结三点:

  • Block 可以截获局部变量。
  • 修改 Block 外部的局部变量,Block 内部被截获的值不会改变。
  • 直接修改 Block 内部的值,编译错误。但是可以操作局部变量。

下面通过 C++ 代码分析下为什么会出现这种现象, 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 将其转化为 C++ 代码,主要代码如下:

// block 结构体
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 函数体
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 是带有自动变量的匿名函数,我个人认为可以从上面 C++ 代码来做验证,Block 被转化为 static void __main_block_func_0,即是一个函数。

单独看 block 的构成结构体 __main_block_impl_0

  • 可以看到,在 block 内部使用的局部变量 val、fmt 作为成员变量被追加到 __main_block_impl_0 结构体中。而 block 中没有使用的局部变量不会追加,如 dmy。
  • 初始化 block 结构体实例时,即 void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));,截获了局部变量 val 和 fmt 来初始化 block 的结构体实例。

再看一下 block 函数体代码:

/// block 函数体
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 内部拷贝的值,也不会发生改变。

不过可以使用 __block 修饰局部变量,来满足 block 内部修改的需求。后面会对 __block 做详细分析。

改变存储于特殊存储区域的变量

在 Block 内部,下列几种值是可以直接访问和修改的:

  • 全局变量
  • 静态全局变量
  • 静态变量

下面通过源码来与自动变量对比来分析下:

#include <stdio.h>
int global_val = 1;     // 全局变量
static int static_global_val = 2;       // 全局静态变量

int main() {
    // 局部静态变量
    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;
}

调用该方法,打印如下:

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

可知:

  • 这三种类型的值,可以在 Block 内部修改,并且内外修改相互影响。

转化成 C++ 代码:

/// 定义变量
int global_val = 1;
static int static_global_val = 2;

/// block 结构体
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  函数体
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 结构体可以看到,全局变量、静态全局变量并没被截获到 block 内部,而局部静态变量 static_val 也是通过指针访问。所以他们在 block 内外的改变都是相互影响的,用的是同一份值。

Block 截获对象

下面看一下 Block 语法中使用 Array 对象的代码,Array已经超过其作用域:

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

调用 testBlock() 方法,控制台打印:

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

array 在超出其作用域后,并没有被彻底销毁,看一下其 C++ 代码实现:

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 语言的结构体不能含有 __strong 修饰符的变量,因为编译器不知道应该何时进行 C 语言结构体的初始化和废弃操作,不能很好地管理内存。

但是 OC 的运行时库能够准确的把握 Block 从栈复制到堆以及堆上的 Block 函数被废弃的时机。在实现上是通过 __testBlock_block_copy_0__testBlock_block_dispose_0 两个函数进行的:

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 函数相当于 retain 实例方法的函数,将对象赋值在对象类型的结构体成员变量中。 _Block_object_dispose 函数相当于调用 release 实例方法的函数,释放赋值在对象类型的结构体成员变量中的对象。

这两个函数在源码中并没有被调用,而是在 Block 从栈复制到堆上时以及堆上的 block 被废弃时会调用这些函数,如下表:调用 copy 函数 和 dispose 函数的时机

函数 调用时机
copy 函数 栈上的 Block 复制到堆时
dispose 函数 堆上的 Block 被废弃时

什么时候栈上的 Block 会复制到堆上呢?

  • 调用 Block 的 copy 实例方法时
  • Block 作为函数的返回值时
  • 将 Block 赋值给附有 __strong 的修饰符 id 类型的类,或 Block 类型成员变量时
  • 在方法名中含有 usingBlock 的 Cocoa 框架方法或者 GCD 的 API 中传递 Block 时

上面四种情况,栈上的 Block 都会复制到堆上,但其实本质都可以归结为 block_copy 函数被调用时 Block 从栈复制到堆。

特殊说明:针对如下修改,书中说程序会强制结束:

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

但是我测试时,调用 testBlock 函数依然输出:

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

不确定是 Apple 之后做了修改,还是我不应该借用 OC 的机制这么写。

另外, __testBlock_block_copy_0 函数是否存在,与是否调用了 copy 实例方法并没有关系。

相对的,在释放复制到堆上的 Block 后,谁都不持有 Block 而被废弃时调用 dispose 函数,相当于对象的 dealloc 方法。比如在使用了 __weak 关键字时:

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 转化,会报错:

 cannot create __weak reference in file using manual reference counting

__block 的实现原理

__block 修饰局部变量

__block 说明符 更精确的表达方式应该是 __block 存储域类说明符(__block storage-class-specifier)。C 语言有如下存储域类说明符:

  • typedef
  • extern
  • static
  • auto
  • register

__block 说明符类似 static、auto和 register 说明符,用于指定将变量值放到哪个存储域中。例如,auto 表示作为自动变量存储在栈中,static 表示作为静态变量存储在数据区中。给自动变量加上 __block 说明符后,就会改变这个自动变量的存储区域。

下面通过给局部变量添加 __block 关键字后效果来分析:

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 内部可以被修改,且修改的值被同步到 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 修饰的自动变量,被转变为 __Block_byref_valNumber_0 结构体存在,即栈上生成 __Block_byref_valNumber_0 结构体实例。
  • block 的构造结构体 __lineBlock_block_impl_0 中可以看到,通过 __block 变量的结构体指针访问该变量。
  • block 函数实现方法 __lineBlock_block_func_0 中看到,block 内部通过 valNumber->__forwarding->valNumbe 指针修改 __block 变量。

关于 __block 变量的结构体实现:

struct __Block_byref_valNumber_0 {
  void *__isa;
__Block_byref_valNumber_0 *__forwarding;
 int __flags;
 int __size;
 int valNumber;
};
  • 结构体的成员变量 valNumber 持有原本变量的字面值。
  • __forwarding: 结构体实例的成员变量 __forwarding 持有指向该实例自身的指针,可以通过成员变量 __forwarding 访问成员变量 valNumber,即 valNumber->__forwarding->valNumbe。如下图:
访问__block变量.png

__Block_byref_valNumber_0 结构体之所以不在 __lineBlock_block_impl_0 结构体内部定义,是为了在多个 block 中共用一个 __block 变量结构体。

另外,当 Block 从栈复制到堆时,被 Block 持有的 __block 变量也会复制到堆上,注意,这里会增加 __block 变量的引用计数。同时,如果 Block 被废弃,持有的 __block 变量也会被释放。

不管 __block 变量配置在栈上还是堆上,都能够正确的访问该变量。通过下面这个图可以更深刻的理解这句话:

复制__block变量.png

__block 修饰对象

__block 说明符可以用来指定任何类型的自动变量,下面用来指定 id 类型的自动变量:

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

ARC 有效时,id 类型以及对象类型变量必定附加所有权修饰符,缺省为附有 __strong 修饰符的变量。改代码通过 clang 转换。 __block 变量的结构体如下:

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 类型或对象类型,生成了 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 转换为 Block 结构体类型的自动变量, __block 变量转换为 __block 变量的结构体类型的自动变量。而所谓 结构体类型的自动变量,即栈上生成的该结构体的实例。

Block__block 变量的实质:

名称 实质
Block 栈上 Block 的结构体实例
__block变量 栈上 __block 变量的结构体实例

而 Block 根据存储域的不同,可以归结为三类:

Block 的类 存储域 拷贝效果
_NSConcreteStackBlock 从栈拷贝到堆
_NSConcreteGlobalBlock 程序的数据区域(.data) 什么也不做
_NSConcreteMallocBlock 引用计数增加

全局 Block: _NSConcreteGlobalBlock

因为全局 Block 的结构体实例设置在程序的数据存储区,可以在程序任意位置通知指针来访问,只有两种情况下会产生全局 Block:

  • 记述全局变量的地方有 Block 语法时。
  • Block 语法的表达式中不截获自动变量时。

关于情况一,如下:

#include <stdio.h>

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

通过 Clang 分析代码,获取 Block 的结构体实例如下,可知确实是全局 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 的结构体如下

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,从变量作用域外也可通过指针安全的使用。但设置在栈上的 Block,如果其所属变量作用域结束,该 Block 就被废弃。由于 __block 变量也配置在栈上,同样,其所属的变量作用域结束,则该 __block 变量也被废弃。

Blocks 机制提供了将 Block 和 __block 变量从栈上复制到堆上的方法来解决这个问题。将配置在栈上的 Block 复制到堆上,这样即使 Block 语法记述的变量作用域结束,堆上的 Block 还可以继续存在。

堆 Block:_NSConcreteMallocBlock

将栈 Block 复制到堆后,block结构体的 ISA 成员变量变成了 _NSConcreteMallocBlock。

在对 Block 使用 copy 实例方法时:

Block 的类 副本源的配置存储域 复制(copy)效果
_NSConcreteStackBlock 从栈拷贝到堆
_NSConcreteGlobalBlock 程序的数据区域 什么也不做
_NSConcreteMallocBlock 引用计数增加

不管 Block 配置之在何处,用 copy 方法都不会引起任何问题,在不确定时调用 copy 方法即可。

在 ARC 中不能显示调用 release,多次调用 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;
}

内存管理并不会存在问题。

大多数情况下,编译器会自行判断将 Block 从栈上复制到堆上:

  • block 作为函数返回值的时候。
  • 部分情况下向方法或函数中传递 Block 时。部分情况主要是下面两种情况:
    • Cocoa 框架的方法且方法名中含有 usingBlock 时。
    • GCD 中的 API。

除了以上两种情况,我们基本是都要手动 copy Block,才能将其从栈复制到堆。

备注,前文也提到过 Block 从栈复制到堆的情况,那里多了个将 Block 赋值给 __strong 类型的对象,这里却没有说,暂时没有找到验证的方式。

那么 __block 变量在 Block 执行 copy 操作后会发生什么呢?

  • 任何一个 Block 被复制到堆上时,使用的 __block 变量也会复制到堆上,并被该 Block 持有。
  • 如果接着有其他 Block 复制到堆上,被复制的 Block 也持有 __block 变量,则会增加 __block 变量的引用计数。反过来如果堆上 Block 被废弃,它所持有的 __block 变量也会被释放。

Block 循环引用

如果在 Block 内部使用附有 __strong 修饰符的对象类型自动变量,那么当 Block 从栈复制到堆时,该对象为 Block 所持有。这样容易引起循环引用。如下:

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,导致循环引用,MyObject 的对象实例会无法释放,造成内存泄漏。

另外,编译器有时候能够及时的查处循环引用,进行编译警告 Capturing 'self' strongly in this block is likely to lead to a retain cycle

__weak 修饰符

为避免循环引用,可声明附有 __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,引起循环引用:

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 语法内部使用的 obj_ 实际上截获了 self。对编译器来说,obj_ 只是对象用结构体的成员变量,即:

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

这里说个容易出错的情况,如果某个属性用 weak 关键字呢?

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

@implementation Person

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

这里依然会有循环引用的警告,因为循环引用是 self 和 block 之间的事,和被这个 Block 持有的成员变量时strong、weak都没有关系,即使是基本数据类型 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_();
}

如果不调用 execBlc 实例方法,即不给成员变量 blk_ 赋值,便会循环引用并造成内存泄漏。

使用 __block 避免循环引用的优点:

  • 通过 __block 变量可控制对象的持有时间。
  • 为避免循环引用必须执行 Block,否则循环引用一直存在。

说一下,使用 __block 来避免循环引用,感觉并不方便。不过也可以更具实际情况去具体选择。

参考文档

2018年 iOS 面试心得

后记

《Objective-C高级编程》三篇总结之一:引用计数篇

《Objective-C高级编程》三篇总结之二:Block篇

《Objective-C高级编程》三篇总结之三:GCD篇

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容