Block深入学习笔记

2019-06-11  本文已影响0人  妖精的菩萨

前言

block是日常iOS开发高频率使用的闭包,之前也看过不少文章,但是一直疏于总结,今日再次深入研究一下,并记录其过程。

Block结构定义

通过clang编译,去除block的相关嵌套(可以去除的原因是因为结构体本身并不带有任何额外的附加信息),得到如下结构定义:

struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};
struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

分析可知,block实际包含6部分:

NSGlobalBlock -> __NSGlobalBlock -> NSBlock -> NSObject -> null

Block源码实现

为了研究编译器是如何实现 block 的,我们需要使用 clang。clang 提供一个命令,可以将 Objetive-C 的源码改写成 c 语言的,借此可以研究 block 具体的源码实现方式。该命令是:

clang -rewrite-objc 文件地址

one. 未捕获外界变量

我们在main.m文件中写如下代码:

void testBlock(){
    void(^myBlock)(void) = ^{};
    myBlock();
}

编译生成main.cpp,内部核心代码如下:

//block的结构
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;//指向调用函数的地址
};

//主要函数
struct __testBlock_block_impl_0 {
  struct __block_impl impl;
  struct __testBlock_block_desc_0* Desc;//block描述信息
  __testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//block的实现。{}里面的代码。
static void __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself) {
}

//block描述信息
static struct __testBlock_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __testBlock_block_desc_0_DATA = { 0, sizeof(struct __testBlock_block_impl_0)};
void testBlock(){
    void(*myBlock)(void) = ((void (*)())&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
}

通过主要函数切入分析:

1、如下,由于clang 改写的具体实现方式和 LLVM 不太一样,并且这里没有开启 ARC。所以打印的为栈block。开启ARC后、如果没有捕获外界变量,则为全局block,反之为堆block

impl.isa = &_NSConcreteStackBlock;

2、impl为函数指针,指向此处的_testBlock_block_func_0,代表block内部的函数实现。

3、desc__testBlock_block_desc_0结构体,代表当前block的附加信息,包括结构体大小,需要copy和dispose的变量列表等。

4、flags:标记这个block此时的状态,因为C语言是静态语言,无法获取block此时的状态,那么就难以去管理其的生命周期,这里的flag就是标记其此时的状态.这里的flag其实不止是对其生命周期有管理,也有存储了里面的函数签名,是否全局等信息。

[图片上传失败...(image-f59630-1560243731607)]

two. 复制外界变量

我们通过修改main.m文件的代码,增加变量a。

#include <stdio.h>
void testBlock(){
    int a = 10;
    void(^myBlock)(void) = ^{
        printf("a = %d\n",a);
    };
    myBlock();
}

如下可见,函数实现的地方发生了变化,其中增加了一个变量a。其实这个变量是在声明block时,被复制到函数中的。所以当修改函数内部a的值,并不会影响到外部值的变化。

static void __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
  printf("a = %d\n",a);
}

three. __block捕获外界变量

捕获的本质意义是因为作用域的问题,当涉及到跨函数访问的时候,便需要捕获。而全局变量一直在内存中,所以不需要捕获。

通过修改main.m中的代码,给a添加__block关键字修饰,并且在函数内部修改实现。

#include <stdio.h>
void testBlock(){
    __block int a = 10;
    void(^myBlock)(void) = ^{
        printf("a = %d\n",a);
        a = 11;
    };
    myBlock();
    printf("a = %d\n",a);
}

编译得:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
//保存捕获的外部变量。
struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

struct __testBlock_block_impl_0 {
  struct __block_impl impl;
  struct __testBlock_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;//clang编译下的都是stackBlock
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref
        printf("a = %d\n",(a->__forwarding->a));
        (a->__forwarding->a) = 11;
    }
static void __testBlock_block_copy_0(struct __testBlock_block_impl_0*dst, struct __testBlock_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

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

static struct __testBlock_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __testBlock_block_impl_0*, struct __testBlock_block_impl_0*);
  void (*dispose)(struct __testBlock_block_impl_0*);
} __testBlock_block_desc_0_DATA = { 0, sizeof(struct __testBlock_block_impl_0), __testBlock_block_copy_0, __testBlock_block_dispose_0};
void testBlock(){
    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
    void(*myBlock)(void) = ((void (*)())&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    printf("a = %d\n",(a.__forwarding->a));
}

分析可知:

对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的。

1、增加了__Block_byref_a_0结构体,用来保存我们需要捕获的外部变量。结构体的中值有:

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};
static void __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself) {
        __Block_byref_a_0 *a = __cself->a; // bound by ref
        printf("a = %d\n",(a->__forwarding->a));
        (a->__forwarding->a) = 11;
}

