Block的本质与使用

1、block的基本概念及使用

  blcok是一种特殊的数据结构,它可以保存一段代码,等到需要的时候进行调用执行这段代码,常用于GCD、动画、排序及各类回调。

  Block变量的声明格式为: 返回值类型(^Block名字)(参数列表);

  //声明一个没有传参和返回值的blcok
    void(^myBlock1)(void) ;
    //声明一个有两个传参没有返回值的blcok 形参变量名称可以省略,只留有变量类型即可
    void(^myBlock2)(NSString *name,int age);
    //声明一个没有传参但有返回值的blcok
    NSString *(^myBlock3)();
    //声明一个既有返回值也有参数的blcok
    int(^myBlock4)(NSString *name);

  block的赋值: Block变量 = ^(参数列表){函数体};

//如果没有参数可以省略写(void)
    myBlock1 = ^{
        NSLog(@"hello,word");
    };
    
    myBlock2 = ^(NSString *name,int age){
        NSLog(@"%@的年龄是%d",name,age);
    };
    
    //通常情况下都将返回值类型省略,因为编译器可以从存储代码块的变量中确定返回值的类型
    myBlock3 = ^{
        return @"小李";
    };
    
    myBlock4 = ^(NSString *name){
        NSLog(@"根据查找%@的年龄是10岁",name);
        return 10;
    };

  当然也可以直接在声明的时候就赋值: 返回值类型(^Block名字)(参数列表) = ^(参数列表){函数体};

int(^myBlock5)(NSString *address,NSString *name) = ^(NSString *address,NSString *name){
         NSLog(@"根据查找家住%@的%@今年18岁了",address,name);
         return 18;
    };

  blcok的调用:Block名字();

    //没有返回值的话直接 Block名字();调用
    myBlock1();
    
    //有参数的话要传递相应的参数
    myBlock2(@"校花",12);
    
    //有返回值的话要对返回值进行接收
    NSString *name = myBlock3();
    NSLog(@"%@",name);
    
    //既有参数又有返回值的话就需要即传参数又接收返回值
    int age = myBlock5(@"河北村",@"大梨");
    NSLog(@"%d",age);

  在实际使用Block的过程中,我们可能需要重复地声明多个相同返回值相同参数列表的Block变量(blcok内部执行的代码功能不一样),如果总是重复地编写一长串代码来声明变量会非常繁琐,

所以我们可以使用typedef来定义Block类型。

#import "ViewController.h"
typedef void(^commentBlock)(NSString *name,int age);
@interface ViewController ()
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    commentBlock commentBlock1 = ^(NSString *name,int age){
        //这里的操作是将age的name从数据库中筛选出来
    };
    commentBlock commentBlock2 = ^(NSString *name,int age){
        //这里的操作是将age的name添加到数据库
    };
    commentBlock commentBlock3 = ^(NSString *name,int age){
       //这里的操作是将age的name从数据库中删除
    };
    commentBlock1(@"li",12);
    commentBlock2(@"dong",19);
    commentBlock3(@"mi",8);
    
}
    这样可以减少重复代码,避免重复用void(^commentBlock)(NSString *name,int age);声明blcok

  上面,只是讲到了blcok的一些基本使用,那么在我们实际开发中,block是怎么应用的呢?其实在实际开发中把block作为方法的参数是一种比较常见的用法,比如我们用到的网络请求工具.

比如,我们举一个block作为参数的小例子:

 1 #import "ViewController.h"
 2 typedef void(^BtnBlock)(void);
 3 @interface ViewController ()
 4 @property(nonatomic,weak)BtnBlock currentBlcok;
 5 @end
 6 
 7 @implementation ViewController
 8 
 9 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
