Block分析

Block分析




Block是C语言的扩展。可以用一句话来表示Blocks的扩展功能:带有自动变量(局部变量)的匿名函数。其实他就是一个函数指针,用法和C语言函数一模一样,也可以传入参数,也有返回值。
当然和函数相比,Blocks的功能强大很多,同时也会复杂很多,与函数有一些区别,主要表现在这些方面:

- 语法上的区别
- 是一个匿名指针
- 截获自动变量
- 内存管理与释放





1、Block语法
前面提到过Block其实是一个匿名函数指针,那我们知道在C语言中可以将一个函数的地址赋值给函数指针类型变量中。像这样:

intfunctionName(intcount){
    return count;
}
 int (*block)(int) = &functionName;

而声明一个Block和上面这个函数指针就十分类似了,他看起来是这样的:
 int (^blo)(int);
和C语言中的函数指针相比唯一的区别就是将“*”替换成了“^”。那这种Block类型变量和一般的C语言变量的用法是完全一模一样的,他有以下用法:
1、自动变量
2、函数参数
3、静态变量
4、静态全局变量
5、全局变量 


我们来对一个Block来进行赋值,他应该是这样的:
int (^blo)(int) = ^(int count){
        return count;
 };
”^“这个符号表明这是一个Block,而后面的括号中则是包含着参数,在花括号你可以进行一些操作同时根据需要来确定时候返回。
你可以像使用一个C语言函数一样来使用Block:
    int count = blo(10);

他同样会有参数传入,同样有返回值,看起来和C语言函数并没有太大的区别。

然而Block比C语言函数强大太多了,比如说Block可以作为函数参数。




2、作为函数参数
我们可以这样来声明一个OC的方法:
-(void)functionName:(void(^)(int count))hander;

然后这样来调用这个方法:
[self functionName:^(int count) {
        NSLog(@"%d",count);
 }];
我们可以看到hander变成了我们的回调,事实上apple的大量api接口也是这么设计的。functionName在这个方法中也许我们进行了大量的计算,也许开辟了很多线程,等待了很长的时间,但是所有的这些复杂过程对于用户来说(方法的使用者)来说都是不关心的。用户关心的也许只有在hander中返回的”count“这个参数。
我们可以把这个方法写的更加漂亮一些,我们添加一个Block类型变量,我们会使用到C语言中的typedef。
typedef void(blo)(int count);
我们给我们的这个带有“count”参数的闭包起了一个blo的别名,所以在接下来的函数声明中我们就可以使用这个blo来代替原本的参数类型,像这样:
-(void)functionName:(blo)hander;

3、截获自动变量
     我们来看下面这段代码:
    NSString *name = @"I am Eric";
    void (^blo)(void) = ^{

        NSLog(@“@“,name);
    
    };

    name = @"I am Strong";


我们在Block中输出字符串,然而字符串在Block后面被修改过 ,实际运行后,控制台输出的是:“I am Eric
那这个就是Block的截获变量值,简单的来说在编译Block时,Block会保存(截获)其中使用到的变量,在Block中不管这些变量在其后的运行中是否会被修改,Block中记录的值永远不会被改变,那么这个就是Block的截获自动变量。



4、__block说明符
自动变量截获只能获取变量在瞬间的值,而不能对他进行修改,当我们尝试去修改截获的自动变量值的时候,编译器会报错,我们来看下下面的代码:
 int var = 1;
   
void(^blockName)(void) = ^{
        var =
2;
    };


当你在工程中这么做的时候,编译器会发出以下错误:
Variable is not assignable (missing __block type specifier)

其实这个报错就是在提醒我们,当我们想在Block中修改变量的值的时候,我们需要给变量加上 “__Block”修饰符,比如像这样:

 __block int var = 1;
   
void(^blockName)(void) = ^{
        var =
2;
    };

使用附有__Block说明符的自动变量可以在Block中赋值,该变量称为__block变量。

那么是不是所有在Block中变更对象中都需要加上__Block说明符呢?并不是这样的。比如下面的代码并不会发生任何问题:

 NSMutableArray *arr = [@[] mutableCopy];
   
void(^blockName)(void) = ^{
        [arr
addObject:@"object"];
    };
我们截获的变量是一个NSMutableArray类型的,虽然对一个OC对象进行赋值会发生错误,但是如果我们仅仅是在Block中对OC对象进行操作的话是没有问题的,比如以上操作就是对一个可变数组进行操作,而没有执行赋值,当然下面的代码会发生错误。
 NSMutableArray *arr = [@[] mutableCopy];
   
void(^blockName)(void) = ^{
        arr = [@[] mutableCopy];
    };
在上面这种情况下,你同样需要给arr变量加上__block修饰符。
如果用C语言指针来解释这个问题的话,那么就是你不能在Block中改变变量的指针,然而如果仅仅是如果想上面那段代码那样对变量进行操作,并不改变其指针指向的话,那么并不会发生问题。

谈到C语言指针,其实在Block中如果用到了C语言数组那么你一定要格外小心。
const char text[] = "StrongX";
   
void(^block)(void) = ^{
       
printf(@"%c",text[2]);
    };

像上面这段代码,编译器会发出以下错误:
1、 Cannot refer to declaration with an array type inside block
2、 Implicit conversion of an Objective-C pointer to 'const char *' is disallowed with ARC

这是因为在现在的Block中,截获自动变量的方法并没有实现对C语言的截获。这时,使用指针可以解决该问题.
const char *text = "StrongX";
   
void(^block)(void) = ^{
        printf("%c",text[2]);
   };




