Block底层原理

2021-02-02  本文已影响0人  星空WU

1、 block 定义

:带有自动变量的匿名函数。

匿名函数:没有函数名的函数,一对{}包裹的内容是匿名函数的作用域。

自动变量:栈上声明一个变量既不是静态变量,又不是全局变量,是不可以在栈内声明的匿名函数中使用的。但是在block中却可以,虽然使用block不用声明类,但是block提供了类似OC的类一样,可以通过成员变量来保护作用域外变量值的方法,那些再block的一对{}里面使用到,但却是在{}作用域外声明的变量,就是block捕捉的自动变量。

    block 表达式语句:^ 返回值类型(参数列表){表达式}

无参数无返回值block

    有参无返回值

有参数有返回值

无参数有返回值

用typedef定义block

2、block的分类

    block主要分为三类:

        全局block:_NSConcreteGlobalBlock;存储在全局内存中,相当于单例。

        栈block:_NSConcreteStackBlock;存储在栈内存中,超出其作用域则马上被销毁 。

        堆block:_NSConcreteMallocBlock;存储在堆内存中,是一个带引用计数的对象,需要自行管理其内存。    

    如何区分block存储区域?

    block不访问外部变量(包括栈和堆中的变量),此时为全局block,arc和mrc都是如此。

    

block访问外部变量:

    MRC环境下:访问外部变量的block默认是存储在栈中的。

    ARC环境下:访问外部变量的block默认是存储在堆中的(实际是放在栈区,然后ARC情况下又自动拷贝到堆区),自动释放。

    

在ARC环境下我们怎么获取栈block呢?

此时我们通过__weak不进行强持有,block就还是栈区的block。

此时我们通过__weak不进行强持有,block就还是栈区的block

ARC环境下,访问外部变量的block为什么要自动从栈区拷贝到堆区呢?

因为:栈上的block,如果其所属的变量作用域结束,该block就会被废弃,如同一般的自动变量。当然,block中的__block变量也同时会被废弃。

为了解决栈块在其变量作用域结束之后被废弃(释放)的问题,我们需要把block复制到堆中,延长其生命周期。开启ARC时,大多数情况下编译器会恰当的进行判断是否有必要将block从栈复制到堆,如果有,自动生成将block从栈复制到堆的代码。block的复制操作执行的是Copy实例方法。block只要调用了Copy方法,栈块就会变成堆块

    上面的代码中,函数返回的block是配置在栈上的,所以返回返回函数调用方法时,block变量作用域就被释放了,block也会被释放。但是,在ARC环境下是有效的,这种情况编译器会自动完成复制。

    在非ARC情况下则需要开发者调用Copy方法手动复制。

    将block从栈区复制到堆区非常消耗CPU,所以当block设置在栈上也能使用时,就不要复制了,因为此时的复制只是在浪费CPU资源。

block的复制操作,执行的是Copy实例方法。不同类型的block使用的Copy方法的效果如下:

   

根据表格我们知道,block在堆区Copy会造成引用计数增加,这与其它OC对象是一样的。虽然block在栈中也是以对象的身份存在,但是栈区没有引用计数,因为不需要,我们都知道栈区的内存由编译器自动分配释放。

底层分析

使用 clang将OC代码文件转换成C++文件,查看block的方法。

    

    首先切换到目标代码所在文件夹下,

输入下面的指令

注意:clang -x objective-c -rewrite-objc -isysroot  意思是编译绝对路径中的文件,

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk  是你的xcode中SDK的路径,

BlockStudyViewController.m 是你要编译的文件

执行完后在项目中会多一个.cpp文件

打开文件后我们可以看到ViewDidLoad 方法被编译的样子

定位到myBlock100, 我们发现myBlock100被构造成了__BlockStudyViewController__test03_block_impl_0

寻找 __BlockStudyViewController__test03_block_impl_0

可以看到__BlockStudyViewController__test03_block_impl_0是一个结构体。同时我们也可以说block是一个__BlockStudyViewController__test03_block_impl_0类型的对象,这也是为什么block能够%@打印的原因

block自动捕获外部变量

仍然是上面的例子,

block自动捕获的外部变量,在block的函数体内是不允许被修改的。

编译器生成了一个同名的变量,__BlockStudyViewController__test03_block_impl_0中的num是值拷贝,因此,在block内存会生成一个,内容一样的同名变量,此时如果在函数体内进行num++的操作,则编译器就不清楚该去修改哪个变量。所以block自动捕获的变量,在函数体内部是不允许修改的。

那么我们要修改外部变量要怎么办呢?

1、__block修饰外部变量。

2、将变量定义成全局变量

3、将变量用参数的形式,传入block里面

__block 原理

    

分析编译文件.cpp

在__BlockStudyViewController__test03_block_impl_0中,我们发现num变成了一个__Block_byref_num_0类型的对象,

在__BlockStudyViewController__test03_block_func_0中,由之前的值拷贝变成了指针拷贝。

总结:__block修饰外界变量的时候:

    外界变量会生成__Block_byref_num_0结构体;

     结构体用来保存原始变量的指针和值 ;

    将变量生成的结构体对象的指针地址传递给block,然后在block内部就可以对外界变量进行修改了。    

__Block_byref_num_0中的__forwarding 是什么?


可以看到,__forwarding是一个指向自己本身的指针(自身为结构体)。

那么,在Copy操作之后,既然__block变量也被Copy到堆区上了,那么访问该变量是访问栈上的还是堆上的呢?这个时候我们就要来看一下,在Copy过程中__forwarding的变化了:

可以看到,通过__forwarding,无论是在block中,还是block外访问__block变量,也不管该变量是在栈上或是在堆上,都能顺利的访问同一个__block变量。

注意:这里与上面的结论并不矛盾。大家要主要到局部变量a被__block修饰之后,会变成__Block_byref_a_0结构体对象。所以无论是在栈区还是在堆区,只要__forwarding指向的地址一样,那么就可以在block内部修改外界变量。这里大家要仔细观察一下__Block_byref_a_0结构体。

举例再次理解值捕获

block只能捕获局部的非静态变量的值,局部非静态对象得值,对于全局和静态变量或静态对象是不能捕获值的,

以局部static对象为例

block并未能捕获值,而是捕获地址

分析cpp文件

看到代码1处已经是二重指针,3处在调用的时候取出的是指向name变量地址的内存,而不是name所指向的内存,所以要想获取name所指向的内存,4处通过*name取值进行传参.

捕获总结

    如果是局部变量的话,如果不持有他的话是不是过了作用域就释放了,那就不能完成方法的正常调用,所以对于局部变量,一定会捕获的;

    对于全局变量,block捕获变量的原因要使用变量,既然是全局变量,那在哪都可以访问,所以不需要捕获;

    那为什么局部变量有的是传地址有的是传值呢,对于非static修饰的局部变量其实是auto的,这种变量是放在栈区的,过了作用域就会被系统回收,如果block捕获变量的地址的话,那可能捕获的地址已经被系统回收,或者已经被其他的对象占用了,这个时候程序会出现无法预料的异常,但是如果是static修饰的,是放在数据区的,不会随着作用域的而销毁,从而放地址是安全的。

上一篇 下一篇

猜你喜欢

热点阅读