Block详解

__block说明符

Block只能保存局部变量瞬间的值,所以当我们尝试修改截获的自动变量值,就会报错。例如:

int val = 0;
void (^blk)(void) = ^(val = 1);
blk();
printf("val = %d\n", val);

该源代码会产生编译错误:

error: variable is not assignable (missing __block type specifier)

因此,若想修改截获的局部变量值,就必须用__block修饰

__block int val = 0;
void (^blk)(void) = ^(val = 1);
blk();
printf("val = %d\n", val);

执行结果为:

val = 1;

下面我们再看一个例子:

id array = [[NSMutableArray alloc] init];
    void (^blk)(void) = ^{
        id obj = [[NSObject alloc] init];
        [array addObject:obj];
    };

这是没问题的。如果向截获的变量array赋值则会产生编译错误。

id array = [[NSMutableArray alloc] init];
    void (^blk)(void) = ^{
        array = [[NSMutableArray alloc] init];
    };

同样,使用__block修饰array就行了。

另外在使用c语言数组时,必须小心使用其指针。

const char text[] = "hello";
    void (^blk)(void) = ^{
        printf("%c\n", text[2]);
    };

看似没有向截获的自动变量赋值,只是使用了字符串数组。但是Block并没有实现截获c语言数组。此时可以使用指针解决问题

const char *text = "hello";
    void (^blk)(void) = ^{
        printf("%c\n", text[2]);
    };

Block的实质

我们通过一个实例来看看block的实质

void (^blk)(void) = ^{
        printf("Block\n");
 };

通过clang来转换为c++的代码

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m

我们看一下转换后的代码:

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) {
        printf("Block\n");
 }

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() {
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

    //使用block
    (void (*) (struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk)
}

我们可以看到Block的匿名函数转化为c语言函数来处理:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        printf("Block\n");
 }

__cself是指向实例自身的变量self
__main_block_impl_0结构体声明如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  //这是__main_block_impl_0的构造函数
  __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;
  }
};

__main_block_impl_0结构体由__block_impl和__main_block_desc_0两个成员变量和一个构造函数组成。
其中__block_impl结构体声明如下:

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

第二个成员变量为Desc指针,声明如下:

struct __main_block_desc_0 {
  unsigned long reserved;//今后版本升级所需的区域
  unsigned long Block_size//Block的大小
}

因此,如果展开__main_block_impl_0,可记述如下形式:

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

接下来看看__main_block_impl_0的构造函数:

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

_NSConcreteStackBlock用来初始化block_impl结构体的isa成员变量
flags用于初始化block_impl结构体的flags
fp用于初始化block_impl结构体的FuncPtr

接下来我们再看一下原来构造函数的调用:

void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

该源代码将__main_block_impl_0结构体类型的变量(即栈上生成的__main_block_impl_0结构体实例的指针)赋值给blk。

然后再看看是如何使用block的

 (void (*) (struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);

去掉转换部分:

(*blk->impl.FuncPtr)(blk);

就是通过函数指针FuncPtr来调用blk本身。
这也证明了__main_block_func_0的参数__cself指向block值。

直到现在我们明白了block的本质:

  • block本质就是一个__main_block_impl_0。
  • block通过内部的函数指针FuncPtr来调用它本身。

还有一点刚才没有说明,刚才在初始化__block_impl 的isa成员变量的_NSConcreteStackBlock又是什么呢???
要理解_NSConcreteStackBlock,我们结合objc_object,它也有一个isa指针,用于指向该对象所属的类。
同理:在将Block作为对象处理时,__block_impl的isa指针指向的类的信息保存在_NSConcreteStackBlock上。即它是在栈上生成的__block_impl结构体实例。



截获自动变量值

int main() {
    int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    void (^blk)(void) = ^{
        printf(fmt, val);
    };
    blk();
    return 0;
}

clang之后:

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

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 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));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

我们先看一下不同之处:

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将所使用的局部变量作为成员变量追加到了__main_block_impl_0结构体中。
注意:未使用的的变量将不会追加。

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val)

将追加的局部变量作为参数来初始化结构体。
__main_block_impl_0展开代码如下:

    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = 0;
    impl.FuncPtr = _main_block_func_0;
    Desc =&__main_block_desc_0_DATA;
    fmt = "val = %d\n"

由此可见,在__main_block_impl_0实例中,自动变量被截获。