10     [self alertWithBlcok:^{
11         NSLog(@"用户点击了确定  在这里可以执行对应的操作");
12     }];
13 //    [self alertWithBlcok:nil];
14 }
15 - (void)alertWithBlcok:(BtnBlock)block{
16     _currentBlcok = block;
17     //低层最大的背景View
18     UIView *alertBgView = [[UIView alloc]initWithFrame:self.view.bounds];
19     alertBgView.tag = 99;
20     alertBgView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.73];
21     [self.view addSubview:alertBgView];
22     
23     //中间的View
24     UIView *alertCenterView = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 240, 140)];
25     alertCenterView.clipsToBounds = YES;
26     alertCenterView.layer.cornerRadius = 10;
27     alertCenterView.center = alertBgView.center;
28     alertCenterView.backgroundColor = [UIColor redColor];
29     [alertBgView addSubview:alertCenterView];
30 
31     
32     //取消按钮
33     UIButton *cancelBtn = [[UIButton alloc]initWithFrame:CGRectMake(0, 100, alertCenterView.frame.size.width/2, 40)];
34     [cancelBtn setTitle:@"取消" forState:UIControlStateNormal];
35     cancelBtn.titleLabel.font = [UIFont systemFontOfSize:15];
36     [cancelBtn setTitleColor:[UIColor colorWithRed:51/255.0 green:51/255.0 blue:51/255.0 alpha:1.0] forState:UIControlStateNormal];
37     [cancelBtn addTarget:self action:@selector(dismissAlertView) forControlEvents:UIControlEventTouchUpInside];
38     [alertCenterView addSubview:cancelBtn];
39     
40     //短的分割线
41     UIView *shortView = [[UIView alloc]initWithFrame:CGRectMake(alertCenterView.frame.size.width/2, 110, 1, 20)];
42     shortView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.53];
43     [alertCenterView addSubview:shortView];
44     
45     //取消按钮
46     UIButton *continueBtn = [[UIButton alloc]initWithFrame:CGRectMake(alertCenterView.frame.size.width/2, 100, alertCenterView.frame.size.width/2,40)];
47     [continueBtn setTitle:@"确定" forState:UIControlStateNormal];
48     [continueBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
49     continueBtn.titleLabel.font = [UIFont systemFontOfSize:15];
50     [continueBtn addTarget:self action:@selector(buttonAction) forControlEvents:UIControlEventTouchUpInside];
51     [alertCenterView addSubview:continueBtn];
52 }
53 - (void)dismissAlertView{
54     [[self.view viewWithTag:99] removeFromSuperview];
55 }
56 -(void)buttonAction{
57     if (_currentBlcok) {
58         _currentBlcok();
59     }
60 }
弹框点击

用户点击确定按钮执行的操作可以通过block先封存起来,等用户点击确定按钮时再调用,最终实现效果:

  当然blcok除了作为方法参数外,还可以当做属性和返回值。

 

2、block的底层结构

  接下来我们来看一下block究竟是一个什么样的结构?

  通过clang命令将oc代码转换成c++代码(如果遇到_weak的报错是因为_weak是个运行时函数,所以我们需要在clang命令中指定运行时系统版本才能编译):

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main.cpp
-(void)viewDidLoad{
    [super viewDidLoad];
    int i = 1;
    void(^block)(void) = ^{
        NSLog(@"%d",i);
    };
    block();
}

转换成c++代码如下:

//block的真实结构体
struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  int i;
    //构造函数(相当于OC中的init方法 进行初始化操作) i(_i):将_i的值赋给i flags有默认值,可忽略
  __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int _i, int flags=0) : i(_i) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//封存block代码的函数
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
  int i = __cself->i; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_3g_7t9fzjm91xxgdq_ysxxghy_80000gn_T_ViewController_c252e7_mi_0,i);
    }

//计算block需要多大的内存
static struct __ViewController__viewDidLoad_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)};

//viewDidLoad方法
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
    //定义的局部变量i
    int i = 1;
    //定义的blcok底部实现
    void(*block)(void) = &__ViewController__viewDidLoad_block_impl_0(
                                            __ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, i));
    //block的调用
    bloc->FuncPtr(block);
}

