iOS底层原理探索Block本质(二)

面试题:

问:在24行打断点,person对象是否被释放?

在这里插入图片描述

按说,person的作用域是其附近的两个{},过了两个{}后,person对象应该被释放,而实际上,在24行断点处,person对象并没有消失。

问:为什么呢?

首先我们将程序运行,可以看到其运行过程:

24行打印block学习[2478:134123] ---------
25行打印block学习[2478:134123] 调用了block---10
26行结束打印block学习[2478:134123] YZPerson消失了

将main.m转化为底层代码后,我们进行分析,可以看到block的构成

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

可以看到,block内部捕获了局部变量YZPerson的值,相当于block内部有一个指针对person对象进行了强引用,从而保证了person对象没有消失。 在26行过后,block对象消失,因此,person对象也消失。

由于在ARC环境下,系统会帮我们做很多事情,我们需要具体看一下里面的一些细节,因此,我们切换到MRC环境下。

在MRC环境下:

在这里插入图片描述

在这里插入图片描述

我们发现一个有趣的现象:
在MRC环境下:
没有对block进行copy操作,person会被释放。
对block进行copy操作,person不会被释放。

首先,上述例子中,block是局部变量,而我们知道:局部变量是存储在栈区

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

由于访问量auto变量person,因此,其实存储类型是NSStackBlock类型。

[^{
    NSLog(@"调用了block---%d", person.age);
} copy];

由于访问量auto变量person,其实存储类型是NSStackBlock类型,又因为调用了copy,最终其存储类型是NSMallocBlock类型

当block是NSStackBlock类型时,不能拥有其内部的变量。
这是因为,其本身就是存储在栈区,是不稳定的。
而当执行copy操作后,其存储在堆区,可以拥有其内部的变量。

在ARC下,由于block指向对象是有强指针引用的,因此会默认对其进行copy操作,将block指向的对象存放在堆区,因此是可以拥有其内部的变量person。

在ARC环境下:

在这里插入图片描述

注意,23行调用的是person.age,而不是weakPerson.age

在这里插入图片描述

注意,23行调用的是weakPerson.age

一个现象:在block内部引用使用__weak修饰的auto变量weakPerson,在26行YZPerson消失了。这又是为什么呢?
当block进行了copy操作,其内部又经历了哪些方法和操作呢?

我们再次探究源码:

#import "YZPerson.h"
typedef void(^YZBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        YZBlock block;
        {
            YZPerson *person = [[YZPerson alloc] init];
            person.age = 10;
            block = ^{
                       NSLog(@"调用了block---%d", person.age);
            };
        }
        NSLog(@"---------");
        block();
    }
    return 0;
}

使用命令行指令

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-12.0.0 main.m 

其中:
-fobjc-arc表明是arc环境
-fobjc-runtime=ios-12.0.0需要用到运行时,版本12.0.0

转换为底层代码后,block里面的内容为

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

可以看出person是__strong修饰的。

#import "YZPerson.h"
typedef void(^YZBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        YZBlock block;
        {
            YZPerson *person = [[YZPerson alloc] init];
            person.age = 10;
            __weak YZPerson *weakPerson = person;
            block = ^{
                       NSLog(@"调用了block---%d", weakPerson.age);
            };
        }
        NSLog(@"---------");
        block();
    }
    return 0;
}

查看底层代码:

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

可以看出person是__weak修饰的。

当block内部没有引用外部局部变量的时候

block = ^{};
            
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

当block内部引用外部局部变量的时候

block = ^{
	NSLog(@"调用了block---%d", weakPerson.age);
};
            
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
};

两个函数的实现:

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

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

可以发现,相比block内部没有引用外部局部变量,__main_block_desc_0里面多了两个函数指针,copy和dispose

当block从栈拷贝到堆里面的时候,会自动调用 __main_block_copy_0 函数,在里面实现_Block_object_assign,在这个里面有调用外部引用的weakPerson,该调用是强指针或者弱指针是根据block定义里面的weakPerson类型做判断,追溯到上面,其实是代码中__weak YZPerson *weakPerson = person;__weak修饰起的作用。在我们的例子这,该调用是一个__weak弱指针调用。

同样,有创建就有消除,当堆上的block将移除的时候,会自动调用__main_block_dispose_0函数,在里面实现_Block_object_dispose,在这个里面同样会调用外部引用的weakPerson。

如果block是在栈上是NSStackBlock类型时,将不会对auto变量产生强引用

