Block 是iOS在4.0之后新增的程式语法,在iOS SDK 4.0之后,block应用几乎无处不在。
在其他语言中也有类似的概念称做闭包(closure),比如object C的好兄弟Swift 中闭包(swift 闭包详解)的使用跟 OC的block一样重要。总的来说:
Block是C语言的
Block是一个数据类型
Block 是一个提前准备好的代码,在需要的时候执行
1. block作用:
Block用来封装一段代码,可以在任何时候执行;
- Block可以作为函数参数或者函数的返回值,而其本身又可以带输入参数或返回值。
- 苹果官方建议尽量多用block。在多线程、异步任务 、集合遍历、集合排序、动画转场用的很多
在新的iOS API中block被大量用来取代传统的delegate和callback,而新的API会大量使用block主要是基于以下两个原因:
A. 可以直接在block代码块中写等会要接着执行的代码,直接把block变成函数的参数传入函数中,这是新API最常使用block的地方。
B. 可以存取局部变量,在传统的callback操作时,若想要存取局部变量得将变量封装成结构体才能使用,而block则是可以很方便地直接存取局部变量。
2. Block的定义:
定义时,把block当成数据类型
特点:
1. 类型比函数定义多了一个 ^
2. 设置数值,有一个 ^,内容是 {} 括起的一段代码
(1)基本定义方式
void (^myBlock)() = ^ {
NSLog(@"hello");
};
myBlock();
void (^sumBlock)(int, int) = ^ (int x, int y) {
NSLog(@"%d", x + y);
};
sumBlock(10, 20);
int (^sumBlock2)(int, int) = ^ int (int a, int b) {
return a + b;
};
NSLog(@"%d", sumBlock2(4, 8));
- 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
- 30
- 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
- 30
(2) block 指针
Block Pointer是这样定义的:
回传值 (^名字) (参数列);
int (^square)(int);
square = ^(int a){ return a*a ; };
int result = square(5);
NSLog(@"%d", result);
(3) 用typedef先声明类型,再定义变量进行赋值
typedef int (^MySum)(int,int);
MySum sum = ^(int a,int b)
{
return a + b;
};
(4) block 访问外部变量
但是block使用有个特点,Block可以访问局部变量,但是不能修改:
int sum = 10;
int (^MyBlock)(int) = ^(int num)
{
sum++;
return num * sum;
};
如果要修改就要加关键字 __block (下面详细说明):
__block int sum =10;
(5) block 与函数指针
下面比较下函数指针与block异同:
-
定义函数指针 int (*myFn)();
调用函数指针 (*myFn)(10, 20);
-
定义Block int (^MyBlocks)(int,int);
调用Blocks MyBlocks(10, 20);
3. block访问外部变量
block 访问外部变量有几个特点必须知道:
- block内部可以访问外部变量;
- 默认情况下block内部不能
修改
外面的局部变量;
- 给局部变量加上关键字
_block
,这个局部变量就可以在block内部修改;
block中可以访问外部变量。但是不能修改它
,否则编译错误
。但是可以改变全局变量、静态变量(static)、全局静态变量。
上面的特点是有原因滴:
A. 为何不让修改变量
:这个是编译器决定的。理论上当然可以修改变量了,只不过block捕获的是外部变量的副本,名字一样。为了不给开发者迷惑,干脆不让赋值。道理有点像:函数参数,要用指针,不然传递的是副本(大家想起那个经典的两个数调换值的问题了吧)。
B. 可以修改静态变量的值
。静态变量属于类的,不是某一个变量。所以block内部不用调用cself指针。所以block可以调用。
(1) __block存储类型
通过__block存储类型修饰符, 变量在block中可被修改。__block存储跟register、auto和static存储类型相似(但是之间互斥),用于局部变量。__block变量存储在堆区
,因此,这个block使用的外部变量,将会在栈结束被留下来。
从优化角度考虑,block存储在栈上,如果block被拷贝(通过Block_copy或者copy),变量被拷贝到堆
。因此__block变量的地址就会改变。
__block变量还有两个限制,他们不能是可变数组(NSMutableArray),不能是结构体(structure)。
__block 变量的内部实现要复杂许多,__block 变量其实是一个结构体对象,拷贝的是指向该结构体对象的指针
(2) block访问外部变量
上面已经说过,默认block 访问的外部变量是只读属性的,若要对外部变量进行读写,需要在定义外部变量时加一个 __block, 示例如下:
void demoBlock1()
{
int x = 10;
NSLog(@"定义前 %p", &x);
void(^myBlock)() = ^ {
NSLog(@"%d", x);
NSLog(@"in block %p", &x);
};
NSLog(@"定义后 %p", &x);
x = 20;
myBlock();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
void demoBlock2()
{
__block int x = 10;
NSLog(@"定义前 %p", &x);
void (^myBlock)() = ^ {
x = 80;
NSLog(@"in block %p", &x);
};
NSLog(@"定义后 %p", &x);
myBlock();
NSLog(@"%d", x);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
下面的例子就有点难度了,让我们看下block对指针变量的访问
void demoBlock3()
{
NSMutableString *strM = [NSMutableString stringWithString:@"zhangsan"];
NSLog(@"定义前 %p %p", strM, &strM);
void (^myBlock)() = ^ {
[strM setString:@"lisi"];
NSLog(@"inblock %p %p", strM, &strM);
};
NSLog(@"定义后 %p %p", strM, &strM);
myBlock();
NSLog(@"%@", strM);
}
- 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
- 30
- 31
- 32
- 33
- 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
- 30
- 31
- 32
- 33
上面的例子搞定了,来让我们看下各种类型的变量与block之间的互动:
extern NSInteger CounterGlobal;
static NSInteger CounterStatic;
NSInteger localCounter = 42 ;
__block char localCharacter;
void (^aBlock)( void ) = ^( void )
{
++ CounterGlobal ;
++ CounterStatic ;
CounterGlobal = localCounter;
localCharacter = 'a' ;
};
++localCounter;
localCharacter = 'b' ;
aBlock();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
(3) block 引用成员变量
OC对象,不同于基本类型
,Block会引起对象的引用计数变化。若我们在block中引用到oc的对象,则对象的引用计数器会加1, 不过在对象前 加__block修饰,则参考计数不变。
- 若直接存取实例变量(instance variable),self的参考计数将被加1。
- 若透过变量存取实例变量的值,则变量的参考计数将被加1。
- 在对象前加 __block 则参考计数不会自动加1。
dispatch_async (queue, ^{
doSomethingWithObject (instanceVariable);
});
id localVaribale = instanceVariable;
dispatch_async (queue, ^{
doSomethingWithObject (localVaribale);
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
上面只是简单演示下block引用成员变量,下面我们研究下block引用成员变量时出现的一个经典问题:循环引用
。
在block内部使用成员变量,如下:
@interface ViewController : UIViewController
{
NSString *_string;
}
@end
在block创建中:
_block = ^(){
NSLog(@"string %@", self.string);
};
上面代码中block是会对内部的成员变量进行一次retain, 即self会被retain一次。
对于block 使用 成员变量self.string来说,block内部是直接强引用self的。也就是block持有了self,在这里bock又作为self的一个成员被持有,就会导致循环引用和内存泄露
。
修改方案很简单:
新建一个__block scope
的局部变量,并把self赋值给它,而在block内部则使用这个局部变量来进行取值,上面说过:__block标记的变量是不会被自动retain的。
__block ViewController *controller = self;
_block = ^(){
NSLog(@"string %@", controller.string);
};
4. block 基本使用
当block定义完成后,我们除了可以像使用一般函数的方式来直接调用它以外,还可以有其他妙用,这些灵活的应用才是block最为强大的地方。
(1) block 作为函数参数
我们可以像使用一般函数使用参数的方式将block以函数参数的型式传入函数中,在这种情况下,大多数我们使用block的方式将不会倾向定义一个block,而是直接以内嵌的方式
来将block传入,这也是目前新版SDK中主流的做法
下面的例子中,block本身就是函数参数的一部分
char *myCharacters[ 3 ] = { "TomJohn" , "George" , "Charles Condomine" };
qsort_b (myCharacters, 3 , sizeof ( char *), ^( const void *l, const void *r)
{
char *left = *( char **)l;
char *right = *( char **)r;
return strncmp (left, right, 1 );
}
);
(2) Block当作方法的参数
NSArray *array = [ NSArray arrayWithObjects : @"A" , @"B" , @"C" , @"A" , @"B" , @"Z" , @"G" , @"are" , @" Q" ,nil ];
NSSet *filterSet = [ NSSet setWithObjects : @"A" , @"B" , @"Z" , @"Q" , nil ];
BOOL (^test)( id obj, NSUInteger idx, BOOL *stop);
test = ^ ( id obj, NSUInteger idx, BOOL *stop) {
if (idx < 5 )
{
if ([filterSet containsObject : obj])
{
return YES ;
}
}
return NO ;
};
NSIndexSet *indexes = [array indexesOfObjectsPassingTest :test];
NSLog ( @"indexes: %@" , indexes);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
(3)OC方法中block实例
A. sortedArrayUsingComparator:
NSArray *sortedArray = [array sortedArrayUsingComparator: ^(id obj1, id obj2) {
if ([obj1 integerValue] > [obj2 integerValue])
{
return (NSComparisonResult)NSOrderedDescending;
}
if ([obj1 integerValue] < [obj2 integerValue])
{
return (NSComparisonResult)NSOrderedAscending;
}
return (NSComparisonResult)NSOrderedSame;
}];
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
B. enumerateObjectsUsingBlock
通常enumerateObjectsUsingBlock: 和 (for(… in …)在效率上基本一致,有时会快些。主要是因为它们都是基于 NSFastEnumeration
实现的。快速迭代在处理的过程中需要多一次转换,当然也会消耗掉一些时间. 基于Block的迭代可以达到本机存储一样快的遍历集合. 对于字典同样适用。
-
注意”enumerateObjectsUsingBlock” 修改局部变量时, 你需要声明局部变量为 __block 类型.
-
enumerateObjectsWithOptions:usingBlock: 支持并发迭代或反向迭代,并发迭代时效率也非常高.
-
对于字典而言, enumerateObjectsWithOptions:usingBlock 也是唯一的方式可以并发实现恢复Key-Value值.
示例代码:
NSMutableArray *test = [NSMutableArray array];
for (int i= 0; i < 10000; i++)
{
[test addObject:@"i"];
}
[test enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSLog(@"%@",obj);
}];
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
5. block内存管理
(1)堆(Stack)和栈(Heap)
heap和stack是内存管理的两个重要概念。这里指的是内存的分配区域。
-
stack的空间由操作系统进⾏行分配。
在现代操作系统中,一个线程会分配⼀个stack. 当一个函数被调用,一个stack frame(栈帧)就会被压到stack里。里包含这个函数涉及的参数,局部变量,返回地址等相关信息。当函数返回后,这个栈帧就会被销毁。⽽这一切都是自动的,由系统帮我们进行分配与销毁。对于程序员是透明的,我们不需要手动调度。
.
-
heap的空间需要手动分配。 heap与动态内存分配相关,内存可以随时在堆中分配和销毁。我们需要明确请求内存分配与内存销毁。 简单来说,就
是malloc与free.
(2)Objective-C中的Stack和Heap
首先所有的Objective-C对象都是分配在heap的。 在OC经典的内存分配与初始化:
NSObject *obj = [[NSObject alloc] init];
一个对象在alloc的时候,就在Heap分配了内存空间。 stack对象通常有速度的优势,⽽且不会发生内存泄露问题。那么为什么OC的对象都是分配在heap的呢? 原因在于:
- stack对象的⽣生命周期所导致的问题。例如一旦函数返回,则所在的stack frame就会被摧毁。那么此时返回的对象也 会一并摧毁。这个时候我们去retain这个对象是无效的。因为整个stack frame都已经被摧毁了。简单⽽言就是stack 对象的⽣命周期不适合Objective-C的引用计数内存管理⽅方法。
.
- stack对象不够灵活,不具备足够的扩展性。创建时⻓度已经是固定的,⽽stack对象的拥有者也就是所在的stack frame
我们知道block 在使用@property定义时,官方建议我们使⽤用copy修饰符
@property (nonatomic, copy) void (^completion)(NSString *text);
虽然在ARC时代已经不需要再显式声明了,使用strong是没有问题的,但是仍然建 议我们使⽤copy以显示相关拷贝⾏为。
(3)为什么要使用copy?!
其实Objective-C是有它的Stack object的。那就是block。
在Objective-C语⾔言中,⼀一共有3种类型的block:
- _NSConcreteGlobalBlock 全局的静态block,不会访问任何外部变量。
- _NSConcreteStackBlock 保存在栈中的block,当函数返回时会被销毁。
- _NSConcreteMallocBlock 保存在堆中的block,当引⽤用计数为0时会被销毁。
这⾥我们主要基于内存管理
的角度对它们进行分类。
NSConcreteGlobalBlock,这种不捕捉外界变量的block是不需要内存管理的,这种block不存在于Heap或是Stack⽽是作为代码片段存在,类似于C函数。
NSConcreteStackBlock,需要涉及到外界变量的block在创建的时候是在stack上⾯分配空间的,也就是⼀旦所在函数返回,执行弹栈,则会被摧毁。这就导致内存管理的问题,如果我们希望保存这个block或者是返回它,如果没有做进⼀步的copy处理,则必然会出现问题。
举个栗子,在手动管理引⽤计数时,如果在exampleD_getBlock方法返回block 时没有执行[[block copy] autorelease]
的操作,则方法执行完毕后,block就会被销毁, 返回block是无效的。
typedef void (^dBlock)();
dBlock exampleD_getBlock() {
char d = 'D';
return ^{
printf("%c\n", d);
};
}
void exampleD()
{
exampleD_getBlock();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
NSConcreteMallocBlock,因此为了解决block作为Stack object的这个问题,我们最终需要把它拷⻉到堆上来。
拷贝到堆后,block的⽣命周期就与⼀般的OC对象⼀样了,我们通过引用计数来对其进行内存管理。
现在我们知道为么么要Copy了吧-_-
block在创建时是stack对象,如果我们需要在离开当前函数仍能够使用我们创建的block。我们就需要把它 拷⻉到堆上以便进行以引用计数为基础的内存管理。
在ARC模式下,系统帮助我们完成了copy的⼯作。在ARC下,即使你声明的修饰符是strong,实际上效果是与声明为copy一样的。 因此在ARC情况下,创建的block仍然是NSConcreteStackBlock类型,只不过当block被引用或返回时
,ARC帮助我们完成了copy和内存管理的工作。
总结
在ARC下,我们可以将block看做⼀一个正常的OC对象,与其他对象的内存管理没什么不同。MRC下要使用 Block_copy()和 Block_release 来管理内存。
(4)再来一个栗子
上面讲到ARC下, block在被引用或返回时类型会由NSConcreteStackBlock转换为 NSConcreteHeapBlock,那在MRC环境下该怎么办呢。
block在创建的时候,它的内存是分配在栈(stack)上,而不是在堆(heap)上。
我们在viewDidLoad中创建一个_block:
- (void)viewDidLoad
{
[superviewDidLoad];
int number = 1;
_block = ^(){
NSLog(@"number %d", number);
};
}
并且在一个按钮的事件中调用了这个block:
- (IBAction)testDidClick:(id)sender {
_block();
}
此时如果按了按钮之后就会导致程序崩溃,解决这个问题的方法很简单
在创建完block的时候需要调用 Block_copy
函数。它会把block从栈上移动到堆上,那么就可以在其他地方使用这个block了。Block_copy
实际上是一个宏,如下:
#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
使用后,使用 Block_release,从堆中释放掉
修改代码如下:
_block = ^(){
NSLog(@"number %d", number);
};
_block = Block_copy(_block);
同理,特别需要注意的地方就是在把block放到集合类当中去的时候,如果直接把生成的block放入到集合类中,是无法在其他地方使用block,必须要对block进行copy。示例如下:
[array addObject:[[^{ NSLog(@"hello!"); } copy] autorelease]];
Q:为什么不使用简单的copy方法 而是 Blockcopy呢?
因为blcok是复杂的匿名函数,简单的copy在有些时候不能实现准确的copy,详细就要看各自的C源码了
6. 视图控制器反向传值
使用Block的地方很多,其中传值只是其中的一小部分,下面介绍Block在两个界面之间的传值:
先说一下思想:
首先,创建两个视图控制器,在第一个视图控制器中创建一个UILabel和一个UIButton,其中UILabel是为了显示第二个视图控制器传过来的字符串,UIButton是为了push到第二个界面。
第二个界面的只有一个UITextField,是为了输入文字,当输入文字,并且返回第一个界面的时候,当第二个视图将要消失的时候,就将第二个界面上TextFiled中的文字传给第一个界面,并且显示在UILabel上。
其实核心代码就几行代码:
在第二个视图控制器的.h文件中定义声明Block属性
typedef void (^ReturnTextBlock)(NSString *showText);
@interface TextFieldViewController : UIViewController
@property (nonatomic, copy) ReturnTextBlock returnTextBlock;
- (void)returnText:(ReturnTextBlock)block;
@end
第一行代码是为要声明的Block重新定义了一个名字
ReturnTextBlock
这样,下面在使用的时候就会很方便。
第三行是定义的一个Block属性
第四行是一个在第一个界面传进来一个Block语句块的函数,不用也可以,不过加上会减少代码的书写量
实现第二个视图控制器的方法
- (void)returnText:(ReturnTextBlock)block {
self.returnTextBlock = block;
}
- (void)viewWillDisappear:(BOOL)animated {
if (self.returnTextBlock != nil) {
self.returnTextBlock(self.inputTF.text);
}
}
其中inputTF是视图中的UITextField。
第一个方法就是定义的那个方法,把传进来的Block语句块保存到本类的实例变量returnTextBlock(.h中定义的属性)中,然后寻找一个时机调用,而这个时机就是上面说到的,当视图将要消失的时候,需要重写:
- (void)viewWillDisappear:(BOOL)animated;
方法。
在第一个视图中获得第二个视图控制器,并且用第二个视图控制器来调用定义的属性
如下:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
TextFieldViewController *tfVC = segue.destinationViewController;
[tfVC returnText:^(NSString *showText) {
self.showLabel.text = showText;
}];
}
可以看到代码中的注释,系统告诉我们可以用
[segue destinationViewController]
来获得新的视图控制器,也就是我们说的第二个视图控制器。
这时候上面(第一步中)定义的那个方法起作用了,如果你写一个[tfVC return Text按回车 ,系统会自动提示出来一个:
tfVC returnText:<
的东西,我们只要在焦点上回车,就可以快速创建一个代码块了,大家可以试试。这在写代码的时候是非常方便的。
面试题:
- __block什么时候用
当需要在block 中修改外部变量时使用,当需要访问内部成员变量时。
2.在block里面, 对数组执行添加操作, 这个数组需要声明成 __block吗?
当然不需要,因为数组可以理解为指针,在block中对数组进行添加操作,只是改变了指针指向的值,而没有修改外部数组地址,详细参见block访问成员变量示例3
3.在block里面, 对NSInteger进行修改, 这个NSInteger是否需要声明成__blcok
必须需要,NSInteger -> typedef long NSInteger; 这货披着OC的外衣,其实就是一个基本类型,基本类型在没有static 等的保护下,当然需要__block
悄悄告诉你哦,block在iOS的面试中是非常重要的,如果你能把上面讲解的内容理解了,那么就仰天长啸出门去了。
参考
- http://www.cocoachina.com/ios/20120514/4247.html
使用block开发遇到的问题
-
http://blog.csdn.net/hherima/article/details/3858610
这篇block 博客系列从C源码的角度详细分析了blcok原理
-
http://mobile.51cto.com/iphone-446829.htm
- http://blog.devtang.com/blog/2013/07/28/a-look-inside-blocks/
巧叔的谈Objective-C Block的实现