这样取值的原因是因为:

a是个局部变量保存在栈区,而block是结构体在堆区,试想如果a在方法执行完毕后那么原值会被系统释放,此时如果block被调用如果是直接a->a(即是访问上图中结构体里的int a,这个int a 就是保存的外界的栈中的a),那么a能取到值吗?肯定不可以的。

__forwarding指针的作用就是标记当block被copy到堆中,在捕获的外界变量被__block标记后,__forwarding指针就会将其原本指向栈中变量的地址转为指向堆中的a结构体,此时里面的a就会转为堆里的a.所以通过a->__forwarding->a去取值是能够保证正常取到a的.

这也就是为什么__block标记后就能修改原a,而不进行这样标记就无法修改值,因为c语言里的a是基本变量,基本变量的传值是值传递.而被__block进行捕获后已经转为了对象,拷贝进堆区后传到block内的其实是在堆区的a地址,这里的传值是地址传递!

Block类型分析

block的类型,取决于isa指针,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型。在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。

void testBlockClass(){
    int age = 1;
    void (^block1)(void) = ^{
        NSLog(@"block1");
    };
    
    void (^block2)(void) = ^{
        NSLog(@"block2:%d",age);
    };
    
    NSLog(@"%@/%@/%@",[block1 class],[block2 class],[^{
        NSLog(@"block3:%d",age);
    } class]);
}

通过打印得到三种不同block类型:

__NSGlobalBlock__/__NSMallocBlock__/__NSStackBlock__

__NSConcreteStackBlock

顾名思义,栈block放在栈区。通过在ARC环境下,给某个文件添加-fno-objc-arc标识,使文件能在MRC环境下运行。

在block中有捕获外界变量的则为_NSConcreteStackBlock

__block int a = 10;
void(^myBlock)(void) = ^{
    printf("a = %d\n",a);
};
NSLog(@"%@",[myBlock class]);

__NSMallocBlock

顾名思义,堆block放在堆区。在MRC环境下,继续上面代码,block中如果捕获了外界变量,此时通过打印[myBlock copy],得到的结果便是__NSMallocBlock。这里再次copy,便只是增加了堆block的引用计数而已。

值得注意的是如果在ARC中,此时只要捕获外界变量,那么就直接是MallocBlock,因为ARC下会自动将这个栈block copy到堆中。

__NSGlobalBlock

全局block存放在数据区。如果block未捕获外界变量,则为__NSGlobalBlock,这时对block进行copy将不再发生任何变化。

Block常见问题

我们日常在使用block的时候也会遇到各种问题,通过上文的底层剖析,对一系列问题来进行原理性解释,更加巩固知识记忆。

Q1:确定下面代码的输出值
int age=10;
void (^Block)(void) = ^{
    NSLog(@"age:%d",age);
};
age = 20;
Block();

根据上面two. 复制外界变量介绍可知,block在声明时就将外部变量复制进来,所以之后外部变量值的更改不会影响内部的值。所以这里打印依旧是10.

Q2:确定下面代码的输出值
int age = 10;
static int num = 25;
void (^Block)(void) = ^{
    NSLog(@"age:%d,num:%d",age,num);
};
age = 20;
num = 11;
Block();

这里考察了外部变量修改是否对内部造成影响,因为age到block中是值拷贝,同上文,所以外部的修改不会影响内部的打印。而static变量为指针传递,所以会影响。

变量age可能会自动销毁的,内存可能会小时,所以不能采用指针拷贝。而num一直在内存中。

综上,这里的打印的结果是10,11。

Q3:block访问self是否需要捕获?

会,self是当调用block函数的参数,参数是局部变量,self指向调用者。

Q4:block访问成员变量是否需要捕获?

会,成员变量的访问其实是self->xx,先捕获self,再通过self访问里面的成员变量

Q5:在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上的几种情况?
Q6:当block内部访问了对象类型的auto变量时,是否会强引用?

如果block在空间,不管外部变量是强引用还是弱引用,block都会弱引用访问对象。

