# iOS基础 # block知识整理

什么是block

在iOS 4.0之后,block横空出世,它本身封装了一段代码并将这段代码当做变量,通过block()的方式进行回调。

自从block出现之后,很多API都开始采用这样的结构,由此可见,block确实有许多优势存在,这里将一些简单用

法总结如下:

1、如何声明一个block变量

我们通过^符号来声明block类型,形式如下:

(1) 标准声明与定义
return_type (^blockName)(var_type) = ^return_type (var_type varName) { // ... };
blockName(var);

(2) 当返回类型为void
void (^blockName)(var_type) = ^void (var_type varName) { // ... };
blockName(var);
可省略写成
void (^blockName)(var_type) = ^(var_type varName) { // ... };
blockName(var);

(3) 当参数类型为void
return_type (^blockName)(void) = ^return_type (void) { // ... };
blockName();
可省略写成
return_type (^blockName)(void) = ^return_type { // ... };
blockName();

(4) 当返回类型和参数类型都为void
void (^blockName)(void) = ^void (void) { // ... };
blockName();
可省略写成
void (^blockName)(void) = ^{ // ... };
blockName();

(5) 匿名Block
Block实现时,等号右边就是一个匿名Block,它没有blockName,称之为匿名Block:
^return_type (var_type varName)
{ //... };
2.2 typedef简化Block的声明
利用typedef简化Block的声明:

声明
typedef return_type (^BlockTypeName)(var_type);

例子1:作属性
//声明 typedef void(^ClickBlock)(NSInteger index); //block属性 @property (nonatomic, copy) ClickBlock imageClickBlock;

例子2:作方法参数
//声明 typedef void (^handleBlock)(); //block作参数 - (void)requestForRefuseOrAccept:(MessageBtnType)msgBtnType messageModel:(MessageModel *)msgModel handle:(handleBlock)handle{
  ...

2、block的常见用法

1 局部位置声明一个Block型的变量

位置
return_type (^blockName)(var_type) = ^return_type (var_type varName) { // ... };
blockName(var);

例子
void (^globalBlockInMemory)(int number) = ^(int number){ 
    printf("%d \n",number);
};
globalBlockInMemory(90);

2 @interface位置声明一个Block型的属性

位置
@property(nonatomic, copy)return_type (^blockName) (var_type);

例子
//按钮点击Block @property (nonatomic, copy) void (^btnClickedBlock)(UIButton *sender);

3 在定义方法时,声明Block型的形参

用法
- (void)yourMethod:(return_type (^)(var_type))blockName;

例子
- (void)addClickedBlock:(void(^)(id obj))clickedAction;

4 在调用如上方法时,Block作实参

例子
- (void)addClickedBlock:(void(^)(id obj))clickedAction{
    self.clickedAction = clickedAction; // :先判断当前是否有交互事件,如果没有的话。。。所有gesture的交互事件都会被添加进gestureRecognizers中 
    if (![self gestureRecognizers]) { 
        self.userInteractionEnabled = YES; // :添加单击事件 
        UITapGestureRecognizer *tap = 
        [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap)];
        [self addGestureRecognizer:tap];
    }
}

- (void)tap{ 
    if (self.clickedAction) { 
        self.clickedAction(self);
    }
}

3、block的少见用法

1 Block的内联用法

这种形式并不常用,匿名Block声明后立即被调用:

^return_type (var_type varName)
{ //... }(var);

2 Block的递归调用

Block内部调用自身,递归调用是很多算法基础,特别是在无法提前预知循环终止条件的情况下。注意:由于Block内部引用了自身,这里必须使用__block避免循环引用问题。

__block return_type (^blockName)(var_type) = [^return_type (var_type varName)
{ if (returnCondition)
    {
        blockName = nil; return;
    } // ... // 【递归调用】 blockName(varName);
} copy];

【初次调用】
blockName(varValue);

3 Block作为返回值

方法的返回值是一个Block,可用于一些“工厂模式”的方法中:

用法:
- (return_type(^)(var_type))methodName
{ return ^return_type(var_type param) { // ... };
}

4、block中访问对象的微妙关系

1、如果你在一个block块中仅仅访问对象,而不是对他进行修改操作,是没有任何问题的:

- (void)viewDidLoad {
    [super viewDidLoad];
    int tem=2;
    block1 = ^(int a,int b){
        int count= tem+1;
        return count;
    };
    NSLog(@"%d",block1(1,1));
}

2、如果在block块中直接修改变量,编译器会报错:

 block1 = ^(int a,int b){
        tem+=1;
        return tem+1;
 };
为什么会出现这样的情况,根据猜测,可能是block内部将访问的变量都备份了一份,如果我们在内部修改,外部的变量并不会被修改,我们可以通过打印变量的地址来证明这一点:

- (void)setUp {
    int i = 1;
     NSLog(@"%p + out",&i);
    self.block = ^(int a,int b){
        NSLog(@"%p + inside",&i);
        return 1;
    };
    self.block(2,3);
}
打印结果如下:
2017-03-06 11:11:07.657 SwiftDemo[7890:9947536] 0x7fff535bba7c + out
2017-03-06 11:11:07.657 SwiftDemo[7890:9947536] 0x7fff535bba78 + inside

可以看出,变量的地址已经改变。

3、block在捕获变量的时候只会保存变量被捕获时的状态(对象变量除外),之后即便变量再次改变,block中的值也不会发生改变。

4、如何对外部变量进行修改,__block 做了什么

为了可以在block块中访问并修改外部变量,我们常会把变量声明成__block类型,通过上面的原理,可以发现,其实这个关键字只做了一件事,如果在block中访问没有添加这个关键字的变量,会访问到block自己拷贝的那一份变量,它是在block创建的时候创建的,而访问加了这个关键字的变量,则会访问这个变量的地址所对应的变量。我们可以通过代码来证明:

- (void)setUp {
    __block int i = 1;
     NSLog(@"%p + out",&i);
    self.block = ^(int a,int b){
        NSLog(@"%p + inside",&i);
        return 1;
    };
    self.block(2,3);
}

结果:
2017-03-06 11:17:16.438 SwiftDemo[8088:9950943] 0x7fff51ef0a78 + out
2017-03-06 11:17:16.439 SwiftDemo[8088:9950943] 0x7fff51ef0a78 + inside

由此,我们可以理解,如果block中操作的对象是指针,那么直接可以进行修改,这包括OC对象,如果不是,则需要用__block关键字修饰。

5、关于引用计数

在block中访问的对象,会默认retain:

- (void)setUp {
    UIImage *image = [UIImage new];
    NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)(image)));
    
    self.block = ^(int a,int b){
        NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)(image)));
    };
    self.block(2,3);
}
结果如下:
2017-03-06 11:27:47.856 SwiftDemo[8494:9958586] 1
2017-03-06 11:27:47.856 SwiftDemo[8494:9958586] 3

而添加__block的对象不会被retain;

2017-03-06 13:41:02.850 SwiftDemo[8747:9968324] 1
2017-03-06 13:41:02.850 SwiftDemo[8747:9968324] 1

注意:如果我们访问类的成员变量,或者通过类方法来访问对象,那么这些对象不会被retain,而类对象会被retain,最常见的是在block中直接使用self会有warning:

! Capturing 'self' strongly in this block is likely to lead to a retain cycle

循环引用

block在iOS开发中被视作是对象,因此其生命周期会一直等到持有者的生命周期结束了才会结束。另一方面,由于block捕获变量的机制,使得持有block的对象也可能被block持有,从而形成循环引用,导致两者都不能被释放。导致内存泄露。所谓的内存泄露就是本应该释放的对象,在其生命周期结束之后依旧存在。

解决办法

系统提供给我们__weak的关键字用来修饰对象变量,声明这是一个弱引用的对象
__weak typeof(*&self) weakSelf = self;


所有的Block里面的self必须要weak一下?

很显然答案不都是,有些情况下是可以直接使用self的,比如调用系统的方法:

[UIView animateWithDuration:0.5 animations:^{ 
    NSLog(@"%@", self);
}];
因为这个block存在于静态方法中,虽然block对self强引用着,但是self却不持有这个静态方法,所以完全可以在block内部使用self。

5、block与内存管理

根据Block在内存中的位置分为三种类型:

NSGlobalBlock

NSGlobalBlock是位于全局区的block,它是设置在程序的数据区域(.data区)中。
只用到全局变量、静态变量的block。生命周期为程序的生命周期,不持有对象。
static NSString *string = @"hello word";
typedef void(^Block)();
Block block = ^{
    NSLog(@"%@",string);
};
NSLog(@"%@",block);
//<__NSGlobalBlock__: 0x10ec4c090>


NSStackBlock

NSStackBlock是位于栈区,超出变量作用域,栈上的Block以及 __block变量都被销毁。
只用到外部局部变量、成员属性变量、没有强指针引用的block,copy操作会复制到堆上

@property (nonatomic, strong) NSString *str;

int multiplier = 7;
NSLog(@"%@",^(int num) {
    NSLog(@"%@",_str);
    return num * multiplier;
});
//<__NSStackBlock__: 0x7fff5f8989e8>




NSMallocBlock

NSMallocBlock是位于堆区,在变量作用域结束时不受影响。
有强指针引用或者调用copy方法复制到堆中的block,生命周期由程序员控制

- (void(^)())blockReturn {
NSString *strBlock = @"NSMallocBlock";
return ^(){
    NSLog(@"%@",strBlock);
};
}
NSLog(@"%@",[self blockReturn]);
//<__NSMallocBlock__: 0x600000243f90>


在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。这一句验证怎么不对呢

Block的复制

  • 在全局block调用copy 什么也不做
  • 在栈上调用copy那么复制到堆上
  • 在堆上调用copy block 引用计数增加

6、block的实质是什么

一个对象,一个结构体

有isa指针指向自己的类(global malloc stack),

有desc结构体描述block的信息,forwarding指向自己或堆上自己的地址,

如果block对象截获变量,这些变量也会出现在block结构体中。

最重要的block结构体有一个函数指针,指向block代码块。block结构体的构造函数的参数,包括函数指针,描述block的结构体,自动截获的变量(全局变量不用截获),引用到的block变量。(__block对象也会转变成结构体)

block代码块在编译的时候会生成一个函数,函数第一个参数是前面说到的block对象结构体指针。执行block,相当于执行block里面__forwarding里面的函数指针。

__block_impl结构体定义

  //  结构体 __block_impl 的声明
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

实质的更多描述

1、默认block的类型是NSGlobalBlock,一旦在block中引用自动变量(包括OC对象),无论是否修改,block类型变为NSMallocBlock。

2、默认情况下,在block内不能对截获的自动变量(或OC对象)进行修改,编译器会报错。

3、默认情况下,在block中引用的自动变量或OC对象(包括调用OC对象方法),block中的自动变量地址不同,而引用前后自动变量的地址一致

4、在自动变量(或OC对象)前添加__block,无论是否修改自动变量,引用后与block自动变量的地址一致,而且与引用前不一致。

另外,需要注意的一点是,block目前没有实现对C语言数组的截获

7、block和函数指针的理解

相似点:

1、函数指针和Block都可以实现回调的操作,声明上也很相似,实现上都可以看成是一个代码片段。

2、函数指针类型和Block类型都可以作为变量和函数参数的类型。(typedef定义别名之后,这个别名就是一个类型)

不同点:

1、函数指针只能指向预先定义好的函数代码块(可以是其他文件里面定义,通过函数参数动态传入的),函数地址是在编译链接时就已经确定好的。

2、Block本质是Objective-C对象,是NSObject的子类,可以接收消息。

3、函数里面只能访问全局变量,而Block代码块不光能访问全局变量,还拥有当前栈内存和堆内存变量的可读性(当然通过__block访问指示符修饰的局部变量还可以在block代码块里面进行修改)。

4、从内存的角度看,函数指针只不过是指向代码区的一段可执行代码,而block实际上是程序运行过程中在栈内存动态创建的对象,可以向其发送copy消息将block对象拷贝到堆内存,以延长其生命周期。

推荐阅读更多精彩内容