Block 再学习 !

如何优雅的使用 Block?

How Do I Declare A Block in Objective-C?

阮一峰的一句话解释简洁明了:闭包就是能够读取其它函数内部变量的函数

详情:http://blog.csdn.net/jasonblog/article/details/7756763

block的几种适用场合:

  • 任务完成时回调处理
  • 消息监听回调处理
  • 错误回调处理
  • 枚举回调
  • 视图动画、变换
  • 排序

作为基本变量 As a local variable

1
returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...};

作为对象属性 As a property

1
@property (nonatomic, copy) returnType (^blockName)(parameterTypes);

作为方法参数 As a method parameter

1
- (void)someMethodThatTakesABlock:(returnType (^)(parameterTypes))blockName;

作为回调 As an argument to a method call:

1
[someObject someMethodThatTakesABlock:^returnType (parameters) {...}];

作为类型别名 As atypedef

1
2
typedef returnType (^TypeName)(parameterTypes);
TypeName blockName = ^returnType(parameters) {...};

什么是Block

  • Block是iOS中一种比较特殊的数据类型
  • 是一个能工作的代码单元,可以在任何需要的时候被执行
  • 本质是轻量级的匿名函数,可以作为其他函数的参数或者返回值。
  • Block是苹果官方特别推荐使用的数据类型, 应用场景比较广泛
    • 动画
    • 多线程
    • 集合遍历
    • 网络请求回调
  • Block的作用
    • 用来保存某一段代码, 可以在恰当的时间再取出来调用
    • 功能类似于函数和方法
1
2
//Block 是对 C 语言的一个拓展
//快速创建 Block 用 inlineBlock

Block的格式

  • Block的定义格式
1
2
3
返回值类型 (^block变量名)(形参列表) = ^(形参列表) {

};

Block 再学习 !

  • block最简单形式(无参无返回值)
1
2
3
4
5
6
void (^block名)() = ^{代码块;}

例如:
void (^myBlock)() = ^{
NSLog(@"Damonwong");
};
  • block中级进阶形式(有参无返回值)