如果block在空间,如果外部强引用,block内部也是强引用;如果外部弱引用,block内部也是弱引用。

这里主要分析堆block

1、如果block被拷贝到堆上。

会调用block内部的copy函数 -> 函数内部调用_Block_object_assign -> _Block_object_assign根据变量原有修饰符(__strong、__weak、__unsafe_unretained)作出相关操作,来辨别是否采用强弱引用。(注意:这里仅限于ARC时会retain,MRC时不会retain)。

2、如果block从堆上移除

会调用block内部的dispose函数 -> 函数内部调用_Block_object_dispose -> dispose函数会自动释放引用的auto变量。

Q7:weak 在使用clang转换OC为C++代码时,可能会遇到以下问题:
cannot create __weak reference in file using manual reference

此时需要更改编译指令,支持ARC、指定运行时系统版本,如:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
Q8:判断gcd引用的对象何时销毁

1、直接引用。

    Person *person = [[Person alloc] init];
    person.age = 10;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"age:%d",person.age);
    });
    NSLog(@"touchesBegan");

打印结果:

touchesBegan ——> age:10 ——> Person-dealloc

分析:gcd的block默认也会做copy操作,这里的block为堆block,会对person对象强引用。直到block销毁时对象才销毁。

2、__weak引用。

    Person *person = [[Person alloc] init];
    person.age = 10;
    __weak Person *weakPerson = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"age:%p",weakPerson);
    });
    NSLog(@"touchesBegan");

打印结果:

touchesBegan --> Person-dealloc --> age:0x0

这里道理很简单,block内部弱引用,当外部函数作用域结束后,person便销毁了。所以内部打印为空。

3、双重gcd

    Person *person = [[Person alloc] init];
    person.age = 10;
    
    __weak Person *weakPerson = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC)),
                   dispatch_get_main_queue(), ^{
                       
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"2-----age:%p",person);
        });
        NSLog(@"1-----age:%p",weakPerson);
    });

    NSLog(@"touchesBegan");

打印结果:

touchesBegan -> 1-----age:0x604000015eb0 -> 2-----age:0x604000015eb0 -> Person-dealloc

分析:只要内部有强引用,都会等到强引用部分打印完后才会释放。如果弱引用部分迟于强引用部分执行,那弱引用部分打印为空。

Q9:block内部可以向可变数组添加元素吗?

答案是可以的。如果可变数组已经生成,这时它在内存中的地址便可以确定,添加元素只是使用数组指针,并未发生修改。

Q10:被__block修饰的对象类型在block上如何操作的?

1、当__block变量在栈上、不会对指向的对象产生强引用。

2、copy到堆中时、结论如上文Q6.

Q11:如何循环引用?

这恐怕是我们日常开发中最经常遇到的问题了,因为循环引用所造成的内存泄漏致使内存只涨不跌。解决问题大致有以下三种方式:

1、__weak __strong:这里的__strong为了防止vc的提前释放,导致block内部使用的vc为空,因此通过强引用使vc的生命周期也能通过block内部管理。

__weak ViewController *weakSelf = self;
    void(^myBlock)(void) = ^{
        __strong ViewController *strongSelf = weakSelf;
        NSLog(@"name == %@",strongSelf.name);
    };

2、通过__block:这里需要在block里面当不需要使用vc的引用属性的时候要在生命作用域里手动将其置空,并至少保证要将其调用一次。

__block ViewController *blockSelf = self;
    void(^myBlock)(void) = ^{
        NSLog(@"name == %@",blockSelf.name);
        blockSelf = nil;
    };
    myBlock();

3、将VC传参:前面有提过block也是匿名函数,如果将vc作为形参传入block中,那么其vc指针的生命周期就只在block内部,当block执行完毕其指针就会被释放,也就是说,其引用次数也会被-1,block持有vc的引用链就会被断掉,也就不存在循环引用问题了。

void(^myBlock)(ViewController *vc) = ^{
        NSLog(@"name == %@",vc.name);
    };
myBlock(self);

其它更多问题参考block本质,本文只是摘取了部分。后续有新的block疑问可以添加到此文中。

参考链接

1、唐巧-谈Objective-C block的实现

2、https://www.galloway.me.uk/2013/05/a-look-inside-blocks-episode-3-block-copy/

上一篇下一篇

猜你喜欢

热点阅读