从中我们可以看出,定义的block实际上就是一直指向结构体_ViewController_viewDidLoad_block_impl_0的指针(将一个_ViewController_viewDidLoad_block_impl_0结构体的地址赋值给了block变量)。而这个结构体中,我们看到包含以下几个部分:

impl、Desc、引用的局部变量、构造方法。

而从构造方法我们又可以看出impl中有以下几个成员:isa、Flags、FuncPtr,所以综合以上信息我们可以知道block内部有以下几个成员:

接下来,我们依依来看block底层结构中这些结构体j或者参数的作用是什么?

首先Desc:

static struct __ViewController__viewDidLoad_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)};

  desc结构体中存储着两个参数,reserved和Block_size,并且reserved赋值为0而Block_size则存储着__ViewController__viewDidLoad_block_impl_0的占用空间大小。最终将desc结构体的地址传入__ViewController__viewDidLoad_block_impl_0中赋值给Desc。所以Desc的作用是记录Block结构体的内存大小

接下来,我们来看,int i:

  i也就是我们定义的局部变量,因为在block块中使用到i局部变量,所以在block声明的时候这里才会将i作为参数传入,也就说block会捕获i。如果没有在block中使用age,这里将只会传入impl,Desc两个参数。这里需要注意的一点是,调用block结构体的构造函数时,是将我们定义的局部变量i的值传进去了的,也就是构造函数实现的时候i(_i)  这部分代码的作用就是将_i的值传给i。其实这也就解释清楚为什么我们在block中无法修改i的值了,因为block用到的i根本和我们自己定义的i不是同一个,block内部是自己单独创建了一个参数i,然后将我们定义的局部变量i的值赋给了自己创建的i。

最后我们来看一下impl这个结构体:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

我们在block结构体的构造函数中也可以看出这几个成员分别有什么作用:

  isa:指针,存放结构体的内存地址

  Flags:这个用不到 有默认值

  FuncPtr:block代码块地址

所以通过以上分析,我们可以得出以下几个结论:

1、block本质上也是一个OC对象,它内部也有个isa指针,这一点我们也可以通过打印其父类是NSObject来证明;

2、block是封装了函数调用以及函数调用环境的OC对象.(所谓调用环境就是比如block用到了变量i就把它也封装进来了);

3、FuncPtr则存储着viewDidLoad_block_func_0函数的地址,也就是block代码块的地址。所以当调用block的时候,bloc->FuncPtr(block);是直接调用的FuncPtr方法。

4、impl结构体中isa指针存储着&_NSConcreteStackBlock地址,可以暂时理解为其类对象地址,block就是_NSConcreteStackBlock类型的。

5、Desc存储__viewDidLoad_block_impl_0结构体所占用的内存大小,也就是存储着Block的内存大小。

我们 可以用一张图来表示各个结构体之间的关系:

再简单化就是:(网络图片 但是原理一样)

block底层的数据结构也可以通过一张图来展示(variables就是结构体中所引用的变量,invoke就是上面的FuncPtr也就是block封装代码的函数地址,这个图是根据上面的分析可以总结出):

1.isa指针,所有对象都有该指针,用于实现对象相关的功能。
2.flags,用于按bit位表示一些block的附加信息,block copy的实现代码可以看到对该变量的使用。
3.reserved,保留变量。
4.invoke,函数指针,指向具体的Block实现的函数调用地址。就是FuncPtr
5.descriptor,表示该Block的附加描述信息,主要是size大小,以及copy和dispose函数的指针。
6.variables,截取过来的变量,Block能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。

 

3、blcok变量捕获

我们定义了几个变量:全局变量name、局部变量-auto变量:i,obj、局部变量-静态变量:height,分别在blcok内部修改和访问↓↓

  

  我们发现在block内部都可以访问这些变量,但是无法修改局部变量中的auto变量,无法修改的原因我们在上面的分析中也可以看出,是因为block内部自己创建了对应的变量,外部auto变量只是将值传递到block内赋给block创建的内部变量。block内部存在的只是自己创建的变量并不存在block外部的auto变量,所以没办法修改。

  但是为什么全局变量和静态变量就可以访问呢?我们将oc代码转换成c++代码来看

 

