Block

Block本质

In programming languages, a closure is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.

img

  • Block 是将函数及其执行上下文封装起来的对象
  • Block的调用即是函数的调用
  • Block本质上也是一个OC对象,它内部也有个isa指针
  • Block是封装了函数调用以及函数调用环境的OC对象

block地层结构图中的第一个成员就是一个isa指针,所以我们可以将block当成一个对象来看待。isa常见的就是_NSConcreteStackBlock_NSConcreteMallocBlock_NSConcreteGlobalBlock这3种

Block 写法


@property (nonatomic, copy)void (^addBlockResult)(BOOL) ;

int multiplier = 6
int(^Block)(int) = ^int(int num){
    return num * multiplier;
}

Block 结构

//如下代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        void (^block)(int, int) = ^(int c, int b){
            NSLog(@"I am a block!");
            NSLog(@"I am a block!");
            NSLog(@"c = %d",c);
            NSLog(@"b = %d",b);
            NSLog(@"a的值为%d",a);
        };
        block(50,100);
    }
    return 0;
}

通过

 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

将OC文件用Clang重写

#import <Foundation/Foundation.h>

//将block的底层结构struct __main_block_impl_0直接般到main.m里面

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

struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int a;

};

int main(int argc, const char * argv[]) {
    @autoreleasepool {


        int a = 10;

        void (^block)(int, int) = ^(int c, int b){
            NSLog(@"I am a block!");
            NSLog(@"I am a block!");
            NSLog(@"c = %d",c);
            NSLog(@"b = %d",b);
            NSLog(@"a的值为%d",a);

        };

        struct __main_block_impl_0 *tmpBlock = (__bridge struct __main_block_impl_0 *)block;

        block(50,100);


    }
    return 0;
}

运行时下block内部的信息

Block 底层代码

img

Block 捕获外部变量

Block 捕获基础类型

Block捕获auto变量

接下来看看这种情况

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        //Block的定义
        void (^block)(void) = ^(){
            NSLog(@"Age is %d", age);
        };
        //先修改age的值
        age = 20;
        //Block的调用
        block();
    }
    return 0;
}

在block之前定义了一个int a = 10,然后在block内部使用了这个age,而且我在调用block之前,先将age的值修改成了20,那么此时程序运行会是什么结果呢

Interview03-block[4064:375528] Age is 10
Program ended with exit code: 0

结果是block中打印出的a10,我们在block外部对age的修改结果并没有对block的内部打印产生影响

(1)首先看一下此时block对应的结构体

img

我们发现有三处变化

  • 新增了一个int age成员变量
  • 构造函数里面多了一个参数 int _age
  • 构造函数里面参数尾部多了一个: age(_age),这是c++的语法,作用时将参数_age自动赋值给成员变量age

(2)再看一下main函数中的block定义以及赋值的代码

img

在用block构造函数生成block的时候,使用了外部定义的 int a = 10,因为c函数的参数都是值传递,所以这里是将此时外部变量a的值10传给了block的构造函数__main_block_impl_0,因此block内部的成员变量age会被赋值成10

(3)再看一下block内部封装的函数img

可以看到打印代码里面使用的age,实际上就是block内部的成员变量age,不是我们在外面定义的那个age,因此,当block被赋值之后,其成员变量age被赋值成了当时构造函数传进来的参数10,所以最终打印出来值就是10不论外部的age再如何的修改。外部的age跟block的成员变量age是两个不同的变量,互不影响。

其实,上面我门讨论的这个block外部变量age是一个局部auto变量,也叫自动变量。除了auto变量,C语言里面还有局部static变量静态变量)和全局变量,接下来我们就看看,Block对于这几种变量的使用,做了如何的处理。

Block捕获局部static变量

首先我们将上面的OC代码改造如下

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        static int height = 10;
        //Block的定义
        void (^block)(void) = ^(){
            NSLog(@"Age is %d, height is %d", age, height);
        };
        //先修改age和height的值
        age = 20;
        height = 20;
        //Block的调用
        block();
    }
    return 0;
}

