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指向堆上的结构体成员变量。

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

推荐阅读更多精彩内容