发现,block内部虽然访问了四个变量,但是其底层只捕获了三个变量,并没有捕获全局变量name

而且比较局部变量中的auto变量和静态变量,发现blcok底层捕获auto变量时是捕获的其值,而捕获静态变量时是捕获的变量地址(i是值, *height是地址),这也就是为什么我们可以在block修改静态变量,因为blcok内修改的静态变量其实和blcok外的静态变量是同一个内存地址,同一个东西。

  关于auto变量obj,blcok内部也是捕获它的值,不要因为它有*就觉得捕获的是地址,因为obj本身就是个对象,本身就是地址,如果block捕获的是obj的地址的话应该是NSObject **obj 即指向指针的地址。

所以我们通过上面这个例子可以总结出:

  

为什么会出现这种区别呢?

首先,为什么捕获局部变量而不捕获全局变量这个问题很好理解:

  全局变量整个项目都可以访问,block调用的时候可以直接拿到访问,不用担心变量被释放的情况;

  而局部变量则不同,局部变量是有作用域的,如果blcok调用的时候blcok已经被释放了,就会出现严重的问题,所以为了避免这个问题block需要捕获需要的局部变量。比如我们局部变量和block都卸载了viewDidLoad方法,但是我在touchesBegan方法中调用block,这个时候局部变量早就释放了,所以block要捕获局部变量)

接下来,为什么auto变量是捕获的值,而静态变量是捕获的地址呢?

  这是因为自动变量和静态变量存储的区域不同,两者释放时间也不同。

  我们在关于局部变量、全局变量的分析中讲到了自动变量是存放在栈中的,创建与释放是由系统设置的,随时可能释放掉,而静态变量是存储在全局存储区的,生命周期和app是一样的,不会被销毁。所以对于随时销毁的自动变量肯定是把值拿进来保存了,如果保存自动变量的地址,那么等自动变量释放后我们根据地址去寻值肯定会发生怀内存访问的情况,而静态变量因为项目运行中永远不会被释放,所以保存它的地址值就完全可以了,等需要用的时候直接根据地址去寻值,肯定可以找到的。

那么,又有一个问题了,为什么静态变量和全局变量同样不会被销毁,为什么一个被捕获地址一个则不会被捕获呢

  我个人觉得是静态变量和全局变量因为两者访问方式不同造成的,我们都知道全局变量整个项目都可以拿来访问,所以某个全局变量在全局而言是唯一的(也就是全局变量不能出现同名的情况,即使类型不同也不行,否则系统不知道你具体访问的是哪一个)而静态变量则不是,全局存储区可能存储着若干个名为height的静态变量。

  所以这就导致了访问方式的不同,比如说有个block,内部有一个静态变量和一个全局变量,那么在调用的时候系统可以直接根据全局变量名去全局存储区查找就可以找到,名称是惟一的,所以不用捕获任何信息即可访问。而静态变量而不行,全局存储区可能存储着若干个名为height的静态变量,所以blcok只能根据内存地址去区分调用自己需要的那个。

  我之前有个想法:是不是因为两者访问范围不同,全局变量可以全局访问,静态变量只能当前文件访问。但仔细想想block即使不是在当前文件调用的,但它的具体执行代码块内代码肯定是在当前文件执行的,也就是blcok内部访问变量不存在跨文件访问的情况,既然两者都可以访问到那么访问范围就不是原因了。

 

-(void)viewDidLoad{
    [super viewDidLoad];
    void(^block)(void) = ^{
        NSLog(@"%@",self);
    };
    block();
    
    void(^block2)(void) = ^{
        NSLog(@"%@",self.address);
    };
    block2();
}

 

上面这段代码中的block是怎么捕获变量的呢?

我们转换成c++代码可以看出,我们可以看到viewdidload实际上是转换成了 void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd)

