美文网首页
Block本质

Block本质

作者: 杨小雨的杂货铺 | 来源:发表于2019-03-20 15:55 被阅读0次

    本文章只是读博客的理解。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本质

          本文链接:https://www.haomeiwen.com/subject/gebamqtx.html