我们有增加了一个static变量height,并且在同样的地方修改height的值,便于和之前的age进行对比。首先运行代码看一下结果

Interview03-block[4725:476530] Age is 10, height is 20
Program ended with exit code: 0

可以看到,block输出的 height值是我们在外部重新为其赋的20

(1)借用上面的分析流程一样,先看一下block对应的结构体

img

针对static变量height, block内部为其增加了一个int *height;成员变量,构造函数里面对应的参数是int *_height。看到这里这里要存储的是一个地址,该地址应该就是外部static变量height的地址值。

(2)main函数里的block赋值过程

对于static变量,block捕获的是它的地址

block构造函数里面传入的,就是外部的这个height的地址值。

(3)block内部的函数

img

那么可以看到,block内部的函数也是通过block所存储的地址值*height访问了外部的static变量height的值。

因此,当我们从外部修改height的值之后,调用block打印出的height的值也相应的改变了,因为block内部是通过 指针 引用了外部的这个static变量height

对于autostatic变量,为什么block选择用不同方式处理它们呢?

一个自动变量auto)的存储空间位于函数栈空间上,在函数开辟栈空间时被创建,在函数结束时销毁,而block的调用时机有可能发生在函数结束之后的,因此就无法使用自动变量了,所以在block一开始定义赋值的过程里,就将自动变量的值拷贝到他自己的存储空间上。 而对于局部静态变量(static),C语法下static会改变所修饰的局部变量的生命周期,使其在 程序整个运行期间都存在 ,所以block选择持有它的指针,在block被调用时,通过该指针访问这个变量的内容就行。

Block使用全局变量

上面讨论block对于局部变量的处理,在看一看对于全局变量,情况又是如何

img

输出结果如下

Interview03-block[13997:1263406] Age is 20, height is 20
Program ended with exit code: 0

在通过命令行生成一下编译后的C++文件,同样还是在文件底部去看

block使用全局变量

block没有对全局变量进行捕获行为,只需要在要用的时候,直接通过变量名访问就行了,因为全局变量时跨函数的,可以直接通过变量的名字直接访问。 同样,者也帮我我们理解了为什么对于局部的变量,block需要对其采取“捕获”行为,正是因为局部变量定在与函数内部,无法跨函数使用,所以根据局部变量不同的存储属性,要么将其值直接进行拷贝(auto),要么对其地址进行拷贝(static)。

总结

img

  1. 局部变量会被block捕获

  2. 自动变量(auto),block通过值拷贝方式捕获,在其内部创建一个同类型变量,并且将自动变量的值拷贝给block的内部变量,block代码块执行的时候,直接访问它的这个内部变量

  3. 静态变量(static),block通过地址拷贝方式捕获,在其内部创建一个指向同类型变量的指针, 将静态变量的地址值拷贝给block内部的这个指针,block代码块执行的时候,通过内部存储的指针间接访问静态变量

  4. 全局变量不会被block捕获, block代码块执行的时候,通过全局变量名直接访问

Block对于self的处理

img

img

编译结果显示blockself进行了捕获。But why? 我们知道,图中的block位于test方法里面,实际上任何的oc方法,转换成底层的c函数,里面都有两个默认

的参数,self_cmd

img

所以作为函数默认参数的self的实际上也是该函数的局部变量,根据我们上面总结的原则,只要是局部变量,block都会对其进行捕获,这就解释通了。

下面的情况呢

img

先看编译结果

img

看得出来,还是进行了捕获,在图中标明的黄色框框,就很好理解了,block最终访问CLPerson的成员变量_age的时候,是通过self +_age偏移量,获得_age的地址后从而进行间接访问的,所以在oc代码中,_age 的写法等同与self->_age,说白了,这里还是需要用到self,因此block还是需要对self进行捕获的。