也就是这个方法转换为底层实现时是有两个参数的:self和_cmd,既然self是方法的参数,那么self肯定是个局部变量,又因为这个局部变量并没有static修饰,所以self应该会被捕获并且是值传递

在访问实例对象(self)的属性(address),我们发现block并没有捕获这个具体的属性而是捕获的实例对象(self),这是因为通过self就可以获取到这个实例对象的属性,捕获一个实例对象就够了,而在block内部使用这个属性的时候,也是通过实例对象来获取的↓↓

 

4、blcok的类型

我们在分析block底层结构的时候,看到了isa存储的是&_NSConcreteStackBlock地址,也就是这个block是个block类型的,那么blcok只有这一种类型吗?

答案是否定的,blcok有三种类型,我们可以通过代码来验证:

有的同学可能将oc转为c++↓↓发现oc代码编译后三个block都是StackBlock类型的,和我们刚才打印的不一样。这是因为runtime运行时过程中进行了转变。最终类型当然以runtime运行时类型也就是我们打印出的类型为准。

既然存在三种不同的类型,那系统是根据什么来划分block的类型的呢?不同类型的block分别存储在哪呢?

也就是根据两点:有没有访问auto变量、有没有调用copy方法:

而这三种变量存放在内存中的位置也不同:__NSMallocBlock__是在平时编码过程中最常使用到的。存放在堆中需要我们自己进行内存管理。

关于判断类型的两个条件,我们第一个条件也就是判断有误访问auto变量这个是明白的,但是第二个就不太清楚了,调用copy有什么用?做了哪些操作了?

__NSGlobalBlock __ 调用copy操作后,什么也不做
__NSStackBlock __ 调用copy操作后,复制效果是:从栈复制到堆;副本存储位置是堆
__NSMallocBlock __ 调用copy操作后,复制效果是:引用计数增加;副本存储位置是堆

也就是:

由于ARC环境下,系统会对一下情况下的block自动做copy处理:

//1.block作为函数返回值时
typedef void (^Block)(void);
Block myblock()
{
    int a = 10;
    //此时block类型应为__NSStackBlock__
    Block block = ^{
        NSLog(@"---------%d", a);
    };
    return block;
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block = myblock();
        block();
       // 打印block类型为 __NSMallocBlock__
        NSLog(@"%@",[block class]);
    }
    return 0;
}

//2.将block赋值给__strong指针时,比如(arc中默认所有对象都是强指针指引)
 void (^block1)(void) = ^{ 
    NSLog(@"Hello");
}; 
//3.block作为Cocoa API中方法名含有usingBlock的方法参数时。例如:遍历数组的block方法,将block作为参数的时候。 NSArray *array = @[]; [array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { }]; //4.block作为GCD API的方法参数时 例如:GDC的一次性函数或延迟执行的函数,执行完block操作之后系统才会对block进行release操作。 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ }); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ });

所以我们关闭ARC才能更好的看清楚copy的作用:

project -> Build settings -> Apple LLVM complier 3.0 - Language -> objective-C Automatic Reference Counting设置为NO

然后我们定义几个不同类型的block,并分别调用copy方法查看结果:

发现block的copy方法确实是有这样的作用,需要说明的一点,block3虽然多次copy后打印出来的retainCount始终是1,但其内存管理器中仍然会增加。

既然copy方法最大的一个作用是把block从栈拷贝到堆,它这样做的原因是什么?block在栈中和堆中有什么区别吗?

  我们都知道栈中对象的内存空间是随时可能被系统释放掉的,而堆中的内存空间是由开发者维护的,如果block存在于栈中就可以出现一个问题,当我们调用block的时候,他被释放掉了,从而出现错误:

我们发现,当我们调用block的时候打印出来莫名其妙的东西,这是因为test方法执行完后,栈内存中block所占用的内存已经被系统回收,因此就有可能出现乱得数据。

  可能有的同学会根据上面block访问auto变量的想法来思考,blcok不是已经捕获了这个变量了么,其实这完全是两码事,block确实是把变量a捕获到了自己内部,但是现在它自己的空间都被释放掉了,更不用说它捕获的变量了,肯定被释放掉了。