参考:当block是NSStackBlock类型时,不能拥有其内部的变量,因为,其本身就是存储在栈区,是不稳定的。而当执行copy操作后,其存储在堆区,可以拥有其内部的变量
当block内部访问了对象类型为auto的变量时候

如果block被拷贝到堆上
会调用block内部的copy函数
copy函数内部会调用_Block_object_assign函数
_Block_object_assign函数会根据auto变量的修饰符( __strong、 __weak、__unsafe_unretain )作出相应的操作,形成强引用或者弱引用。

如果block从堆上移除
会调用block内部的dispose函数
dispose函数内部会调用_Block_object_dispose函数
_Block_object_dispose函数会自动释放引用的auto变量
在这里插入图片描述

blcok内部引用__weak修饰的auto局部变量,在26行结束后,YZPerson被销毁的原因是因为,block内部对其进行的是__weak弱引用。

需要注意的是,如果block内部访问的局部变量为非对象类型,是不会生成copy和dispose函数的。

几个测试题b

在这里插入图片描述

block作为GCD参数的时候,会将block复制到堆上,而里面又引用了person局部变量的对象,因此会对block里面的person对象变量进行类似强引用功能,从而保证person在{}消失的时候不会消失。在3秒过后,GCD释放,从而person对象也释放。

在这里插入图片描述

block作为GCD参数的时候,会将block复制到堆上,而里面又引用了person局部变量的对象,但是,前面是__weak修饰的,因此会对block里面的person对象变量进行类似弱引用功能。因此,在{}执行完毕后,person就被销毁。

在这里插入图片描述

block作为GCD参数的时候,会将block复制到堆上,第二次使用block做GCD参数,会将block的引用做类似+1操作。第二个block里面引用了person局部变量的对象,因此会对第二个block里面的person对象变量进行类似强引用功能。因此,在第二个block执行完毕后,person才被销毁。

在这里插入图片描述

block作为GCD参数的时候,会将block复制到堆上,第二次使用block做GCD参数,会将block的引用做类似+1操作。第二个block里面引用了person局部变量的对象,但是,前面是__weak修饰的,因此会对block里面的person对象变量进行类似弱引用功能。因此,在{}执行完毕后,person就被销毁。

在这里插入图片描述

block作为GCD参数的时候,会将block复制到堆上。block里面引用了person局部变量的对象,因此会对第一个block里面的person对象变量进行类似强引用功能。
第二次使用block做GCD参数,会将block的引用做类似+1操作。第二个block里面引用了person局部变量的对象,但是是__weak类型的,因此会对第二个block里面的person对象变量进行类似弱引用功能。
因此,在第一个block执行完毕后,person就被销毁。
在这里插入图片描述

block作为GCD参数的时候,会将block复制到堆上。block里面引用了person局部变量的对象,但是是__weak类型的,因此会对第一个block里面的person对象变量进行类似弱引用功能。
第二次使用block做GCD参数,会将block的引用做类似+1操作。第二个block里面引用了person局部变量的对象,因此会对第二个block里面的person对象变量进行类似强引用功能。
因此,在第二个block执行完毕后,person才销毁。一个简单的栗子:

问:下面的打印结果是什么?

int age = 10;
YZBlock block = ^{
    NSLog(@"---%d---", age);
};
age = 20;
block();

打印结果是:---10---

原因很简单,是因为block将变量age捕获到block内部,并且由于是auto变量,捕获的是值。
虽然age=20,但是在编译的时候,block已经将age=10的值捕获进去。因此,打印的是10。

在这里插入图片描述

问:为何不能直接在block内部修改外部局部变量呢?

int age是在main函数里面创建的;

block内部的age是定义在__main_block_func_0里面的;
不能通过修改__main_block_func_0里面的age从而去反向改变main里面的age值。

另外,捕获其实是新建一个同名变量,因此,block里面的age是一个新建的age,其值是10。

从下面的例子可以看出,block内部的变量跟block外部的变量,不是同一个变量。
在这里插入图片描述

可以看出:
16行、22行的age地址相同,也就是block外部的age是同一个
19行、20行的age地址相同,也就是block内部的age是同一个
而19-20行的age地址跟16、22行的age地址不同,说明block内部的age变量与block外部的age变量不是同一个。

既然block内外age变量不是同一个,就不能通过修改block内部的age变量,去修改block外部的age变量。
至于为什么内部的age变量也不能修改,是因为 block内部的捕获新建是隐式的,在外部看来并没有新建一个age,block内外的age就是同一个age。为了避免用户想去通过修改block内部的age而去修改外部的age值,苹果直接将block内部的age做了限制,只能使用,禁止赋值。

