Block学习总结(三)

关于block的存储域

一、 block变量存储域

1. ARC和MRC不同的存储情况

通过对block本质的探究,发现block内部也是有一个isa指针指向它所属的类,所以block其实也是一个对象。

// ARC 环境下
    void (^blk)() = ^{
    
    };
    NSLog(@"%@",[blk class]);

其打印结果为 " _ _NSGlobalBlock _ _ ",可想而知block是属于一个叫NSGlobalBlock类的对象。其实 block的类有三种。分别为 NSGlobalBlock ,NSStackBlock ,NSMallocBlock。从它们的名字能猜测到,这三种类型的block对应着block不同的存储区域。相继为 global(全局数据区域) stack (栈区) malloc(堆区)。

分别在ARC和MRC的情况下看看哪些情况下对应的block存储区域,首先是ARC的情况下

    NSLog(@"未赋值 未引用外部变量 %@", [^void() {
    
    } class]);
    
    void (^blk)() = ^{
    
    };
    NSLog(@"赋值 未引用外部变量 %@",[blk class]);
    
    int a = 1;
    void (^blk1)() = ^{
        a;
    };
    NSLog(@"赋值 引用外部变量 %@",[blk1 class]);
    
    NSLog(@"未赋值 引用外部变量  %@", [^void() {
        a;
    } class]);

最后的输出结果为:

2017-01-08 21:49:15.389 block存储域[10956:1104789] 未赋值 未引用外部变量 __NSGlobalBlock__
2017-01-08 21:49:15.389 block存储域[10956:1104789] 赋值 未引用外部变量 __NSGlobalBlock__
2017-01-08 21:49:15.389 block存储域[10956:1104789] 赋值 引用外部变量 __NSMallocBlock__
2017-01-08 21:49:15.390 block存储域[10956:1104789] 未赋值 引用外部变量  __NSStackBlock__

可以对ARC的情况下做一个总结:

  • NSGlobalBlock === 在block未引用外部变量的时候
  • NSMallocBlock === 在block引用了外部变量的时候,且将block赋值给了block类型的变量
  • NSStackBlock === 在block引用了外部变量的时候, 没有将block赋值给block类型的变量时。

接下来是对在MRC的情况下,block的不同存储区域

    NSLog(@"未赋值 未引用外部变量 %@", [^void() {
    
    } class]);
    
    void (^blk)() = ^{
    
    };
    NSLog(@"赋值 未引用外部变量 %@",[blk class]);
    
    int a = 1;
    void (^blk1)() = ^{
        a;
    };
    NSLog(@"赋值 引用外部变量 %@",[blk1 class]);
    
    NSLog(@"未赋值 引用外部变量  %@", [^void() {
        a;
    } class]);

输出结果为:

2017-01-08 21:56:21.789 block存储域[10994:1108230] 未赋值 未引用外部变量 __NSGlobalBlock__
2017-01-08 21:56:21.790 block存储域[10994:1108230] 赋值 未引用外部变量 __NSGlobalBlock__
2017-01-08 21:56:21.790 block存储域[10994:1108230] 赋值 引用外部变量 __NSStackBlock__
2017-01-08 21:56:21.791 block存储域[10994:1108230] 未赋值 引用外部变量  __NSStackBlock__

我们可以看到无论是否引用外部变量和是否赋值给了block类型的变量,都没有出现mallocBlock,那么MRC的情况下如何才会生成mallocBlock呢。这时我们需要调用对象的copy方法。

    void (^blk)() = ^{
    
    };
    NSLog(@"copy 未引用外部变量 %@",[[blk copy] class]);
   
    int a = 1;
    void (^blk1)() = ^{
        a;
    };
    NSLog(@"copy 引用外部变量 %@",[[blk1 copy] class]);

2. ARC情况下什么时候会进行copy

block引用了外部变量就会存储在栈区,在ARC环境下大多数情况会自动copy操作后复制到堆区。但是有些情况下编译器不会自动进行copy操作:

  • block作为函数参数的时候不会自动进行copy操作,除了以下情况之外:

    (1) Cocoa框架的方法切方法名中又usingBlock。

    (2) GCD的API。

通过以下代码验证:

  - (void)viewDidLoad {
    [super viewDidLoad];
    NSArray *tempArray = [self getBlockArray];
    blk_t blk = tempArray[0];
    blk();
}

- (id)getBlockArray {
    int a = 10;
    return [[NSArray alloc] initWithObjects:^{
        NSLog(@"blk1 %d",a);
    },^{
        NSLog(@"blk2 %d",a);
            }, nil];
}

因为作为参数传递的block的存储区域为栈区,而出了 getBlockArray 函数后,block的作用域结束了,当调用函数取得数组时,访问已经释放了的block,就会造成了crash。要是想正确访问block ,可以做以下修改,对作为参数传递的block进行copy操作。

- (id)getBlockArray {
    int a = 10;
    return [[NSArray alloc] initWithObjects:[^{
        NSLog(@"blk1 %d",a);
    } copy],[^{
        NSLog(@"blk2 %d",a);
            }copy], nil];
}

二、 __block变量存储域

我们知道对于block内部使用的外部变量,不允许在block内部修改外部变量的值。以下代码是编译不过的。

    int a = 10;
    void (^blk)() = ^(){
    a = 20;
    } ;
    blk();

