学习MJ的视频课程,整理总结知识点–__block的底层实现

本文根据前面所学的block的特性,结合Demo来讲解关于block的底层的一些东西。包括block的底层源码,

__block

关于block还要一个比较常见的场景就是block修饰,这一部分参见[block底层-Demo](https://cdn.ticsmatic.com/source/2020-05-21/diceng/day09/Interview04-__block.zip)

1
2
3
4
5
6
7
int age = 10;
// __block int age = 10;
void (^block)(void) = ^{
age = 20;
NSLog(@"%d", age);
};
block();

我们知道如果不加__block,编译器是会报错。根据前面所学的block变量捕获,我们知道当block访问的外部变量是auto类型时,如果这个变量是基本类型,block会先把这个变量捕获到内部,所以block内部的age变量和外边的age此时已经不属于同一块内存空间了,自然就不能修改。

那怎么才能修改age的值,有以下两种方法
1.用static或全局变量修饰age变量
2.用__block修饰age变量

第1种方法我们我们知道此时block内部要么捕获age的指针地址,要么不捕获age,直接访问age,都是可以对age进行修改。
第2种加上__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;
__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_byref_age_0的结构如下

1
2
3
4
5
6
7
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};

而此时block内部的代码如下

1
2
3
4
5
6
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref

(age->__forwarding->age) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_k2_y136z2p908718gwl0q6xh92w0000gn_T_main_149db6_mi_0, (age->__forwarding->age));
}

定义__block int age = 10;转化为源码如下

1
2
3
4
5
6
7
__Block_byref_age_0 age = {
(void*)0,
(__Block_byref_age_0 *)&age,
0,
sizeof(__Block_byref_age_0),
10
};

我们发现,用block修饰后,age被包装成`Block_byref_age_0 *age;,变为结构体,而这个结构体内部存放的有age的值。block内部的age变为Block_byref_age_0 *age; // by ref。 block函数实现部分age->forwarding指向自己的结构体地址,可以通过(age->forwarding->age) = 20;`修改内部age的值。这也解释为什么block可以修改值的本质原因。

概述一下block修饰的作用:
__block修饰的变量会被包装成一个对象(`
Block_byref_xx_0),这个对象的结构体内部有用于存储真实的变量的成员。block内部有__Block_byref_xx_0成员变量用来访问和修改变量xx`。

__block修饰符小结:

  1. __block可以用于解决block内部无法修改auto变量值的问题
  2. 编译器会将block变量包装成一个对象(`Block_byref_xx_0`)
  3. __block不能修饰全局变量、静态变量(static)

再用一张图片做一下总结
__block修饰的变量

__block修饰的变量的地址

我们通过下面这段代码研究输出的age的地址

1
2
3
4
5
6
__block int age = 10;
void (^block)(void) = ^{
age = 20;
NSLog(@"%d", age);
};
NSLog(@"%p", &age); // p/x &(blockImpl->age->age)

这段代码的底层源码上面我们已经分析过了,block内部有一个__Block_byref_age_0 *age; // by ref的成员变量,而__Block_byref_age_0结构体内部有一个int age;的成员变量。那么上面的代码输出的age地址是__Block_byref_age_0的地址,还是__Block_byref_age_0内部的int age的地址?

其实我们分析也可以得知,输出的应该是__Block_byref_age_0内部的int age的地址。
一方面OC的语言设计者是不希望暴露block的内部细节给开发者,因为开发者平时没必要关注内部的细节。还有KVO动态生成子类,但是返回的class还是原来的类也是这样,为了屏蔽这个动态的子类。另一方面,我们在block内部给age赋值时,block源码内部也是通过结构体拿到int age进行赋值操作,所以读取的话,理论上应该也是返回这个int age

我们可以通过实现block的源码来验证一个观点,参见__block转结构体-Demo

__block内存管理

如果认真看__block编译后的源码会发现,__block修饰的变量的底层源码和block访问对象类型的auto变量的底层源码有很多的相似之处。

参见对比源码中__block底层-Demomain.cpp文件

比如__block的变量会被包装成对象,拥有这个对象,对这个对象进行内存管理。block源码内部都会有copy和dispose函数等。block内部都会通过_Block_object_assign函数引用者block要访问的外部变量。block销毁时,也都会调用_Block_object_dispose函数进行解除引用。

但是也是有一些不同点,对__block的内存管理可以总结如下:

  1. 当block在栈上时,并不会对__block变量产生强引用

  2. 当block被copy到堆时

    • 会调用block内部的copy函数
    • copy函数内部会调用_Block_object_assign函数
    • _Block_object_assign函数会对__block变量形成强引用(retain)
    • 当block拷贝到堆上时,内部访问的变量如果在栈上也会拷贝到,如果在堆上,引用计数会+1。这样来达到block来管理内部访问的变量
      __block变量01)__block变量02
  3. 当block从堆中移除时

__forwarding