5、Block存储域
存储域一共分为三种:
· _NSConcreteStackBlock
. _NSConcreteGlobalBlock
._NSConcreteMallocBlock
这三种存储域分别对应“栈存储域”、“全局存储域”、“堆存储域”。
Block与OC变量并不相同,并不全是存储在“栈存储域”的

Block存储在“全局作用域当中”:

当我们声明一个全局的Block的时候,比如这样:
void (^block)(void)=^{NSLog(@"I am a global block");};
- (void)viewDidLoad {
···

那么这个时候Block就将被存储在_NSConcreteGlobalBlock中,因为当这个时候Block中无法对自动变量进行截获,也就是Block的内容将不依赖于运行时的状态,因此将Block放在“全局作用域”中将是最为合适的。
事实上,只要Block的内容不依赖于运行时的环境,也就是不对自动变量进行截获,那么不管Block的声明实现位置在哪里,这个Block都将被存储在“全局作用域”当中。

Block存储在“堆作用域”当中:
当我们将Block作为回调使用时,我们将发现Block超出了块作用域的时候仍将可以被使用,比如在网络回调中:
-(void)getDataFromHttp{
    [Http requestNetWork:^(NSDictionary *result){
       
    }];
}

我们经常会使用类似上面这种方式来进行网络数据请求,在Block中对请求返回数据进行处理,然而这种情况下,当网络数据返回时,Block已经超出了他的作用域,包括如果在Block中对变量进行截获,那么在数据返回时也可以对自动变量进行操作,这是因为在这种情况,Block将被复制在“堆存储域”中,包括Block中的自动变量也将会被拷贝到堆存储域当中。

还有一种情况就是当将Block作为函数返回值返回时,Block同样会被拷贝到“堆存储域”中,再来进行返回。

在大多数情况下,XCode会自动帮我们来判断Block在什么情况下需要被拷贝到“堆存储域”中,但是在某些情况下我们需要手动来进行这个过程,把Block从“栈”拷贝到“堆”中,我们需要使用“copy”命令。




6、Block循环作用域
前面已经提到过Block在引用自动变量的时候将会把变量从栈中拷贝到堆中,所以当拷贝__strong变量时,十分容易引起循环引用,从而造成内存泄漏。
下面这段代码就将会引起循环引用:

@interface ViewController : UIViewController


typedef void(^myBlo)(void);

@property (nonatomic, strong) myBlo Block;

….
self.Block = ^{
        NSLog(@"%@,self.string);
    };
   
self.Block();

我们可以知道ViewController持有一个变量Block,然而在Block中再次截获了self,也就是Block持有self,ViewController的释放需要Block来释放self,而Block同样需要ViewController释放才会释放,这样就是一个十分标准的循环引用,解决这个问题我们可以使用__weak说明符。

__weak ViewController *wself = self;
self.Block = ^{
        NSLog(@"%@,wself.string);
    };
   
self.Block();

当我们使用__weak说明符以后,Block不再持有self,于是也就打破了循环引用。

事实上并不需要在Block中显示的出现self以后才会发生循环引用,在下面这种情况下同样会发生循环引用:

@implementation ViewController
{
    NSString *string;
}
self.Block = ^{
        NSLog(@"%@,string);
    };
self.Block();

以上Block中并没有出现self,然而在这种情况下也会发生训话引用,原因是以上虽然没有使用get方法来获取变量,直接通过内存地址来获取变量的情况下等同于以下代码:

@implementation ViewController
{
    NSString *string;
}
self.Block = ^{
        NSLog(@"%@,self->string);
    };
self.Block();

这样我们应该就能很明白为什么同样会发生循环应用,解决这样的循环引用,我们同样可以使用__weak说明符:
@implementation ViewController2
{
    NSString *string;
}
__weak NSString *wstr = string;
self.Block = ^{
        NSLog(@"%@,wstr);
    };
self.Block();

有一点值得注意的是,就算你的Block在运行时没有被调用,但是在Block中发生了循环引用,那么同样会发生内存泄漏,原因也是十分简单,Block讲自动变量拷贝到“堆存储域”这个动作是在编译时期完成的,所以就算你没有调用Block,XCode也已经在编译时期将自动变量拷贝到了“堆存储域”当中了。


在上文中为了解决Block的循环引用问题,我们使用了__weak说明符,事实上解决循环应用的问题还有另外一种方式。为了解决循环引用我们必须打破双方其中一方的引用,所以我们使用__weak说明符,但是下面的代码同样可以达到相同的目的:

@interface myObject : NSObject

typedef void(^myBlo)(void);

@property (nonatomic, strong) myBlo block;
@end
…………….
#import "myObject.h"

@implementation myObject
{
   
NSString *string;
}
-(
void)dealloc{
   
NSLog(@"myObject release");
}
-(
id)init{
   
self = [super init];
   
   
string = @"StrongX";
   
   
self.block = ^{
       
NSLog(@"%@",string);
    };
  
    return self;
}
@end

我们声明了一个名为myObject的类,这个类中的Block发生了循环引用,如果我们声明了这个类的一个实列对象,那么这么对象因为循环引用而不会被释放。
@property (nonatomic, strong) myObject *Object;

    _Object = [[myObject alloc]init];

当我们声明一个Object的myObject类后,Object就已经发生了内存泄漏,但是如果我们在合适的位置来释放Block,那么就可以解决这个问题:
    _Object.block = nil;
当我们将Block置空以后,block就失去了对Object的引用,所以这个时候就不会在发神循环引用了。

当然使用这种直接将Block置空的方式是十分危险的,因为你改变了Block初始化的值,可能后面的代码运行结果就不是你所预料的了。所以在什么时候将Block置空将是非常关键的一个问题。