Block捕获对象类型

有以下代码:

typedef void(^CLBlock)(void);//➕➕➕

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        CLBlock myBlock;
        {//临时作用域开始
            CLPerson *person = [[CLPerson alloc] init];
            person.age = 10;

             myBlock = ^{
                NSLog(@"---------%d",person.age);
            };
        }//临时作用域结束

        NSLog(@"-----------flag1");
    }
    return 0;
}

由于现在是ARC环境,myBlock属于强指针,因此在将block对象赋值给myBlock指针的时候,编译器会自动对block对象执行copy操作,因此赋值完成后,myBlock指向的是一个堆空间上的block对象副本

通过Clang重写

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  CLPerson *person;

  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, CLPerson *_person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};


static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  CLPerson *person = __cself->person; // bound by copy

                NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__p19yp82j0xd2m_1k8fpr77z40000gn_T_main_2cca58_mi_0,((int (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("age")));
            }



static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
            _Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
}




static void __main_block_dispose_0(struct __main_block_impl_0*src) {
            _Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
}




static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 
                                0, 
                                sizeof(struct __main_block_impl_0), 
                                __main_block_copy_0, 
                                __main_block_dispose_0
                              };



int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        CLBlock myBlock;

        {
            CLPerson * person = objc_msgSend(objc_msgSend(objc_getClass("CLPerson"), 
                                                          sel_registerName("alloc")
                                                          ), 
                                             sel_registerName("init")
                                             );


            objc_msgSend(person, 
                         sel_registerName("setAge:"), 
                         30
                         );


            myBlock = objc_msgSend(&__main_block_impl_0(__main_block_func_0, 
                                                        &__main_block_desc_0_DATA, 
                                                        person, 
                                                        570425344), 
                                   sel_registerName("copy")
                                   );


        }


    }


    return 0;
}

__main_block_desc_0结构体里面多了两个彩蛋

  • 函数指针copy,也就是__main_block_copy_0(),内部调用了_Block_object_assign()
  • 函数指针dispose,也就是__main_block_dispose_0(),内部调用了_Block_object_dispose()

捕捉对象类型的auto变量时__main_block_desc_0的变化

ARCCLPerson *person被认为是强指针,等价于_strong CLPerson *person,而弱指针需要显式地表示为__weak CLPerson *person。通过终端命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-9.0.0 main.m -o main.cpp,可以看到block的内捕获到的person指针如下

image

为了对比,我们再分别看一下下面三种 场景分别是什么情况的:

  • ARC环境-->堆上的block-->弱指针__weak CLPerson *person
  • ARC环境-->栈上的block-->强指针CLPerson *person
  • ARC环境-->栈上的block-->弱指针__weak CLPerson *person

ARC环境-->堆上的block-->弱指针__weak CLPerson *person】 案例如下

***********************main.m*************************
#import <Foundation/Foundation.h>
#import "CLPerson.h"

typedef void(^CLBlock)(void);


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLBlock myBlock;

        {//临时作用域开始
            __weak CLPerson * person = [[CLPerson alloc] init];
            person.age = 30;

            myBlock = ^{
                NSLog(@"---------%d",person.age);
            } ;

        }//临时作用域结束

        NSLog(@"-------------");

    }

    NSLog(@"------main autoreleasepool end-------");

    return 0;
}

block的底层结构如下

image

ARC->堆block->弱指针运行结果

运行结果显示堆上的block使用弱指针__weak CLPerson *person,没有影响person所指向对象的生命周期,出了临时作用域的之后就被释放了。

ARC环境-->栈上的block-->强指针CLPerson *person

***********************main.m*************************
#import <Foundation/Foundation.h>
#import "CLPerson.h"

typedef void(^CLBlock)(void);


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLBlock myBlock;

        {//临时作用域开始
            CLPerson * person = [[CLPerson alloc] init];
            person.age = 30;

            ^{
                NSLog(@"---------%d",person.age);
            } ;

        }//临时作用域结束

        NSLog(@"-------------");

    }

    NSLog(@"------main autoreleasepool end-------");

    return 0;
}

