iOS Block的应用以及深入理解

一、Block的应用

为什么不把Block的应用放到文章的最后呢,我以为实用为大,还是先把block的使用及其注意点写在前面,然后在分析block的本质吧。

1、为了方便声明block类型的变量,我们一般用typedef typedef void (^Block)(void)给block类型起个别名,这样我们就可以直接按如下方式声明block变量了。

typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
//这样声明
Block block = ^{};
    //而不是这样
    //void(^block)(void)=^{};
    return 0;
}

2、在非ARC情况下,定义块的时候(无论是全局块还是局部块),其所占的内存区域是分配在栈中的。如下声明了一个block,如下面的代码就有危险,在条件语句实现的两个block都分配在栈内存中,于是这两个块只在对应的条件语句范围内有效,这样写的代码可以编译,但是运行起来却是时而对时儿错,若编译器未复写待执行的块,则程序正常运行,若复写则程序奔溃。

void (^block)();
if (/*some condition*/) {
    block = ^{
        NSLog(@"Block A");
    };
} else{
    block = ^{
        NSLog(@"Block B");
    };
}
block();

应该按这样的姿势写

void (^block)();
if (/*some condition*/) {
  block = [^{
    NSLog(@"Block A");
  } copy];
} else{
  block = [^{
    NSLog(@"Block B");
  } copy];
}
block();

3、同理2,将block声明为属性的时候,要用strong或copy,还要注意如果你不确定你声明的这个block属性会不会被其他线程修改,你就用atomic加个原子锁,这样就线程安全了

@property (copy) Block block; //属性默认就是atomic

4、调用block的时候,有些童鞋的姿势不太对,假如我声明了一个block属性,正确调用姿势如下。

Block block  = self.block;
if (block) {
  block();
}

大部分童鞋会按下面这样写,那些连判断都不做的童鞋我就不批评你了,回去面壁去

if (self.block) {
  // 其他线程可以在此处捣乱
  block();
}

上面的写法为什么不妥呢,因为对全局block来说即使self.block当时存在,如果另一个线程在执行到我注释的那一行的时候把block释放了咋办,你再调用是不是就得到了一个完美的闪退,你可以先强引用一下这个block,把它赋值给一个临时变量,这样就不怕被其他线程释放了,所以上面的那种姿势不太稳妥。如果你看过AF的源码你就会发现,歪果仁就是按着我说的上面的正确姿势写的。

5、为什么用了__block就可以修改所截获的变量了?

因为block的特性,编译器不允许在block内直接修改所捕获的变量,但是我们可以修改__block修饰的自动变量,因为用__block修饰过之后,原先存储在栈中的变量就变成了存在堆中了,查看用clang过后的cpp文件你会发现在block中多了一个与该变量同名的__Block_byref_i_0结构体的指针变量,__Block_byref_i_0结构体i指针变量中有一个指向自己的__forwarding指针,通过i->__forwarding->i来修改存在堆中的外部变量,而没有用__block修饰的变量,block会把截获的变量copy为自己的一个普通变量。

详情可以本文最后一部分关于block的本质那一块内容。

6、避免循环引用,如果你把一个block声明成了对象的一个属性,那么该对象就会持有这个block,如果在该对象中要实现block属性的话,用到self的时候要用__weak修饰过的,不然会循环引用。

weak的实现原理是系统会为所有的弱引用对象建一张表,当弱引用对象引用的对象被释放的时候,系统会查询这张表,然后把弱引用对象置为nil,这也是代理属性为啥要用weak而不用assign的原因。置为nil后不会变成野指针。

二、block的存储区域

据调研,在MRC下block的存储位置有 栈、堆、全局数据区,ARC下只会存在 堆和全局数据区。
block是否访问外部变量是会影响他的存储区域的。

关于block的存储区域可以查看我的另一篇文章

  1. 下图是ARC模式下的代码


    ARC下的block.png
  2. 下图是非ARC模式下执行的代码


    MRC下的block.png

解释一下上面的结果,学过C的都知道,malloc是分配到堆中了,global是分配到全局数据区了,stack是分配到栈区了。

  1. MRC下此种写法Xcode会报错,但是如果不引用外部变量的话就没事,如果你仔细看7.1与7.2的介绍,你就知道原因了,不过我还是想说一下。因为在MRC情况下引入外部变量时,此种写法的block存在栈里面,而该函数的却返回了block,return标志着一个函数的结束,所以在return的时候block会被释放而报错,在MRC情况下不引入外部变量的话,此种写法的block存在全局数据区里,所以没问题。


    MRC下此种写法报错.png

ARC下,无论引不引入外部变量,都没事,不引入返回的block存在全局数据区,引入的话存在堆中。就不截图了。

  1. 下面这种情况,ARC与MRC下block都存储在全局数据区,这种情况不常出现,一般我们都是在函数中来是实现block的。


    ARC与MRC下都存储在全局数据区.png

