iOS block的用法和原理实现

1.block的语法

1.1 标准声明和定义

 返回类型(^block名称)(参数类型) = ^返回类型(变量类型 变量名称){实现}

直接定义block时,可以省略定义时的返回类型,即

返回类型(^block名称)(参数类型) = ^(变量类型 变量名称){实现}

若参数类型为void,可省略写成

返回类型(^block名称)(void) = ^{实现}

匿名block:block定义时,等号右边的即为匿名block

1.2 typedef简化block的声明

typedef 返回类型(^block名称)(参数类型);

2. block中变量的访问

2.1 block访问局部变量

    int num = 10;
    void (^myBlock)(void) = ^{
        NSLog(@"num的值为%d", num);
    };
    num = 15;
    myBlock();

输出结果为10,若在block中修改num的值,则会报错。
把上述代码转为c++代码,转换步骤:

在终端cd当文件所在目录,在终端输入命令 clang -rewrite-objc 文件名

转换前的代码

@implementation WYYBlock
- (void)test{
    int num = 10;
    void (^myBlock)(void) = ^{
        NSLog(@"num的值为%d", num);
    };
    num = 15;
    myBlock();
}
@end

转换后的代码与block相关的如下:

struct __WYYBlock__test_block_impl_0 {
  struct __block_impl impl;
  struct __WYYBlock__test_block_desc_0* Desc;
  int num;
  __WYYBlock__test_block_impl_0(void *fp, struct __WYYBlock__test_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __WYYBlock__test_block_func_0(struct __WYYBlock__test_block_impl_0 *__cself) {
  int num = __cself->num; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_bh_3br_11pn5hxckj2qxzhfb1c80000gp_T_WYYBlock_00083b_mi_0, num);
    }

static struct __WYYBlock__test_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __WYYBlock__test_block_desc_0_DATA = { 0, sizeof(struct __WYYBlock__test_block_impl_0)};

static void _I_WYYBlock_test(WYYBlock * self, SEL _cmd) {
    int num = 10;
//声明定义block
    void (*myBlock)(void) = ((void (*)())&__WYYBlock__test_block_impl_0((void *)__WYYBlock__test_block_func_0, &__WYYBlock__test_block_desc_0_DATA, num));
    num = 15;
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
}

定义声明block转换后的代码是下面的内容

void (*myBlock)(void) = ((void (*)())&__WYYBlock__test_block_impl_0(
        (void*)__WYYBlock__test_block_func_0, &__WYYBlock__test_block_desc_0_DATA, num)
    );

可以看出来,block变量实际上是一个指向结构体__WYYBlock__test_block_impl_0的指针,而结构体的第三个元素是局部变量num的值
__WYYBlock__test_block_impl_0中,WYYBlock是该文件的类名,test是block所在方法的名称。
结构体的第一个变量是(void*)__WYYBlock__test_block_func_0,在该方法中可以看到,在方法内部有一个参数num接受了外面的num
基本数据类型会直接把值传递过来,所以num的值在block定义完时就已经固定,在定义之后无法在进行修改。

2.2block中访问__block修饰的局部变量

把OC代码修改为

- (void)test{
    __block int num = 10;
    void (^myBlock)(void) = ^{
        NSLog(@"num的值为%d", num);
    };
    num = 15;
    myBlock();
}

可以看到转换后,block的声明和定义变为

void (*myBlock)(void) = ((void (*)())&__WYYBlock__test_block_impl_0(
  (void *)__WYYBlock__test_block_func_0, &__WYYBlock__test_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344)
);

可以看到原来的第三个参数num,变成了*(__Block_byref_num_0 )&num,传入的是num的引用,所以此时定义block后改变num的值,调用block时,num的值会相应改变,也可以在block中改变num的值。
同时,在结构体__WYYBlock__test_block_impl_0中第三个变量也由原来的 int num;变成了 __Block_byref_num_0 *num;
总结:block在定义后,局部变量的值改变不会改变block中局部变量的值,并且在block中局部变量的值无法修改。因为在Block定义时便是将 "局部变量的值" 传给Block变量所指向的结构体,因此在调用Block之前对局部变量进行修改并不会影响Block内部的值。如果要在block中修改局部变量的值,或者局部变量的值会随时修改,局部变量需要用__block修饰

2.3block内访问全局变量

把OC代码修改为

int num = 10;
- (void)test{
    void (^myBlock)(void) = ^{
        NSLog(@"num的值为%d", num);
    };
    num = 15;
    myBlock();
}

运行结果为15,同时在block中修改num的值也不会报错
把当前代码编译成C++文件后可以发现,声明定义block时并没有再传入num的值或者指针

void (*myBlock)(void) = ((void (*)())&__WYYBlock__test_block_impl_0(
(void *)__WYYBlock__test_block_func_0, &__WYYBlock__test_block_desc_0_DATA) );

__WYYBlock__test_block_impl_0结构体中也没有了num,所以说明全局变量不会再传入block中,当调用block时需要使用全局变量时是从类的空间中获取的,所以调用block时获取到的值是num最后的值,在block中也能修改num的值。
全局变量所占用的内存只有一份,供所有函数共同调用,在Block定义时并未将全局变量的值或者指针传给Block变量所指向的结构体。

2.4 block内访问静态变量

把OC代码修改为

- (void)test{
    static int num = 10;
    void (^myBlock)(void) = ^{
        NSLog(@"num的值为%d", num);
    };
    num = 15;
    myBlock();
}

运行结果为15,同时在block中修改num的值也不会报错
把当前代码编译成C++文件后可以发现,声明定义block时传入的是num的指针

void (*myBlock)(void) = ((void (*)())&__WYYBlock__test_block_impl_0(
(void *)__WYYBlock__test_block_func_0, &__WYYBlock__test_block_desc_0_DATA,  &num));

2.5block内访问类型为对象的局部变量

有一个WYYPerson类,类中有一个int类型的age属性,OC代码如下

WYYPerson *person = [[WYYPerson alloc] init];
    person.age = 10;
    void (^myBlock)(void) = ^{
        NSLog(@"%d", person.age);
    };
    person.age = 18;
    myBlock();

运行结果为18,因为局部对象的类型为对象时,对象传入的是引用
编译成C++后的文件内容

//block中的代码
static void __WYYBlock__test_block_func_0(struct __WYYBlock__test_block_impl_0 *__cself) {
  WYYPerson *person = __cself->person; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_bh_3br_11pn5hxckj2qxzhfb1c80000gp_T_WYYBlock_90d3f3_mi_0, ((int (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("age")));
    }
//block的定义和实现
void (*myBlock)(void) = ((void (*)())&__WYYBlock__test_block_impl_0(
(void *)__WYYBlock__test_block_func_0, &__WYYBlock__test_block_desc_0_DATA, person, 570425344));

从block的定义和实现可以知道,传入的是person对象的指针,从block中的代码可以知道,当获取person的值是通过给person对象发送消息获取age的值的,所以在block中也可以修改person的值
把一个类改为在MRC模式下的步骤:
Build phases -> Compile Sources -> 双击需要改成MRC模式的类会弹出输入框 -> 把-fno-objc-arc复制粘贴进去

3.block的三种类型

1.NSGlobalBlock:全局的静态 block,在block中不访问外部局部变量,可以访问外部全局变量和静态变量。此时为NSGlobalBlock。
2.NSStackBlock :保存在栈中的 block,当函数返回时会被销毁。
3._NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁。

3.1 在MRC和ARC两种情况下,block内部不使用局部变量,在block中使用全局变量和静态变量,block是NSGlobalBlock

在block中不访问外部局部变量,代码如下

  void (^myBlock)(void) = ^{
        NSLog(@"不访问外部局部变量");
    };
    myBlock();
    NSLog(@"%@", myBlock);

打印结果为
不访问外部局部变量
<NSGlobalBlock: 0x100f182c0>
在MRC模式下打印结果相同
在MRC模式下在block内部访问静态变量,block也是NSGlobalBlock
代码

static int num = 10;
    void (^myBlock)(void) = ^{
        NSLog(@"%d", num);
    };
    myBlock();
    NSLog(@"%@", myBlock);

结果:<NSGlobalBlock: 0x1006142c0>
在MRC模式下在block内部访问全局变量,block也是NSGlobalBlock
代码:

int num = 10;
- (void)test{
    void (^myBlock)(void) = ^{
        NSLog(@"%d", num);
    };
    myBlock();
    NSLog(@"%@", myBlock);
}

结果:<NSGlobalBlock: 0x1045742c0>
总结:
在ARC下,block内部不使用局部变量,在block中使用全局变量和静态变量,block的类型都是NSGlobalBlock
在MRC下,block内部不使用局部变量,在block中使用全局变量和静态变量,block的类型都是NSGlobalBlock

3.2 在MRC情况下会存在栈block

在ARC模式下没有NSStackBlock,必须在MRC下才能看到, 代码如下:

    int num = 10;
    void (^myBlock)(void) = ^{
        NSLog(@"%d", num);
    };
    myBlock();
    NSLog(@"%@", myBlock);

打印结果为:<NSStackBlock: 0x16ba85328>

3.3 在MRC下分别用assign,retain,strong,weak,copy修饰block

如果是全局block的话,不管用什么修饰词,仍然是全局block,此处不再做代码验证,我自己已经打印过结果,下面只验证在MRC下使用局部变量,本来是栈block
用assign关键字修饰block,完整代码:

@interface WYYBlock ()
@property (nonatomic, assign) void (^myBlock)(void);

@end

@implementation WYYBlock

- (void)test{
    int num = 10;
    self.myBlock = ^{
        NSLog(@"%d", num);
    };
    self.myBlock();
    NSLog(@"%@", self.myBlock);
}
- (void)dealloc{
    NSLog(@"WYYBlock的dealloc方法被调用");
    [super dealloc];
}
- (void)test2{
    NSLog(@"%@", self.myBlock);
}
@end

在外面先调用test方法,再调用test2方法,在执行test2中的代码NSLog(@"%@", self.myBlock);,崩溃。说明在栈中的block出栈之后即会被销毁,此时block为栈block。
用retain关键字修饰block

@property (nonatomic, retain) void (^myBlock)(void);
结果与用assign修饰时相同,仍然为栈block,在执行test2中的代码*NSLog(@"%@", self.myBlock);*,崩溃

用weak关键字修饰block

@property (nonatomic, weak) void (^myBlock)(void);
结果与用assign修饰时相同,仍然为栈block,在获取myBlock时崩溃,造成了野指针

用strong关键字修饰block

@property (nonatomic, strong) void (^myBlock)(void);
打印结果为
<__NSMallocBlock__: 0x280588a20>
 <__NSMallocBlock__: 0x280588a20>
此时block变成了堆block,不再随方法结束而销毁,所以可以第二次进行调用

用copy关键字修饰block

@property (nonatomic, copy) void (^myBlock)(void);
结果与用strong修饰时相同,block变成了堆block

由以上结果可以得到结果,在MRC模式下,当用copy和strong修饰时,block会被存放在堆中,不会因为所在方法结束而销毁block。

3.4 在ARC下分别用assign,retain,strong,weak,copy修饰block

如果是全局block的话,不管用什么修饰词,仍然是全局block,此处不再做代码验证,我自己已经打印过结果,下面只验证在ARC下使用局部变量,本来是堆block
用assign关键字修饰block

@property (nonatomic, assign) void (^myBlock)(void);
此时会变成栈block,当在test2,再次调用block时,会崩溃,因为block已经被销毁

用weak修饰时也会变成栈block,并且编译器会提示使用过后会销毁
用retain,strong,copy修饰时都仍然是堆block。
用retain修饰时会提示警告

Retain'ed block property does not copy the block - use copy attribute instead
提示,retain不会赋值block,推荐使用copy代替

总结:
1.block内部没有调用外部局部变量,或者调用了全局变量或者静态变量时,在MRC和ARC时都在全局区。
2.在ARC下,block调用了外部局部变量,系统默认会对block进行copy操作,也在堆区。在MRC下block调用了外部局部变量,会在栈区,所以此时修饰block要使用strong或者copy,把block放在堆区,这样block才不会随时被销毁,造成调用的时候已经被销毁的情况。
3.block造成循环引用的原因:block内部是一个结构体,会捕获外部传入的局部变量,保存在block的结构体中,如果在block中使用self,block又被self持有,此时会造成循环引用
4.block用weakSelf时,为什么在block内部又要声明一个strongSelf?
如果只用weakSelf打破循环引用的话,block是没有持有控制器的,当控制器被pop掉的时候,控制器就已经被销毁了,可能此时block被调用,可是控制器已经被销毁了,会引起崩溃,所以需要在block中在强引用self
5.strongSelf对weakSelf强引用,weakSelf对self弱引用,最终不也是对self进行了强引用,会导致循环引用吗不会的,因为strongSelf是在block里面声明的一个指针,当block执行完毕后,strongSelf会释放,这个时候将不再强引用weakSelf,所以self会正确的释放。

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