block底层结构如下 image

ARC->栈block->强指针运行结果

运行结果显示栈上的block使用强指针CLPerson *person,没有影响person所指向对象的生命周期,出了临时作用域的之后就被释放了。

ARC环境-->栈上的block-->弱指针__weak CLPerson *person

***********************main.m*************************
#import <Foundation/Foundation.h>
#import "CLPerson.h"

typedef void(^CLBlock)(void);


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLBlock myBlock;

        {//临时作用域开始
            __weak CLPerson * person = [[CLPerson alloc] init];
            person.age = 30;

            ^{
                NSLog(@"---------%d",person.age);
            } ;

        }//临时作用域结束

        NSLog(@"-------------");

    }

    NSLog(@"------main autoreleasepool end-------");

    return 0;
}

block底层结构为

image

ARC->栈block->弱指针运行结果

运行结果显示栈上的block使用弱指针__weak CLPerson *person,没有影响person所指向对象的生命周期,出了临时作用域的之后就被释放了。

Block类型

Block有3种类型

Block的类型

回顾一下程序的内存布局

  • 代码段 占用空间很小,一般存放在内存的低地址空间,我们平时编写的所有代码,就是放在这个区域
  • 数据段 用来存放全局变量
  • 堆区 是动态分配内存的,用来存放我们代码中通过alloc生成的对象,动态分配内存的特点是需要程序员申请内存和管理内存。例如OC中alloc生成的对象需要调用releas方法释放【MRC下】,C中通过malloc生成的对象必须要通过free()去释放。
  • 栈区 系统自动分配和销毁内存,用于存放函数内生成的局部变量

block的存放区域

(1) NSGlobalBlock(也就是_NSConcreteGlobalBlock)

如果一个block内部没有使用/访问 自动变量(auto变量),那么它的类型即为__NSGlobalBlock__,它会被存储在应用程序的 数据段

(2) NSStaticBlock(也就是_NSConcreteStaticBlock)

如果一个block有使用/访问 自动变量(auto变量),那么它的类型即为__NSStaticBlock__,它会被存储在应用程序的 栈区

(3) NSMallocBlock(也就是_NSConcreteMallocBlock)

__NSMallocBlock__调用copy方法,就可以转变成__NSMallocBlock__,它会被存储在堆区上

总结

block的类型总结

对每一种类型的block调用copy后的结果如下

img

在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,例如以下的情况

  • block作为函数参数返回的时候
  • 将block赋值给__strong指针的时候
  • block作为Cocoa API中方法名里面含有usingBlock的方法参数时
  • block作为GCD API的方法参数的时候

Block生命周期

NSConcreteStackBlock 是由编译器自动管理,超过作用域之外就会自动释放了。而 NSConcreteMallocBlock 是由程序员自己管理,如果没有被强引用也会被消耗。NSConcreteGlobalBlock 由于存在于全局区,所以会一直伴随着应用程序。

无论是MAC还是ARC

  • 当block为__NSStackBlock__类型时候,是在栈空间,无论对外面使用的是strong 还是weak 都不会对外面的对象进行强引用
  • 当block为__NSMallocBlock__类型时候,是在堆空间,block是内部的_Block_object_assign函数会根据strong或者 weak对外界的对象进行强引用或者弱引用。

其实也很好理解,因为block本身就在栈上,自己都随时可能消失,怎么能保住别人的命呢?

  • 当block内部访问了对象类型的auto变量时
  • 如果block是在栈上,将不会对auto变量产生强引用
  • 如果block被拷贝到堆上
    • 会调用block内部的copy函数
    • copy函数内部会调用_Block_object_assign函数
    • _Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
  • 如果block从堆上移除
    • 会调用block内部的dispose函数
    • dispose函数内部会调用_Block_object_dispose函数
    • _Block_object_dispose函数会自动释放引用的auto变量(release)