总结(强调一下如非特殊说明,block都是函数中实现):

ARC模式下:不论你声明的是局部block还是全局block,它们只要不截获外部变量,它们都会存储在全局数据区的,如果截获外部变量,block就会存储在堆(heap)中。

非ARC模式下:不论你声明的是局部block还是全局block,它们只要不截获外部变量,它们都会存储在全局数据区的,如果截获外部变量,block就会存储在栈(stack)中。

两种模式下的差别:只要不截获外部变量block一律都存在全局数据区,只有截获了外部变量ARC和MRC才有所区别,而开发中往往我们的block都是后面这么一个情况,现在很少有人使用非ARC了吧,所以还是关注ARC的情况吧,即你只需要记住结论的第一条就好了。

三、block 的本质

block其本质是一个结构体struct,通过clang编译器转换成C++代码可以看出,执行clang -rewrite-objc 要转换的OC文件命令,可以在同级目录下获得一个.cpp文件,里面就是转换后的OC代码,下面我会分三种情况给出OC代码及其对应的cpp代码。

1、只是纯粹的在入口函数中定义了一个block,block中也没有引入外部变量

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
  void(^block)(void)=^{
    NSLog(@"Block!!");
  };
  block();
  return 0;
}

下面是转换后的C++代码,为了方便观察,我把文件最下方的有关block的代码摘录如下

//block的结构体
struct __main_block_impl_0 {
  //block的实现
  struct __block_impl impl;
  //block的描述(包含block的大小以及copy,dispose等)
  struct __main_block_desc_0* Desc;
  //block的 构造函数,对block结构体成员变量的初始化
  __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;
  }
};

//block内的代码实现部分
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_1q_hr0kg_v15rj7ry_618ljfldr0000gn_T_hellow_a5b27a_mi_0);
}

//block的描述,包含block的大小以及copy,dispose
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)};

//OC中的main函数
int main(int argc, char * argv[]) {

  void(*block)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
  return 0;
}

2、在入口函数中定义了一个block,并在block中引入外部整型变量i

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
  //自动变量i
  int i = 10;
  void(^block)(void)=^{
    NSLog(@"Block!!---%d",i);
  };
  block();
  return 0;
}

转换后的cpp代码

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  //这是block捕获的变量
  int i; 
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _i, int flags=0) : i(_i)  {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

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

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[]) {
  int i = 10;
  void(*block)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, i));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
  return 0;
}

3、在入口函数中定义了一个block,并在block中引入外部整型变量i(i可以是普通变量,如NSInteger,也可以是OC对象,如NSArray,但是如果是NSString类型的话,用clang命令转换代码的时候会出错),并且i用__block修饰

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
  __block int i = 10;
  void(^block)(void) = ^{
    i += 1;
    NSLog(@"Block!!---%d",i);
  };
  block();
  return 0;
}

转换后的cpp代码

//存储block截获的外部变量的一个结构体
struct __Block_byref_i_0 {
  void *__isa;
  __Block_byref_i_0 *__forwarding;
  int __flags;
  int __size;
  int i;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  //这是block捕获的变量
  __Block_byref_i_0 *i; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__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_i_0 *i = __cself->i; // bound by ref

  (i->__forwarding->i) += 1;
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_1q_hr0kg_v15rj7ry_618ljfldr0000gn_T_main_10e8d1_mi_0,(i->__forwarding->i));
}

//下面两个指针函数是__main_block_desc_0结构体中的函数指针的实现,前者是要保留block截获的对象,后者则将之释放

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
  _Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);
}

//block的描述
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(int argc, const char * argv[]) {

  __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 10};
  void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));
  ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

  return 0;
}

总结

第一种和第二种比较可得知,当block截获普通的(没有用__block修饰)外部变量时,block会把截获的变量注册成为自己的成员变量(其类型和变量名同原来一样),这也是为什么block不能直接修改截获的变量的原因,因为在block内操作的外部变量其实是block的同名的成员变量。

第一种和第三种比较可知,当block截获被__block修饰的外部变量时,block会把截获的变量封装成__Block_byref_i_0结构体,并把结构体指针变量注册为自己的成员变量(变量名不变,类型为__Block_byref_i_0)。

同时被__block修饰的外部(局部)变量也会被拷贝堆变里面,这样这个外部变量就不会随函数的结束而被释放了(如果这个外部变量指的是局部变量的话),但它的作用域还跟原来一样,__Block_byref_i_0结构体i指针变量中有一个指向自己的__forwarding指针,通过i->__forwarding->i来修改存在堆中的外部变量。

另外,我们如果在block中修改一个属性的话,是不需要用__block修饰的,因为block会把self捕获成自己的一个变量,然后可以给self通过objc_msgSend发送消息来改变其属性。

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

推荐阅读更多精彩内容