1
2
3
4
5
6
7
void (^block名称)(参数列表)
= ^ (参数列表) { // 代码实现; } 例如:
void (^myBlock)(int) = ^(int num){
NSLog(@"num = %i", num);
};
  • block高级进阶形式(有参有返回值)
1
2
3
4
5
6
7
返回类型 (^block名称)(参数列表)
= ^ 返回类型 (参数列表) { // 代码实现; } 例如:
int (^myBlock)(int, int) = ^(int num1, int num2){
return num1 + num2;
};
  • 调用Block保存的代码
1
block变量名(实参);

Block 与 变量

观察下面四段代码的输出值

  • Block 可以读取变量,但是默认情况下不能修改变量的值
1
2
3
4
5
6
7
8
int x = 123;
void (^printXAndY)(int) = ^(int y) {
x = 100; //会报错,
printf("%d %d\n", x, y);
};
printXAndY(456);
/*------------------*/
@"prints: 123 456"
  • Block 能读取变量的原理是 「copy」了一份变量的值。所以在 Block 定义之后修改变量的值,再调用 Block,值依旧是修改前的。换句话说,定义好 Block 之后,修改变量值对 Block 无效。
1
2
3
4
5
6
7
8
int x = 123;
void (^printXAndY)(int) = ^(int y) {
printf("%d %d\n", x, y);
};
x = 456; // 修改 x 为 456,block 依旧输出 123
printXAndY(456);
/*------------------*/
@"prints: 123 456"
  • __blcok关键字的神奇功效。

首先,如果需要对block 外部定义的变量在 block 内修改,那么需要对这个变量添加一个__block修饰。

1
2
3
4
5
6
7
8
__block int x = 123;
void (^printXAndY)(int) = ^(int y) {
x = 100; //不会报错
printf("%d %d\n", x, y);
};
printXAndY(456);
/*------------------*/
@"prints: 100 456"

如果需要在调用之前,变量的修改都会影响 block 内部对这个变量的使用,换句话说,block 对变量不再是简单的值复制,而是动态的"监听"值的变化,然后在调用的时候读取变量的值。需要对这个变量添加一个__block修饰。

1
2
3
4
5
6
7
8
__block int x = 123;
void (^printXAndY)(int) = ^(int y) {
printf("%d %d\n", x, y);
};
x = 456 //block 会「动态」识别外部变量的变化,输出456
printXAndY(456);
/*------------------*/
@"prints: 456 456"
  • 注意区别变量指针所指向的变量
1
2
3
4
5
6
7
8
int x = 123;
NSMutableString *str = [NSMutableString stringWithFormat:@"Damon"];
void (^printStr)() = ^() {
[str appendString:@"wong"];
NSLog(@"%@",str);
};
printStr();
// prints:Damonwong

这里的 [str appendString:@"wong"];不报错是因为str 是指向@"Damon"的函数指针,[str appendString:@"wong"];并不是修改 str 存储的值,本质上是 str 向@"Damon"发送了一条appendString 消息,然后再更改@"Damon"@"Damonwong",而 str 存储的指向@"Damonwong"对象的指针没有发生变化。所以,block 本质是不能修改变量存储的值,但是消息分发依旧有效。



Block 的循环引用问题

虽然 Block 用起来特别方便,但是要特别注意循环应用的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ARC enabled
/************** MyObject Class **************/
typedef void (^myBlock)(void);
@interface MyObject : NSObject
{
myBlock block;
}
@end
@implementation MyObject
- (id)init
{
self = [super init];
block = ^{
NSLog(@"self = %@", self);
};
return self;
}
- (void)dealloc
{
NSLog(@"dealloc");
}
@end
/************** main function **************/
int main()
{
id myObject = [[MyObject alloc] init];
NSLog(@"%@", myObject);
return 0;
}

由于 self 是 __strong 修饰,在 ARC 下,当编译器自动将代码中的 block 从栈拷贝到堆时,block 会强引用和持有 self,而self 恰好也强引用和持有了 block,就造成了传说中的循环引用。

为了避免这种情况发生,可以在变量声明时用 __weak修饰符修饰变量 self,让 block 不强引用 self,从而破除循环。

Block 再学习 !

1
2
3
4
5
6
7
8
9
- (id)init
{
self = [super init];
__weak typeof(self) weak_self = self;
block = ^{
NSLog(@"self = %@", weak_self);
};
return self;
}

Block 再学习 !

黑科技,防止循环引用

1
2
3
4
5
6
7
8
9
10
11
- (id)init
{
self = [super init];
__block typeof(self) temp = self;
block = ^{
NSLog(@"self = %@", temp);
temp = nil;
};
return self;
}
// 使用这个,必须调用一次 block

Tips

  • 宏定义:#define Weak_Ref(obj) _weak typeof(obj) weak##obj = obj;

  • 注意self.name这类点语法,[self name]消息传递及 self

  • block 中使用 self不一定造成循环引用,但可能性极大

  • 重写 dealloc 方法可以很方便的知道是否存在循环引用

Block 在内存中的位置

由于block也是NSObject,我们可以对其进行retain操作。不过在将block作为回调函数传递给底层框架时,底层框架需要对其copy一份。比方说,如果将回调block作为属性,不能用retain,而要用copy。我们通常会将block写在栈中,而需要回调时,往往回调block已经不在栈中了,使用copy属性可以将block放到堆中。或者使用Block_copy()和Block_release()。

  • 根据Block在内存中的位置分为三种类型
    • NSGlobalBlock:类似函数,位于text段,全局的静态 block,不会访问任何外部变量
    • NSStackBlock :保存在栈中的 block,当函数返回时会被销毁
    • NSMallocBlock:保存在堆中的 block,当引用计数为 0 时会被销毁。

MRC 下的 Block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 /*------------MRC-----------------*/
typedef long (^MyBlock)(int, int); MyBlock block1 = ^ long (int a, int b) {
return a + b;
};
NSLog(@"block1 = %@", block1);
// block1 = <__NSGlobalBlock__: 0x47d0> int base = 100;
MyBlock block2 = ^ long (int a, int b) {
return base + a + b;
};
NSLog(@"block2 = %@", block2);
// block2 = <__NSStackBlock__: 0xbfffddf8> MyBlock block3 = [[block2 copy] autorelease];
NSLog(@"block3 = %@", block3);
// block3 = <__NSMallocBlock__: 0x902fda0>

block1没有使用任何外部变量,因此存储在 代码区,编译器给其的类型为NSGlobalBlock

block2使用到了局部变量,在定义(注意是定义,不是运行)block2时,局部变量base当前值被copy到栈上,作为常量供Block使用。编译器给其类型为NSStackBlock

block3 经过拷贝,局部变量 base 的值被 copy 到堆中,编译器给其类型为NSMallocBlock 总结说来,block 的类型取决于内部使用的变量在哪。

ARC 下的 Block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*------------ARC-----------------*/
typedef long (^MyBlock)(int, int);
MyBlock block1 = ^ long (int a, int b) {
return a + b;
};
NSLog(@"block1 = %@", block1);
// block1 = <__NSGlobalBlock__: 0x100001080> int base = 100;
MyBlock block2 = ^ long (int a, int b) {
return base + a + b;
};
NSLog(@"block2 = %@", block2);
// block2 = <__NSMallocBlock__: 0x100203cf0> __block int sum = 100;
MyBlock block3 = ^ long (int a, int b) {
return sum + a + b;
};
NSLog(@"block3 = %@", block3);
// block3 = <__NSMallocBlock__: 0x100207100>

因为 ARC 下,编译器帮我们管理内存,所以只要内部调用了外部变量,编译器都会 copy 一份变量到heap 中,并增加引用计数。 所以block2block3的类型都是NSMallocBlock。其余和 MRC 一样。

Tops:

以下情况,block 会拷贝到堆:

  • 当 block 调用 copy 方法时,如果 block 在栈上,会被拷贝到堆上;

  • 当 block 作为函数返回值返回时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;

  • 当 block 被赋值给 _strong id 类型的对象或 block 的成员变量时,编译器自动将 block 作为Block_copy 函数,效果等同于 block 直接调用 copy 方法;

  • 当 block 作为参数被传入方法名带有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 时。这些方法会在内部对传递进来的 block 调用 copy 或 _Block_copy 进行拷贝;

Objective-C Blocks Quiz


Example A

1
2
3
4
5
6
void exampleA() {
char a = 'A';
^{
printf("%cn", a);
}();
}
  • Always works

Explain

This always works. The stack for exampleA doesn’t go away until after the block has finished executing. So whether the block is allocated on the stack or the heap, it will be valid when it is executed.

函数exampleA不会消失,直到 block 运行结束,所以不管 block 在堆中还是栈中,它都可以运行。


Example B

1
2
3
4
5
6
7
8
9
10
11
12
13
void exampleB_addBlockToArray(NSMutableArray *array) {
char b = 'B';
[array addObject:^{
printf("%cn", b);
}];
} void exampleB() {
NSMutableArray *array = [NSMutableArray array];
exampleB_addBlockToArray(array);
void (^block)() = [array objectAtIndex:0];
block();
}
  • Only works in ARC

Explain

Without ARC, the block is an NSStackBlock allocated on the stack of exampleB_addBlockToArray. By the time it executes in exampleB, the the block is no longer valid, because that stack has been cleared.

With ARC, the block is properly allocated on the heap as an autoreleased NSMallocBlock to begin with.

在 MRC中,这里的block 存在栈中,所以在执行exampleB函数的exampleB_addBlockToArray(array);之后,b 变量变得无效,所以[array objectAtIndex:0]不能成功。 在 ARC 中,这个 block 存在堆中,当运行到[array objectAtIndex:0],block 还没被释放,所以可以运行。


Example C

1
2
3
4
5
6
7
8
9
10
11
12
void exampleC_addBlockToArray(NSMutableArray *array) {
[array addObject:^{
printf("Cn");
}];
} void exampleC() {
NSMutableArray *array = [NSMutableArray array];
exampleC_addBlockToArray(array);
void (^block)() = [array objectAtIndex:0];
block();
}
  • Always works

Explain

Since the block doesn’t capture any variables in its closure, it doesn’t need any state set up at runtime. it gets compiled as an NSGlobalBlock. It’s neither on the stack nor the heap, but part of the code segment, like any C function. This works both with and without ARC.

因为这里的 block 是全局的NSConcreteGlobalBlock,所以不管是 ARC 还是 MRC 都是可以用的。


Example D

1
2
3
4
5
6
7
8
9
10
11
12
typedef void (^dBlock)();

dBlock exampleD_getBlock() {
char d = 'D';
return ^{
printf("%cn", d);
};
} void exampleD() {
exampleD_getBlock()();
}
  • Only works in ARC

Explain

This is similar to example B. Without ARC, the block would be created on the stack of exampleD_getBlock and then immediately become invalid when that function returns. However, in this case, the error is so obvious that the compiler will fail to compile, with the error error: returning block that lives on the local stack.

With ARC, the block is correctly placed on the heap as an autoreleased NSMallocBlock.

在 MRC 时,类似于example B,block 会被创建在栈中,所以当block 返回时,马上失效。在这种情况下,错误是显而易见的,编译器无法编译成功。返回一个错误:returning block that lives on the local stack。 在 ARC 中,当自动释放池销毁,block 才失效


Example E

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef void (^eBlock)();

eBlock exampleE_getBlock() {
char e = 'E';
void (^block)() = ^{
printf("%cn", e);
};
return block;
} void exampleE() {
eBlock block = exampleE_getBlock();
block();
}
  • Only works in ARC

Explain

This is just like example D, except that the compiler doesn’t recognize it as an error, so this code compiles and crashes. Even worse, this particular example happens to work fine if you disable optimizations. So watch out for this working while testing and failing in production.

With ARC, the block is correctly placed on the heap as an autoreleased NSMallocBlock.

这题类似于example D。在 MRC 会造成崩溃。


Block_copy & copy

[block copy] 和 Block_copy(block)不等效。block 的赋值不是简单的拷贝,所以要拷贝最好使用 Block_copy()这个宏。

一、根据需求提出问题

  • 请耐心把这篇文章看完,你对 Block 会有更深刻的了解。
  • 这里直接用一个需求来探究循环引用的问题:如果我想在Block中延时来运行某段代码,这里就会出现一个问题,看这段代码:
    - (void)viewDidLoad {
    [super viewDidLoad];
    MitPerson*person = [[MitPerson alloc]init];
    __weak MitPerson * weakPerson = person;
    person.mitBlock = ^{
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [weakPerson test];
    });
    };
    person.mitBlock();
    }

    直接运行这段代码会发现[weakPerson test];并没有执行,打印一下会发现,weakPerson 已经是 Nil 了,这是由于当我们的 viewDidLoad 方法运行结束,由于是局部变量,无论是 MitPerson 和 weakPerson 都会被释放掉,那么这个时候在 Block 中就无法拿到正真的 person 内容了。

  • 按如下方法修改代码:
    - (void)viewDidLoad {
    [super viewDidLoad];
    MitPerson*person = [[MitPerson alloc]init];
    __weak MitPerson * weakPerson = person;
    person.mitBlock = ^{
    __strong MitPerson * strongPerson = weakPerson;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [strongPerson test];
    });
    };
    person.mitBlock();
    }

    这样当2秒过后,计时器依然能够拿到想要的 person 对象。