函数 调用时机
copy函数 栈上的Block复制到堆上
dispose函数 堆上的block被废弃时

weak的实现原理

在原对象释放之后,weak对象就会变成null,防止野指针。所以就输出了null了。

那么我们怎么才能在weakSelf之后,block里面还能继续使用weakSelf之后的对象呢?

究其根本原因就是weakSelf之后,无法控制什么时候会被释放,为了保证在block内不会被释放,需要添加_strong。

在block里面使用的_strong修饰的weakSelf是为了在函数生命周期中防止self提前释放。strongSelf是一个自动变量当block执行完毕就会释放自动变量strongSelf不会对self进行一直进行强引用。

__block 修饰符

__block修饰符原理:

编译器会将__block变量包装成一个结构体__Block_byref_age_0,结构体内部*__forwarding是指向自身的指针,内部还存储着外部auto变量的值

一开始,栈空间的block有一个__Block_byref_a_0结构体, 指向外部__Block_byref_a_0的地址, 其中它的__forwarding指针指向自身,

当block从栈copy到堆时,

堆空间的block有一个__Block_byref_a_0结构体, 指向外部__Block_byref_a_0的地址, 其中它的__forwarding指针指向自身

一般情况下,对被截获变量进行赋值操作需要添加 __block 修饰符(注意是赋值!!, 赋值≠使用)

NSMutableArray *array = [NSMutableArray array];
void(^Block)(void) = ^{
    [array addObject:@123];
}

//不需要添加 __block,因为是使用

当__block修饰外界变量时

int main(){

    __block int a = 10;
    void(^block)(void) = ^{
        printf("Felix %d ", a);
    };

    block();
    return 0;
}

将代码编译成C++源码

// 原代码
__block int a = 10;
// c++源码
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {
    (void*)0,
    (__Block_byref_a_0 *)&a, 
    0, 
    sizeof(__Block_byref_a_0), 
    10
};

可以看到 变量a 变成了 结构体类型__Block_byref_a_0

下面再看看结构体__Block_byref_a_0的构造

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

通过上面结构体的初始化和结构体的构造, 可以获得以下信息:

  1. __forwarding存放的是自己本身的地址
  2. 结构体内的a变量存放的是外部变量a的值

主结构体__main_block_impl_0的变化

如何从栈指向堆,并建立联系呢?

apple源码,如图:

copy->forwarding = copy; 就是将堆结构体的forwarding指针指向自身 src->forwarding = copy; 就是将栈结构体的forwarding指针指向堆结构体

这样,苹果工程师在背后悄悄地将block copy到了堆上, 而且栈上的block从未被我们利用过。

在看看block入口静态函数

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref
  (a->__forwarding->a)++;
}

通过当前栈空间主结构体上的__Block_byref_a_0结构体指针,访问指向堆空间的__forwarding成员,并获取堆空间上变量的值

当然,不仅__block修饰的变量会这样,前文的对象类型变量同样会在copy函数内部被转化成类似的结构体进行处理。

__block修饰的属性在底层会生成响应的结构体,保存原始变量的指针,并传递一个指针地址给block——因此是指针拷贝

__block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。

__block修饰的变量成了对象

__forwarding存在意义

不论在任何内存位置,都可以顺利访问同一个__block变量.

__block + 基本类型变量

__block + 对象类型变量

__block + __weak + 对象类型变量

Reference

1 深入研究 Block 捕获外部变量和 __block 实现原理

2 深入理解iOS的block

3 iOS中__block 关键字的底层实现原理

4 iOS探索 全方位解读Block

5 iOS - block原理解读(三)

探寻Block的本质(6)—— block的深入分析block的使用场景 大家应该都知道,如果想在block - 掘金 (juejin.cn)

results matching ""

    No results matching ""