面试题:
问:在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;操作