Block 底层原理(一)

2021-01-12  本文已影响0人  Jax_YD

block的本质是对象函数结构体

一、block 定义

block表达式语法

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

1、无参数,无返回值

// 1、无参数,无返回值
void(^block)(void) = ^ {
    NSLog(@"block");
};
block();

2、有参数,无返回值

// 2、有参数,无返回值
void(^block)(int, NSString*) = ^(int a, NSString *name){
    NSLog(@"%@ -- %d", name, a);
};
block(10, @"block");

3、有参数,有返回值

// 3、有参数,有返回值
int(^block)(int, int) = ^(int a, int b){
    NSLog(@"block -- %d", a+b);
    return a+b;
};
block(10, 20);

4、无参数,有返回值

// 4、无参数,有返回值
int(^block)(void) = ^{
    NSLog(@"block -- 40");
    return 40;
};
block();

5、在开发中我们还可以用typedef定义block

typedef int(^block)(int, NSString*);

可以这样使用:

block Myblock = ^(int a, NSString *name){
    NSLog(@"%@ -- %d", name, a);
    return 40;
};
NSLog(@"%d", Myblock(10, @"block"));

也可以定义成属性:

@property (nonatomic, copy) block Myblock;

二、block的分类

(1)block不访问外部变量(包括栈和堆中的变量)
此时block既不在栈中,也不在堆中,在代码段中。ARCMRC下都是如此。
此时为全局block_NSConcreteGlobalBlock

void(^block)(void) = ^{
};
NSLog(@"%@", block);
/*输出结果为*/
<__NSGlobalBlock__: 0x100004030>

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

int a = 10;
void(^block)(void) = ^{
    NSLog(@"%d", a);
};
NSLog(@"%@", block);
/*输出结果为*/
<__NSStackBlock__: 0x7ffeefbff3e8>
int a = 10;
void(^block)(void) = ^{
    NSLog(@"%d", a);
};
NSLog(@"%@", block);
/*输出结果为*/
<__NSMallocBlock__: 0x1040508b0>
int a = 10;
void(^ __weak block)(void) = ^{
    NSLog(@"%d", a);
};
NSLog(@"%@", block);
/*输出结果为*/
<__NSStackBlock__: 0x7ffeefbff3e8>

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

typedef int(^myblock)(int);

myblock func(int a) {
    return ^(int b) {
        return a * b;
    };
}

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

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

将block从栈区复制到堆区非常想好CPU,所以当block设置在栈上也能使用时,就不要复制了,因为此时的复制只是在浪费CPU资源。
block的复制操作,执行的是Copy实例方法。不同类型的block使用的Copy方法的效果如下:

block的类型 副本源的配置存储区域 复制效果
_NSConcreteGlobalBlock 程序的数据区域 什么也不做
_NSConcreteStackBlock 栈区 从栈区复制到堆区
_NSConcreteMallocBlock 堆区 引用计数增加

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

三、block 底层分析

int a = 10;
void(^ block)(void) = ^{
    NSLog(@"%d", a);
};
NSLog(@"%@", block);

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

clang -rewrite-objc XXX.m
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int a = 10;
        void(* block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_w__pbdfr2t16xx3pkwg63c2y5m80000gn_T_main_c75271_mi_1, block);
    }
    return 0;
}

我们从main函数中提取一下block

void(* block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));

/*简化一下,去除强制转换*/
void(* block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a); ///构造函数

可以看到构造函数名为__main_block_impl_0
下面我们再寻找一下__main_block_impl_0

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

1、block自动捕获外部变量

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a; ///编译器自动生成的名字相同的变量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy 值拷贝 

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_w__pbdfr2t16xx3pkwg63c2y5m80000gn_T_main_ab7cb4_mi_0, a);
        }

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

__block 原理

__block int a = 10;
        void(^ block)(void) = ^{
            a++;
            NSLog(@"%d", a);
        };
        block();

下面我们再通过clang来观察一下,底层代码有了什么变化。

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

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref 指针拷贝
            /// 等同于外界的 a++
            (a->__forwarding->a)++;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_w__pbdfr2t16xx3pkwg63c2y5m80000gn_T_main_b21337_mi_0, (a->__forwarding->a));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

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; 
        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
        void(* block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

总结:__block修饰外界变量的时候:
1、外界变量会生成__Block_byref_a_0结构体
2、结构体用来保存原始变量的指针(可以在上面编译后的代码中找到)
3、将变量生成的结构体对象指针地址传递给block,然后在block内部就可以对外界变量进行修改了。

接下来,在给大家看一个东西:

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

可以看到,__forwarding是一个指向自己本身的指针(自身为结构体)。
那么,在Copy操作之后,既然__block变量也被Copy到堆区上了,那么访问该变量是访问上的还是上的呢?这个时候我们就要来看一下,在Copy过程中__forwarding的变化了:

image.png
可以看到,通过__forwarding,无论是在block中,还是block外访问__block变量,也不管该变量是在上或是在上,都能顺利的访问同一个__block变量。
注意:这里与上面的结论并不矛盾。大家要主要到局部变量a__block修饰之后,会变成__Block_byref_a_0结构体对象。所以无论是在栈区还是在堆区,只要__forwarding指向的地址一样,那么就可以在block内部修改外界变量。这里大家要仔细观察一下__Block_byref_a_0结构体

例题:

int a = 10;
int *p = &a;
NSLog(@"开始 a == %d,  s == %p", a, &a);
NSLog(@"开始 p == %d,  s == %p", *p, p);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
     (*p) = 100;
     NSLog(@"异步 a == %d,  s == %p", a, &a);
     NSLog(@"异步 p == %d,  s == %p", *p, p);
});

///打印结果
开始 a == 10,  s == 0x7ffeebb2a0ac
开始 p == 10,  s == 0x7ffeebb2a0ac
异步 a == 10,  s == 0x600003a26c68
异步 p == 0,  s == 0x7ffeebb2a0ac
image

当然这也是一个释放时机的问题,如果有很多业务要处理,可能打印*p的时候,对应的内存地址还没有释放。

上一篇下一篇

猜你喜欢

热点阅读