Block本质

本文章只是读博客的理解。https://juejin.im/post/5b0181e15188254270643e88#heading-23

Block的本质

1637de343b05ffaa.jpg

block的变量捕获

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        auto int a = 10;
        static int b = 11;
        void(^block)(void) = ^{
            NSLog(@"hello, a = %d, b = %d", a,b);
        };
        a = 1;
        b = 2;
        block();
    }
    return 0;
}
// log : block本质[57465:18555229] hello, a = 10, b = 2
// block中a的值没有被改变而b的值随外部变化而变化。

上面的代码生成c++代码查看


1637de344ce8c667.jpg

局部变量

a为auto变量 ,b为static变量可以看到,a捕获的是值,而b捕获的是指针,(static 修饰的变量为指针传递,同样会被block捕获。)

为什么会出现如此的差异呢?因为自动变量可能会被销毁,因为在执行block的时候,可能自动变量已经被销毁了,那么此时如果再去访问已经被销毁的地址肯定会发生坏内存访问。而静态变量不会被销毁,所以完全可以传递地址,而因为传递的是地址,所以在block调用之前修改地址中保存的值,block中的地址是不会变得。所以值会改变。

全局变量

int a = 10;
static int b = 11;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(void) = ^{
            NSLog(@"hello, a = %d, b = %d", a,b);
        };
        a = 1;
        b = 2;
        block();
    }
    return 0;
}
// log hello, a = 1, b = 2

同样生成c++代码查看全局变量调用方式


1637de3446bf83fa.jpg

通过上述代码可以发现,__main_block_imp_0并没有添加任何变量,因此block不需要捕获全局变量,因为全局变量无论在哪里都可以访问。

局部变量因为跨函数访问所以需要捕获,全局变量在哪里都可以访问 ,所以不用捕获。

block的类型

1637de34c0579805.jpg

上图中可以发现,根据block的类型不同,block存放在不同的区域中。
数据段中的NSGlobalBlock直到程序结束才会被回收,不过我们很少使用到NSGlobalBlock类型的block,因为这样使用block并没有什么意义。
NSStackBlock类型的block存放在栈中,我们知道栈中的内存由系统自动分配和释放,作用域执行完毕之后就会被立即释放,而在相同的作用域中定义block并且调用block似乎也多此一举。
NSMallocBlock是在平时编码过程中最常使用到的。存放在堆中需要我们自己进行内存管理。

block是如何定义其类型的

1637de34b6966052.jpg

NSGlobalBlock一般情况下是不使用的,在block内部访问局部变量会自动的转换为NSStackBlock,对NSStackBlock进行一次copy操作的时候回把NSStackBlock转化为NSMallocBlock,那什么情况下会对NSStackBlock进行copy操作呢?
1.block作为函数的返回值时。

typedef void (^Block)(void);
Block myblock()
{
    int a = 10;
    // 上文提到过,block中访问了auto变量,此时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;
}

屏幕快照 2019-03-20 上午10.57.15.png

block访问局部变量的时候,block的类型为NSStackBlock,但是上面的打印内容发现NSMallocBlock,说明block的内存没有被销毁,这个说明在ARC的情况下,会自动帮助我们进行一次copy操作,并且在是适当的时候进行release操作

 // block内没有访问auto变量
        Block block = ^{
            NSLog(@"block---------");
        };
        NSLog(@"%@",[block class]);
        int a = 10;
        // block内访问了auto变量,但没有赋值给__strong指针
        NSLog(@"%@",[^{
            NSLog(@"block1---------%d", a);
        } class]);
        // block赋值给__strong指针
        Block block2 = ^{
            NSLog(@"block2---------%d", a);
        };
        NSLog(@"%@",[block2 class]);
屏幕快照 2019-03-20 上午11.21.52.png
  1. block作为Cocoa API中方法名含有usingBlock的方法参数时
  2. block作为GCD API的方法参数时

block对对象变量的捕获

栈空间上的block不会对对象强引用,堆空间的block有能力持有外部调用的对象,即对对象进行强引用或去除强引用的操作。

在block中捕获对象类型的变量时,我们发现__main_block_impl_0的描述结构体__main_block_desc_0中多了两个参数copy和dispose参数

copy本质就是__main_block_copy_0函数,__main_block_copy_0函数内部调用_Block_object_assign函数,_Block_object_assign中传入的是person对象的地址,person对象
dispose本质就是__main_block_dispose_0函数,__main_block_dispose_0函数内部调用_Block_object_dispose函数,_Block_object_dispose函数传入的参数是person对象

_Block_object_assign函数调用时机及作用
当block进行copy操作的时候就会自动调用__main_block_desc_0内部的__main_block_copy_0函数,__main_block_copy_0函数内部会调用_Block_object_assign函数。
_Block_object_assign函数会自动根据__main_block_impl_0结构体内部的person是什么类型的指针,对person对象产生强引用或者弱引用。可以理解为_Block_object_assign函数内部会对person进行引用计数器的操作,如果__main_block_impl_0结构体内person指针是__strong类型,则为强引用,引用计数+1,如果__main_block_impl_0结构体内person指针是__weak类型,则为弱引用,引用计数不变。
_Block_object_dispose函数调用时机及作用
当block从堆中移除时就会自动调用__main_block_desc_0中的__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数。
_Block_object_dispose会对person对象做释放操作,类似于release,也就是断开对person对象的引用,而person究竟是否被释放还是取决于person对象自己的引用计数。

总结

1.一旦block中捕获的变量为对象类型,block结构体中的__main_block_desc_0会出两个参数copy和dispose。因为访问的是个对象,block希望拥有这个对象,就需要对对象进行引用,也就是进行内存管理的操作。比如说对对象进行retarn操作,因此一旦block捕获的变量是对象类型就会会自动生成copy和dispose来对内部引用的对象进行内存管理。

2.当block内部访问了对象类型的auto变量时,如果block是在栈上,block内部不会对person产生强引用。不论block结构体内部的变量是__strong修饰还是__weak修饰,都不会对变量产生强引用。

3.如果block被拷贝到堆上。copy函数会调用_Block_object_assign函数,根据auto变量的修饰符(__strong,__weak,unsafe_unretained)做出相应的操作,形成强引用或者弱引用

4.如果block从堆中移除,dispose函数会调用_Block_object_dispose函数,自动释放引用的auto变量。

block内修改变量的值

方式一:age使用static修饰。
前文提到过static修饰的age变量传递到block内部的是指针,在__main_block_func_0函数内部就可以拿到age变量的内存地址,因此就可以在block内部修改age的值。

方式二:__block
__block用于解决block内部不能修改auto变量值的问题,__block不能修饰静态变量(static) 和全局变量

这里解释较为复杂,详细过程见原文

你可能感兴趣的:(Block本质)