类似的有局部变量NSMuttableArray *array,在block内部只能使用,不能赋值。
也就是,只能做[array addObject:];等操作
不能做,array = nil;或者 array = [NSMuttableArray array];等操作
在这里插入图片描述

在这里插入图片描述

使用static修饰的局部变量就可以进行修改。
这是因为,使用static修饰的局部变量,block内部捕获的是指针,因此,可以通过指针修改外部age的值,这我们前面讲过。

当然,全局变量是可以修改的,这个就不用说了。

现在我们还可以通过另外一种方法,进行修改外面布局变量的值,这就是__block。
在这里插入图片描述

问:为什么使用__block修饰局部变量就可以修改age的值呢?

__block只能修饰auto局部变量,不能修饰 全局变量 和 static修饰的静态变量。

通过底层源码可以看到:

在这里插入图片描述

首先,我们看下__block int age = 10;转换为:

__attribute__((__blocks__(byref))) __Block_byref_age_0 age = 
{
(void*)0,
(__Block_byref_age_0 *)&age, 
0, 
sizeof(__Block_byref_age_0), 
10
};

简化后

__Block_byref_age_0 age = {
0,
&age, 
0, 
sizeof(__Block_byref_age_0), 
10
};

可以看出age最后被转换为__Block_byref_age_0类型的age结构体。那么__Block_byref_age_0这是个什么类型的东西呢?
从源码可以看出__Block_byref_age_0的定义是

struct __Block_byref_age_0 {
  void *__isa;//isa指针,代表该类型是一个对象
  __Block_byref_age_0 *__forwarding;//接收自己的地址
 int __flags;
 int __size;//改类型值的大小
 int age;//__block修饰的变量age10
};

那么两个结合到一起,可以看到那些值分别代表的意义。
YZBlock block = ^{
            age = 30;
            NSLog(@"---%d---", age);
        };
被转换为:

YZBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));

简化后
YZBlock block = &__main_block_impl_0(
__main_block_func_0, 
&__main_block_desc_0_DATA, 
&age, 
570425344);

__main_block_impl_0的定义是

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看出,block里面并没有直接捕获age值,而是新创建了一个__Block_byref_age_0类型的age对象。

通过其构造函数可以看到其赋值过程

__main_block_impl_0(
__main_block_func_0, 
&__main_block_desc_0_DATA, 
&age, 
570425344);

__main_block_impl_0(
void *fp, 
struct __main_block_desc_0 *desc, 
__Block_byref_age_0 *_age, 
int flags=0)

在这里插入图片描述

那,里面的age=30;是如何修改的呢?
首先,block会将里面的内容封装到__main_block_func_0函数里面,然后通过(age.__forwarding->age) = 30;根据__Block_byref_age_0类型的age里面自己的__forwarding指针,获取里面的age,修改为30;
这里,由于block定义里面是拿的类型为__Block_byref_age_0变量名为age的指针,由于是指针,因此,我们可以拿到里面的值,也可以修改里面的值。
在这里插入图片描述

问:下面的代码为何会报错?

在这里插入图片描述

static修饰变量的时候报错,错误提示是:
Initializer element is not a compile-time constant

这是因为,被static修饰的局部变量,会延长生命周期,使得周期与整个应用程序有关; 只会分配一次内存;程序一运行,就会分配内存,而不是运行到那才分配。

而[[YZPerson alloc] init];是在运行到此处的时候才会分配内存。会有冲突,因此,不能这么写。
在这里插入图片描述

在这里插入图片描述

可以看到,使用static修饰的局部变量,捕获的是指针YZPerson **person;

在这里插入图片描述

在这里插入图片描述

上图表明,person、array等指针类型,由于auto类型,捕获进去的是值,因此,在block里面捕获的是指针。
指针指向的内容可以修改,也就是person.age, [array addObject:@“4”];都可以修改。
但是,person和array指针本身是不可以修改的。因此,person = nil; array = nil;是不可以执行的。

这个试验引出了一个常问的面试题:

Block与数组的关系

从上面的结果来看:
auto类型的array,在block内部可以进行添加删除元素操作,但不可以进行array = nil;操作
static类型的array,捕获的是指针,也可以进行添加删除元素操作,可以进行array = nil;操作
__block,也是对指针操作,也可以进行添加删除元素操作,可以进行array = nil;操作

原文地址:https://www.cnblogs.com/r360/p/15775395.html