所以,这种情况下,是需要将block移到堆上面的,让开发者控制它的生命周期,这就用到了copy(arc环境下不用,因为test方法中将block赋值给了一个__strong指针,会生成copy)

既然blcok存放在堆中了,block内部有捕获了a的值,所以就可以正常输出了。

我们从这种情况也可以知道为什么不同环境下block的声明属性写法的不同:

MRC下block属性的建议写法:

@property (copy, nonatomic) void (^block)(void);

ARC下block属性的建议写法:

@property (strong, nonatomic) void (^block)(void);

@property (copy, nonatomic) void (^block)(void);

copy属性意味着,系统会自动对修饰的block进行一次copy操作,

所以在mrc环境下,copy属性修饰block就不会出现上面block存在栈里,在访问时被释放的情况;

而在arc环境下,系统会在block被__strong指针引用时自动执行copy方法,所以就可以写strong和copy两种。

 

5、__block

既然block内部可以修改静态变量和全局变量的值,而无法修改自动变量的值,那么有没有什么方式可以解决这个问题呢?

答案是肯定的,我们可以通过_ _block修饰这个自动变量,从而可以在block内部访问并修改这个自动变量了:

__block不能修饰全局变量、静态变量(static)

 

那么,_ _block 这个修饰符做了什么操作呢?就让可以让block内部可以访问自动变量

我们通过底层代码可以看出,__weak将int类型的数据转换成了一个__Block_byref_i_0的结构体类型

而这个结构体的结构是:

struct __Block_byref_i_0 {
  void *__isa;
__Block_byref_i_0 *__forwarding;
 int __flags;
 int __size;
 int i;
};

 而从赋值上看,isa为0,既然有isa指针,那么说明这个结构体也是一个对象,__forwarding存储的是__Block_byref_i_0的地址值,flags为0,size为Block_byref_i_0的内存大小,i是真正存储变量值的地方,其内部结构就是这样的↓↓(关于des结构体为什么会多了一个copy函数和一个dispose函数在下面相互引用中会讲到)

而当我们在block内部修改和访问这个变量是,底层实现是这样的:

是通过__Block_byref_i_0结构体的指针__forwarding读取和修改的变量i.

为什么要通过__forwarding转一下呢,而不是直接读取i

这是因为当我们调用block的时候,block可能存在于栈中可能存在于堆中,

void (^block)(void);
void test()
{
    // __NSStackBlock__
    int a = 10;
    block = ^{
        NSLog(@"block---------%d", a);
    } ;

    //情况一:此时调用block还存在与栈中
    block();
    //此时block有两份 一个在栈中 一个在堆中
   [blcok copy];
    //一次copy对应一次release
    [block release];
    
    //这个方法执行完后虽然栈中的block释放了 但是已经拷贝到堆里一份,所以还是可以继续调用的
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        //情况二:test方法执行完后 栈中的block被释放了 堆中还有一个copy的block
        block();
    }
    return 0;
}

__forwarding指向示意图:

  如果是直接通过结构体的内存地址访问变量,因为结构体在堆中的地址和在栈中的地址肯定不一样,情况一和情况二很明显又是执行的同一个方法,所以就没有办法实现这个功能,也就是如果方法里是根据栈中的地址访问属性的,那么情况二就会出错,因为这个时候这个地址已经被释放了,如果是根据堆中的值去访问变量的话,那么情况一又有问题了,因为这个时候堆里还没有这个block呢。所以需要根据__forwarding指针去访问变量,这样的话才能确保情况一和情况二都会访问到这个结构体。

所以我们总结一下上面的分析:

1.__block将int i进行包装,包装成一个__Block_byref_i_0结构体对象,结构体中的i是存储i的int值的;

2.当我们在block内修改或访问该对象时,是通过该对象的__forwarding去找对应的结构体再找对应的属性值,这是因为__forwarding在不同情况下指向不同的地址,防止只根据单一的一个内存地址出现变量提前释放无法访问的情况。

 