二、深入探究原理

    • 这里将会对每行代码逐步进行说明
      1、开辟一段控件存储 person 类对象内容,创建 person 强指针。
      MitPerson*person = [[MitPerson alloc]init];
      2、创建一个弱指针 weakPerson 指向person对象内容
      __weak MitPerson * weakPerson = person;
      person.mitBlock = ^{
      3、在 person 对象的 Block 内部创建一个强指针来指向 person 对象,为了保证当计时器执行代码的时候,person 对象没有被系统销毁所以我们必须在系统内部进行一次强引用,并用 GCD 计时器引用 strongPerson,为了保留 person 对象,在下面会对这里更加详细的说明。
      __strong MitPerson * strongPerson = weakPerson;
      dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
      [strongPerson test];
      });
      };
      4、执行 Block 代码
      person.mitBlock();
    • 下面将详细分析一下下面这段代码:
      person.mitBlock = ^{
      __strong MitPerson * strongPerson = weakPerson;
      dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
      [strongPerson test];
      });
      };
    • 首先需要明白一些关于 Block 的概念:
      • 1、默认情况下,block 是放在栈里面的
      • 2、一旦blcok进行了copy操作,block的内存就会被放在堆里面
      • 3、堆立面的block(被copy过的block)有以下现象
        • 1> block内部如果通过外面声明的强引用来使用,那么block内部会自动产生一个强引用指向所使用的对象。
        • 2> block内部如果通过外面声明的弱引用来使用,那么block内部会自动产生一个弱引用指向所使用的对象。
    • 我们进行这段代码的目的:
      • 首先,我们需要在 Block 块中调用,person 对象的方法,既然是在 Block 块中我们就应该使用弱指针来引用外部变量,以此来避免循环引用。但是又会出现问题,什么问题呢?就是当我计时器要执行方法的时候,发现对象已经被释放了。
      • 接下来就是为了避免 person 对象在计时器执行的时候被释放掉:那么为什么 person 对象会被释放掉呢?因为无论我们的person强指针还是 weakPerson 弱指针都是局部变量,当执行完ViewDidLoad 的时候,指针会被销毁。对象只有被强指针引用的时候才不会被销毁,而我们如果直接引用外部的强指针对象又会产生循环引用,这个时候我们就用了一个巧妙的代码来完成这个需求。
      • 首先在 person.mitBlock 引用外部 weakPerson,并在内部创建一个强指针去指向 person 对象,因为在内部声明变量,Block 是不会强引用这个对象的,这也就在避免的 person.mitBlock 循环引用风险的同时,又创建出了一个强指针指向对象。
      • 之后再用 GCD 延时器 Block 来引用相对于它来说是外部的变量 strongPerson ,这时延时器 Block 会默认创建出来一个强引用来引用 person 对象,当 person.mitBlock 作用域结束之后 strongPerson 会跟着被销毁,内存中就仅剩下了 延时器 Block 强引用着 person 对象,2秒之后触发 test 方法,GCD Block 内部方法执行完毕之后,延时器和对象都被销毁,这样就完美实现了我们的需求。
    • 最后再用一张图来阐述各个指针、Block 与对象之间的关系
      黑色代表强引用,绿色代表弱引用
      • 总结:person.mitBlock 中创建 strongPerson 是为了能够使 GCD Block 保存 person 对象,创建 strongPerson 时候使用 weakPerson 是为了避免 mitBlock 直接引用外部强指针变量所造成的循环引用。
        Block 再学习 !
        Block循环引用.png
      •  
上一篇:空间漫游(SAC大佬的测试)


下一篇:String:字符串常量池