我们获取在前面已经发现,__block变量内部默认有一个__forwarding指针,而且在block实现内部,比如上面我们修改age的值时,也是通过__forwarding来访问age的((age->__forwarding->age) = 20;)。

为什么不直接通过结构体访问里面的age,而是间接通过__forwarding指针来访问age

__forwarding01
__forwarding02

对象类型的auto变量、__block变量小结

当block在栈上时,对它们都不会产生强引用

当block拷贝到堆上时,都会通过copy函数来处理它们

  1. __block变量(假设变量名叫做a)
    _Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
  2. 对象类型的auto变量(假设变量名叫做p)
    _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

当block从堆上移除时,都会通过dispose函数来释放它们
__block变量(假设变量名叫做a)
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

对象类型的auto变量(假设变量名叫做p)
_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

__block修饰对象类型

1
2
__block MJPerson *weakPerson = person;
__block __weak MJPerson *weakPerson = person;

当__block变量在栈上时,不会对指向的对象产生强引用

当__block变量被copy到堆时

  1. 会调用__block变量内部的copy函数
  2. copy函数内部会调用_Block_object_assign函数
  3. _Block_object_assign函数会根据所指向对象的修饰符(strong、weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain

如果block变量从堆上移除
会调用
block变量内部的dispose函数
dispose函数内部会调用_Block_object_dispose函数
_Block_object_dispose函数会自动释放指向的对象(release)

循环引用

因为block默认会对访问的对象进行强引用,如果此时对象已经对这个block强引用了,很容易就造成循环引用,从而导致内存泄露。如下

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

这段测试代码就会有循环引用的问题,因为block内部访问对象类型的person的变量,block此时默认会强引用这个person对象,而这个block又是person的成员变量,所以person会强引用着block。这就出现了循环引用。

ARC下循环引用的解决

__weak

通常的解决方法就是不让block强引用person对象,用__weak修饰person对象,如下

1
2
3
4
5
6
7
MJPerson *person = [[MJPerson alloc] init];
person.age = 10;
__weak MJPerson *weakPerson = person;
// __weak typeof(person) weakPerson = person;
person.block = ^{
NSLog(@"age is %d", weakPerson.age);
};

这样就打破了循环引用了,对象就可以正常的释放了。

__unsafe_unretained

解决循环引用还有另一种方法,就是使用__unsafe_unretained,如下

1
2
3
4
__unsafe_unretained typeof(person) weakPerson = person;
person.block = ^{
NSLog(@"age is %d", weakPerson.age);
};

使用__unsafe_unretained,block就不会对person产生强引用。但是它不安全,体现在当block内的weakPerson销毁时,block内指针存储的地址值不变,此时访问weakPerson就可能出现坏内存访问的问题。而用__weak时,指向的对象销毁时,会自动让指针置为nil。

1
2
// __weak:不会产生强引用,指向的对象销毁时,会自动让指针置为nil
// __unsafe_unretained:不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变

___block

解决循环引用还有一种方法就是使用___block修饰,如下:

1
2
3
4
5
6
__block typeof(person) weakPerson = person;
person.block = ^{
NSLog(@"age is %d", weakPerson.age);
weakPerson = nil;
};
person.block();

分析:
此时内部会有3个对象,分别是block对象、block对象、block对象内部的person对象。
其中block对象会强引用着block对象,block对象内部会强引用着person对象,但是我们上面的代码,在block的实现里面,当block内代码走完时,手动把内部的person指针置为nil,此时block就不会对person对象强引用,就可以打破循环引用了。
![使用
block打破循环引用](https://cdn.ticsmatic.com/img/2020-05-21/15900751183030.png)

MRC下循环引用的解决

MRC不支持__weak,

  1. 可以使用__unsafe_unretained
1
2
3
4
__unsafe_unretained typeof(person) weakPerson = person;
person.block = ^{
NSLog(@"age is %d", weakPerson.age);
};
  1. 可以使用_block,因为block修饰对象类型的变量,在MRC下是不会自动持有对象的
    1
    2
    3
    4
    __block typeof(person) weakPerson = person;
    person.block = ^{
    NSLog(@"age is %d", weakPerson.age);
    };

    补充

    在解决循环引用时,有一种常见的写法是在block内部使用__strong,我们看这段代码
1
2
3
4
5
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
NSLog(@"age is %d", strongSelf->_age);
};

解决循环引用时,使用weak类型的对象,但是当执行block内部的逻辑代码时,如果self被释放,此时block访问释放的self就会挂,在block里面使用的strong修饰的weakSelf是为了在函数生命周期中防止self提前释放。strongSelf是一个自动变量当block执行完毕就会释放自动变量,strongSelf不会对self进行一直进行强引用。

总结

block本质上也是一个OC对象,它内部也有个isa指针

block是封装了函数调用以及函数调用环境的OC对象

block的底层结构如图所示
block底层结构

参考和源码

Demo源码:
__block底层-Demo
__block转结构体-Demo

评论