学习MJ的视频课程,整理总结知识点–block的copy、访问对象类型变量的底层实现

本文结合block的特性,探讨ARC下block自动copy的时机;以及block内访问外部的对象类型变量时底层实现;

block的copy

在上一节的block学习中我们遇到,在ARC环境下,block有时会被自动拷贝到堆空间去,我们可以分析一下原因。

常见的场景就是block里面访问一个局部变量,根据上一节所学,MRC下访问auto变量的block是存储在栈上的,但是我们在ARC环境下演练测试发现它是存储在堆空间上。因为栈空间内存是系统自动做处理的,堆空间才是开发者手动控制。如果让block保留在栈空间,很可能当我们调用block的时候,它已经被系统自动回收了,肯定不是我们想要的结果,所以在ARC下block会被自动copy到堆空间上,有开发者控制它的释放时机(不过这些工作实际是由ARC机制处理了)

ARC下block进行自动copy的根本目的是为了防止block在栈空间上被不合时宜的自动释放,而此时调用被释放的block就会出错,而copy到堆空间就不会自动释放。

从这个根本原因出发,我们可以总结一下在ARC环境下,block在哪些场景会被自动copy到堆上。首先,我们知道需要copy操作的一般是NSStackBlock类型的block,而NSStackBlock类型的block的产生是因为block访问了auto变量。

所以我们可以推出一个关键点:在ARC下,访问了auto变量的block,默认都会进行copy操作,防止它在栈上被自动释放。包括以下几种:

  1. block作为函数返回值时(Masonry中)
  2. 将block赋值给__strong指针时
  3. block作为一些函数的参数时(cocoa API useringBlock参数)、(GCD API block参数)

对象类型的auto变量

在之前的测试中,我们更多的是用block访问基本类型的auto变量,现在我们研究block访问对象类型的auto变量的底层。

参见Demo,新建一个自定义类,研究它的实例对象释放时机

1
2
3
4
5
6
7
8
9
10
int main(int argc, const char * argv[]) {
@autoreleasepool {
{
MJPerson *person = [[MJPerson alloc] init];
person.age = 10;
}
// person dealloc
}
return 0;
}

默认情况下,person对象在出了大括号后,就会调用自己的dealloc方法进行释放。

当我们用一个block访问person的属性时

1
2
3
4
5
6
7
8
9
10
MJBlock block;
{
MJPerson *person = [[MJPerson alloc] init];
person.age = 10;

block = ^{
NSLog(@"---------%d", person.age);
};
}
// person not dealloc

此时发现person出了大括号的作用域,没有调用dealloc,直到block销毁时,person才销毁。

我们简化一下代码如下

1
2
3
4
5
6
MJBlock block;
MJPerson *person = [[MJPerson alloc] init];
person.age = 10;
block = ^{
NSLog(@"---------%d", person.age);
};

我们研究此时的block源码,分析为什么会是这样。clang编译后的block源码如下

1
2
3
4
5
6
7
8
9
10
11
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
MJPerson *person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, MJPerson *_person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

block结构体里面有person的指针,而block在堆空间,在ARC下,block会强引用着person,所以当block销毁时person才会销毁。

我们还可以验证在MRC环境下person的释放时机,此时block是在栈空间的。会发现在MRC下,person出了自己的大括号作用域后,就销毁了。

我们可与得出一个结论:栈空间上的block是不会对对象类型的auto变量强引用,堆空间上的block会对对象类型的auto变量强引用。

我们再使用weak类型的person实验一下此时person的释放时机

1
2
3
4
5
6
7
8
9
10
11
MJBlock block;
{
MJPerson *person = [[MJPerson alloc] init];
person.age = 10;

__weak MJPerson *weakPerson = person;
block = ^{
NSLog(@"---------%d", person.age);
};
}
NSLog(@"------");

会发现person在出了自己的大括号作用域后就销毁了,说明此时block是没有强引用着person对象,我们看下使用__weak时的block源码
此时用clang命令会报错cannot create __weak reference because the current deployment target does
因为__weak是属于运行时的特性,clang默认是编译期,我们要指定clang的runtime环境才能导出源文件。
修改clang命令如下:

1
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-9.0.0  main.m -o main2.cpp

我们看此时的block源码如下

1
2
3
4
5
6
7
8
9
10
11
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
MJPerson *__weak weakPerson;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, MJPerson *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

会发现block里面的person对象是MJPerson *__weak weakPerson;类型,从字面意思上理解就是block不强引用这个person对象了,这也就解释了为什么__weak的person对象在出了自己的大括号作用域后就销毁了,因为没有别的对象对它强引用了。

我们再看下此时block的desc结构体

1
2
3
4
5
6
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};

会发现此时的__main_block_desc_0结构体,相对于访问基本类型的auto变量时多了两个函数指针void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); void (*dispose)(struct __main_block_impl_0*);而这两个函数的实现如下

1
2
3
4
5
6
7
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_FIELD_IS_OBJECT时,会多两个函数实现,分别是_Block_object_assign_Block_object_dispose

实际上_Block_object_assign就是block决定是否引用person对象的关键,当block内部的person是MJPerson *__weak weakPerson;_Block_object_assign就也是用__weak关联这个person

小结

关于block访问对象类型的auto变量,做一个总结

  1. 如果block是在栈上,将不会对auto变量产生强引用

  2. 如果block被拷贝到堆上
    会调用block内部的copy函数
    copy函数内部会调用_Block_object_assign函数
    _Block_object_assign函数会根据auto变量的修饰符(strong、weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用

  3. 如果block从堆上移除
    会调用block内部的dispose函数
    dispose函数内部会调用_Block_object_dispose函数
    _Block_object_dispose函数会自动释放引用的auto变量(release)
    copy-and-dispose

参考和源码

Demo源码:
Block底层-Demo

评论