那么我们就明白为什么可以修改__block修饰的自动变量了,因为__block修饰变量变成了一个对象,我们修改只是修改的这个对象中的一个属性,并不是修改的这个对象:就像这样↓↓

__block修饰下的i不再是int类型而变成一个对象(对象p),我们block内部访问修改和访问的是这个对象内部的int i(字符串p),所以是可以修改访问的。只不过这个转化为对象的内部过程封装起来不让开发者看到,所以就给人的感觉是可以修改auto变量也就是修改时是int i。

block外部访问__block修饰的变量也是通过__forwarding指针找到结构体对象内部的int i,既然是访问你的block内部的属性i,那么就是修改后的21了↓↓

 

6、blcok的内存管理

上面我们讲到了,__block是将int类型的数据包装成了一个对象,然后block内部捕获这个对象访问或者修改对象内部的int属性,那么block对其捕获的对象变量是怎么引用怎么管理的呢? 

当block在栈上时,不会对指向的对象产生强引用

当block被copy到堆时
    会调用block内部的copy函数
    copy函数内部会调用_Block_object_assign函数
    _Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak)做出相应的操作,形成强引用或者弱引用

如果block从堆上移除
    会调用block内部的dispose函数
    dispose函数内部会调用_Block_object_dispose函数
    _Block_object_dispose函数会自动释放指向的对象(release)

也就是block对其捕获的对象时强引用还是弱引用,主要看block存在于哪?

如果在栈上,那么对捕获的对象一律不会产生强引用;

如果在堆上的话,这个要看这个对象自身的修饰符了,自己的修饰符是strong那就是强引用,是weak的话那就是弱引用(oc中的指针默认为strong,除非指定为weak)

这里的copy函数和dispose函数就是我们在解析__block中看到block结构体内存描述结构体(desc)中的多出来的那两个函数:

我们看到copy函数内部调用的是_Block_object_assign函数,assign函数传递了三个参数:对象p的地址,对象p以及3,这个函数的作用就是根据person对象是什么类型的指针,对person对象产生强引用或者弱引用。

  可以理解为_Block_object_assign函数内部会对person进行引用计数器的操作,如果结构体内person指针是__strong类型,则为强引用,引用计数+1,如果结构体内person指针是__weak类型,则为弱引用,引用计数不变。

dispose函数内部调用的是_Block_object_dispose函数,dispose函数传递了两个参数,对象p和3,这个函数的作用就是对person对象做释放操作,类似于release,也就是断开对person对象的引用,而person究竟是否被释放还是取决于person对象自己的引用计数。

下面我们通过图片来形象化地看一下这个流程:

当block从栈copy到堆的时候回自动执行des结构体重的copy函数:

当block从堆中被释放的时候,会调用dispose函数释放捕获的变量:

 

7、循环引用问题

循环引用也是block中一个常见的问题,什么是循环引用呢?

  我们从上面block捕获对象变量的分析可以看出,block在堆中的时候会根据变量自己的修饰符来进行强引用或者弱引用,假设block对person对象进行强引用,而person如果对block也进行强引用的话,那就形成了循环引用,person对象和block都有强指针指引着,使它们得不到释放:

 我们发现当最后一行代码执行完后,dealloc并没有执行,也就是person并没有被释放,这就是因为循环引用。

  block是p对象的一个属性,所以p对block是一个强引用关系,而block内部又捕获了p对象,p默认是强指针的,所以block对p也是一个强引用,双方就形成了这样一个关系:

  当block调用完后,系统对block说:用完了你就释放吧,block说我现在释放不了,因为person对象还要用我呢。然后系统又找到person对象说你先释放吧,person说我也是放不了,因为block里面还要用我呢。就这样,person和block都双双无法释放。

  那么怎么解决这个循环引用呢?很简单,只要把任意一根红线设置为弱引用就行,比如说这样↓↓

这样的话就要对代码这样修改:

@property(nonatomic,weak)MyBlock block;