我们再来看看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);
}

因为__main_block_impl_0截获了fmt和val变量,所以就可以直接执行了。

总的来说,所谓“截获自动变量值”意味着在执行block语法时,block语法所使用的局部变量被保存到block的结构体实例中。


__block说明符

__block说明符类似于static、auto,他们用于指定将变量值设置到哪个存储域中。auto表示作为局部变量存储在栈中,static表示作为静态变量存储在数据区中。

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

该代码编译后如下:

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_val_0 *val = __cself->val; // bound by ref

        (val->__forwarding->val) = 1;
 }

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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() {
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
}

这个__block变量是怎样转换过来的呢?

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

__block居然变成了结构体类型的局部变量,即栈上生成的__Block_byref_val_0结构体实例。
它包含原自动变量的成员变量val

我们再来看看__main_block_impl_0这个结构体有什么不同

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

新增了一个成员变量__Block_byref_val_0,将这个成员变量作为参数来初始化这个结构体
__block赋值的代码又如何呢?

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref

        (val->__forwarding->val) = 1;
 }

__block变量赋值比较复杂,__main_block_impl_0结构体实例持有指向__block变量的__Block_byref_val_0结构体实例的指针。
__Block_byref_val_0结构体实例的成员变量__forwarding持有该实例自身的指针。通过成员变量__forwarding访问成员变量val。

至此为止,我们只是大概了解了__block类型,但是还有两个问题没有解决:
*Block超出变量作用域可存在的理由

  • __block结构体中__forwording成员变量是干嘛的。

我们继续分析。

Block存储域

由前面的讲解,我们知道和
Block的本质:栈上生成的__main_block_impl_0结构体实例
__block的本质:栈上生成的__Block_byref_val_0结构体实例

我们之前说过Block其实也是一个OC对象,它所属的类是_NSConcretStackBlock,除了这个,还有其他两个:

  • _NSConcretStackBlock:设置在栈上
  • _NSConcretGlobalBlock:设置在程序的数据区域(.data)中
  • _NSConcretMallocBlock:设置在堆上

配置在全局变量的block,从变量作用外也可以通过指针安全的使用。
配置在栈上的block,如果其所属的变量作用域结束,该block就会被废弃。由于__block变量也配置在栈上,如果其所属的变量作用域也结束了,__block变量也会被废弃。

什么时候可以设置在程序的数据区域中呢?

  • 使用的block是全局的block
  • 即使不是全局的block,如果不截获的自动变量,也会设置在.data区。

除以上这两种情况,Block都是_NSConcretStackBlock类对象,设置在栈上。

现在我们回答第一个问题:Block超出变量作用域可存在的理由??
因为Blocks提供了将Block和__block变量从栈上复制到对上的办法来解决这个问题。将配置在栈上的Block复制到堆上,这样即使Block变量作用域结束,堆上的Block也可以继续存在。

复制到堆上的Block就是_NSConcretMallocBlock类对象。

impl.isa = &_NSConcretMallocBlock

接下来我们重点看看Blocks提供的复制方法:
实际上在ARC下,大多数情形下编译器会自动的判断,自动生成将Block从栈上复制到堆上的代码。我们看一下将Block作为函数值返回的代码。

typedef int (^blk_t) (int);

blk_t func(int rate) {
    return ^(int count) {
        return rate * count;
    };
}

该源代码返回配置在栈上的Block,当变量作用域结束时,这个Block就会被废弃。虽然如此,但该源代码通过编辑器可转换如下:

blk_t func(int rate) {
  blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);
  tmp = objc_retainBlock(tmp);
  return objc_autoreleaseReturnValur(tmp);
}

objc_retainBlock就是_Block_copy函数。
因此当Block作为函数值返回时,编译器会自动生成复制到堆上的代码。

如果编译器不能生成复制到堆上的代码,就需要手动调用alloc/new/copy/mutableCopy的任意一个代码。

哪些情况编译器会自动将block从栈复制到堆上:

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

第二个问题:__block结构体中__forwording成员变量是干嘛的??
它可以实现无论__block变量配置在栈上还是堆上都能够正确访问__block变量。
有时__block变量配置在堆上,也可以访问栈上的__block变量,这是因为栈上的结构体成员变量__forwarding指向堆上的结构体成员变量。

你可能感兴趣的:(Block详解)