因为在block进行copy的同时,会将block内使用的外部变量也进行一次copy操作,如果这个变量是在栈区的,则会被拷贝到堆区,并被block持有。而如果之前这个变量是在堆区的则不会有影响,只是被block持有。

    int a = 10;
    NSLog(@"定义block前 %p",&a);
    void (^blk)() = ^(){
        NSLog(@"block内部 %p",&a);
    } ;
    blk();
    NSLog(@"定义block后 %p",&a);

打印结果为:

2017-02-08 14:20:06.716 block存储域_test[23117:147534] 定义block前 0x7fff5b635a4c
2017-02-08 14:20:06.717 block存储域_test[23117:147534] block内部 0x60800005a1b0
2017-02-08 14:20:06.717 block存储域_test[23117:147534] 定义block后 0x7fff5b635a4c

16进制转换为10进制,block外部为 140734726625868 ,block内部使用的时候为 106102872449456,2者相差1532868764个字节,转换为1461 mb。因为堆地址远小于栈中的地址,又因为iOS中一个进程的栈区内存只有 1mb,所以在block内部 的变量已经在堆区内了。

__block 变量

对于block内部使用 _ _block变量时,和普通的变量一样,如果这个变量是在栈区的,则会被拷贝到堆区,并被block持有。而如果之前这个变量是在堆区的则不会有影响,只是被block持有。但是为什么能在block内部修改 _ _block修饰的变量。

    __block int a = 10;
    NSLog(@"定义block前 %p",&a);
    void (^blk)() = ^(){
        a++;
        NSLog(@"block内部 %p",&a);
    } ;
    NSLog(@"定义block后 %p",&a);
    blk();

打印的结果为:

2017-02-08 14:44:01.933 block存储域_test[26207:167214] 定义block前 0x7fff569eca48
2017-02-08 14:44:01.934 block存储域_test[26207:167214] 定义block后 0x60800003eb38
2017-02-08 14:44:01.934 block存储域_test[26207:167214] block内部 0x60800003eb38

与普通的外部变量不同的是,定义block后,变量的地址也指向了堆中的那个地址。而不是定义之前的栈中地址。还记得__block内部那个指向 变量自身的那个forwading指针吗,当 _ _block变量赋值到堆区时,栈区的变量的forwading指针也改为了指向堆中的那个变量的地址,所以在block外部使用变量时,它访问的是堆中的那个变量

a++  <===> a._forwarding ->a)++

三、 截获的对象存储域

通常的一个变量,如下,当它超过了作用域之后,就会被释放了。

- (void)test5 {
    NSMutableArray *tempArray = [NSMutableArray array];
}

但是对于block中使用的外部变量,在某些情况下,却能超过变量的作用域存在。

(该代码 是在ARC环境下运行)
blk_t globalblk;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    globalblk();
}

- (void)test5 {
    NSMutableArray *tempArray = [NSMutableArray array];
    globalblk = ^{
        id obj = [[NSObject alloc] init];
        [tempArray addObject:obj];
        NSLog(@"count == %ld a == %d", tempArray.count,a);
    };
}

该代码中,tempArray变量出了 test5这个函数,就超出了作用域,但是该代码却能正确的打印出来。说明: 在ARC环境下,当给block赋值给一个block变量时,将会默认的对block进行一次copy操作。如果在MRC环境下,也就是没有对block进行手动copy的话,这段代码就会crash。

四、 __block和对象

关于__block修饰的对象,被block使用的时候,在ARC和MRC情况下,是有很大的不同的。当在ARC环境下,block内部使用 _ _block修饰的对象的时候,它和正常的外部变量一样,当block从栈区copy到堆区时,会被block所持有,因此能超出变量总用域存在。而在MRC的情况下,当 _ _block修饰的变量被block使用时,不会随着block从栈区copy到堆区而被block所持有。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    globalblk();
}

- (void)test5 {
    __block NSMutableArray *tempArray = [NSMutableArray array];
    globalblk = [^{
        id obj = [[NSObject alloc] init];
        [tempArray addObject:obj];
        NSLog(@"count == %ld", [tempArray count]);
    } copy];
}

以上的代码,在ARC环境下能正常运行,在MRC情况下会因为野指针的访问而crash。

五、 block的循环引用

block持有_ _strong修饰的外部变量时,block会持有该对象,而如果该对象又持有block时,就会造成循环引用的问题。

typedef void (^blk_t)();
@interface Person ()
{
    blk_t blk;
}
@end

@implementation Person
- (instancetype)init {
    if (self = [super init]) {
        blk = [^(){
            NSLog(@"%@",self);
        } copy];
    }
    return self;
}

- (void)dealloc {
    NSLog(@"dealloc");
}

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p = [[Person alloc] init];
}

该段代码的dealloc一定不会被打印,因为在调用 Person的init函数是,成员变量blk使用了 self,使得blk对 self 持有。而blk作为 Person对象的成员变量,Person对象就对 blk持有。造成了这种互相持有的关系后,该对象就无法释放了。

Block学习总结(三)_第1张图片
循环引用问题.png

解决方法(1): 通过对block内部使用的变量,改为weak修饰。

- (instancetype)init {
    if (self = [super init]) {
        __weak typeof(self) weakSelf = self;
        blk = [^(){
            NSLog(@"%@",weakSelf);
        } copy];
    }
    return self;
}

解决方法(2): 之前说过,在MRC环境下,用_ _block修饰变量时,block不会对该变量进行持有。

//仅对MRC环境下有效
- (instancetype)init {
    if (self = [super init]) {
        __block tempSelf = self;
        blk = [^(){
            NSLog(@"%@",tempSelf);
        } copy];
    }
    return self;
}

你可能感兴趣的:(Block学习总结(三))