测试发现确实被释放了,但是这种方案不太合理,因为我们前面讲到了,block最好是放在栈中去操作,在arc中修饰符应该是strong或者copy,所以我们需要再换种方案。

这样的话就需要对代码这样修改了:

    person *p = [[person alloc]init];
    __weak person *weakP = p;
    p.block = ^{
        NSLog(@"%@",weakP);
    };
    p.block();

测试发现,block和person对象都被释放了。

那么除了这个方案还有其他方法么?答案是有的,下面是循环引用的结构↓↓只要这三条线有一条是弱引用就不会发生循环引用的情况。

首先,①这个是没有办法改为弱引用的,因为block要copy到堆中就得用strong或者copy修饰,不能用weak;

我们通过__weak是将②变为弱引用,当然除了__weak,我们也可以用__unsafe_unretained:

__unsafe_unretained和__weak一样,表示的是对象的一种弱引用关系。

  唯一的区别是:__weak修饰的对象被释放后,指向对象的指针会置空,也就是指向nil,不会产生野指针;而__unsafe_unretained修饰的对象被释放后,指针不会置空,而是变成一个野指针,那么此时如果访问这个对象的话,程序就会Crash,抛出BAD_ACCESS的异常。
 __weak person *weakP = p;
    __unsafe_unretained person *unsafeP = p;
    //当p释放后,weakp会自动指向nil 而unsafeP则不会,会继续指向对象的地址,对象已经销毁,此时unsafeP访问的是"僵尸"对象

那么我们还有一种办法可以解决循环引用,那就是将③在block执行完后手动释放:通过__block

    __block person *p = [[person alloc]init];
    p.block = [^{
        NSLog(@"%@",p);
        p = nil;
    }copy];
    
    p.block();

但是这个方案的话必须要求调用block,不调用的话p=nil不会执行,也就是③这条强引用还是存在的。

所以,我们可以总结一下解决循环引用的方案:

ARC环境:

1.用__weak、__unsafe_unretained解决;

2.用__block解决(必须要调用block)

MRC环境:

1.用__unsafe_unretained解决;

2.用__block解决(必须要调用block)。

 

__strong的使用

__weak避免循环引用的用法我们清楚后,我们也会发现很多地方在使用__weak的时候经常和__strong一起使用,比如下面这段代码:

    __weak __typeof(self)weakSelf = self;
    AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
        __strong __typeof(weakSelf)strongSelf = weakSelf;

        strongSelf.networkReachabilityStatus = status;
        if (strongSelf.networkReachabilityStatusBlock) {
            strongSelf.networkReachabilityStatusBlock(status);
        }

    };

这个时候我们就有疑问了,这个__strong的作用是什么呢?不是为了避免循环才加的__weak吗,在用__strong修饰后不会再出现循环引用的问题吗?

  其实很多时候我们只用__weak 不用__strong也不会出现什么问题,但是为了保险起见,还是建议__weak后在block内部再用__strong修饰一下。这是为了保证在block执行完成之前该对象不会被释放,避免weak对象被提前释放引发的不安全问题。比如block内如引用了一个weakPerson对象,但是一段时间后 当我要执行这个block的时候,person已经被释放了,那就会影响代码的运行了。

  其次,__strong修饰后不会再出现循环引用的问题。这是因为strongself只是block内部的一个局部变量  当block执行完成之后就自动释放了,所以不会出现循环引用的问题。

 

最后再提一个面试中经常问道的问题:block可以给NSMutableArray中添加元素吗,需不需要添加__block?

答案是不需要,下面通过代码验证。

因为在block块中仅仅是使用了array的内存地址,往内存地址中添加内容,并没有修改arry的内存地址,因此array不需要使用__block修饰也可以正确编译。

 

下面这两篇文章对block本质有着详细的介绍,同学们也可以看一下↓↓

参考资料

参考资料

 

posted @ 2019-04-26 18:57  高晓牛  阅读(2577)  评论(1编辑  收藏  举报