__block的使用
typedef void (^MJBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
MJBlock block1 = ^{
age = 20;
};
}
return 0;
}
Variable is not assignable (missing __block type specifier)
在block内部是无法修改局部变量的.
怎么样才能在
block
内部修改外部变量呢?有三种方法:
1:使用
static
修饰
age
2:把
age
变成全局变量
3:使用
__block
修饰
age
如果想在
block
内部修改从外部捕获的
auto
变量的值,加上关键字
__block
。代码如下
typedef void (^MJBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
MJBlock block1 = ^{
age = 20;
NSLog(@"age is %d", age);
};
block1();
NSLog(@"%@", [block1 class]);
NSLog(@"MJBlock执行完之后,age = %d",age);
}
return 0;
}
RUN>
*********************** 运行结果 **************************
2021-04-25 14:45:23.221612+0800 Interview04-__block[2888:128162] age is 20
2021-04-25 14:45:23.222238+0800 Interview04-__block[2888:128162] __NSMallocBlock__
2021-04-25 14:45:23.222320+0800 Interview04-__block[2888:128162] MJBlock执行完之后,age = 20
__block
只可以用来作用于
auto
变量,它的目的就是为了能够让
auto
变量能够在
block
内部内修改。全局变量和
static
变量本来就可以从
block
内部进行修改,因此
__block
对它们来说没有意义,所以
__block
被规定只能用于修饰
auto
变量,这一点应该不难理解。
__block的本质
我们把使用
__block
修饰的
age
转换为
C++
代码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main-arm64.cpp
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;
}
};
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};
为了对比,加入一个非
__block
修饰的变量来做对比参考
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
int noblock = 30;//普通auto对象
MJBlock block1 = ^{
age = 20;
NSLog(@"age is %d", age);
NSLog(@"age is %d", noblock);
};
block1();
NSLog(@"%@", [block1 class]);
NSLog(@"MJBlock执行完之后,age = %d",age);
}
return 0;
}
转化
C++
后的代码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int noblock;
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _noblock, __Block_byref_age_0 *_age, int flags=0) : noblock(_noblock), age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
基本类型的
auto
变量被
block
捕获的时候,就是通过值拷贝的形式把值赋给
block
内部相对应的基本类型变量。而案例里面的
__block int age = 10
,我们可以看到在底层,系统是把
int age
包装到了一个叫
__Block_byref_age_0
的对象里面。这个对象的结构如下
struct __Block_byref_age_0 {
void *__isa;;//有isa指针 说明是一个ocobject对象
__Block_byref_age_0 *__forwarding;//指向自身类型对象的指针
int __flags;
int __size;//自己所占大小
int age;//被封装的 基本数据类型变量
};
main函数中
__Block_byref_age_0
被赋了什么值
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
int noblock = 30;
10被存储到了
block
内部
__Block_byref_age_0
对象的
成员变量int age
上。
__Block_byref_age_0
对象里面的成员变量
__forwarding
实际上指向了
__Block_byref_age_0
对象自身。
在看看blcok内部函数操作
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
int noblock = __cself->noblock; // bound by copy
(age->__forwarding->age) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_yh_qjzhl57s63j2m9l4frv27zjc0000gn_T_main_86e9d7_mi_0, (age->__forwarding->age));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_yh_qjzhl57s63j2m9l4frv27zjc0000gn_T_main_86e9d7_mi_1, noblock);
}
block
内部修改
auto
变量,是先通过参数传递进来的
block
找到
age
结构体,然后通过
age
结构体找到
__forwarding
成员,通过之前的分析已经知道
__forwarding
存储的指针指向
age
结构体自己,所以本质上还是通过
age
结构体找到存储
auto
变量值得
age
成员,然后修改成 20.
为什么用
age->__forwarding->age
,而不是
age->age
直接拿到
int age
,通过
__forwarding
转一圈有什么用意?
为什么苹果要设计
forwarding
这种多此一举的方式呢?
因为当block从栈上拷贝到堆上后,
__block变量
也会拷贝到堆上.这时就有两份
__block变量
,一份栈上的,一份堆上的.如果
__block
修饰的变量是存放在栈上,这是
forwarding
指向的是它自己,这样没有问题.但是如果
__block
修饰的变量复制到堆上,它就会把栈上的
forwarding
指向堆上的变量,这样就能保证即使访问栈上的
__block变量
也能获取到堆上的变量值,如图:
上面,我们知道了通过
__block int age = 10
定义之后,这个
a
底层是一个
__Block_byref_age_0
对象,数值10存放在这个对象内部的成员变量
int age
上面。但是我们在写代码的时候,可以直接通过
__Block_byref_age_0
对象age来赋值,那么在
block
定义初始化结束,完成变量捕获之后,oc代码中再次通过
age
访问到的到底是什么呢?如下图所示
#import <Foundation/Foundation.h>
typedef void (^MJBlock) (void);
struct __Block_byref_age_0 {
void *__isa;
struct __Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(void);
void (*dispose)(void);
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
struct __Block_byref_age_0 *age;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
MJBlock block = ^{
age = 20;
NSLog(@"age is %d", age);
};
struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block;
NSLog(@"%@", [block class]);
NSLog(@"MJBlock执行完之后,age = %d",age);
NSLog(@"完成变量捕获之后,其内部的[__Block_byref_age_0 *] a = %p",blockImpl->age);
NSLog(@"oc代码里直接通过a访问的内存空间是:%p",&age);
}
return 0;
}
RUN>
*********************** 运行结果 **************************
2021-04-25 15:32:06.386047+0800 Interview01-__block[3197:151003] __NSMallocBlock__
2021-04-25 15:32:06.386544+0800 Interview01-__block[3197:151003] MJBlock执行完之后,age = 10
2021-04-25 15:32:06.386602+0800 Interview01-__block[3197:151003] 完成变量捕获之后,其内部的[__Block_byref_age_0 *] a = 0x1004b9230
2021-04-25 15:32:06.386641+0800 Interview01-__block[3197:151003] oc代码里直接通过a访问的内存空间是:0x1004b9248
block
内部的
[__Block_byref_age_0 *] age
指向的地址是
0x1004b9230
,之后我们在任意地方通过
age
访问的内存地址是
0x1004b9248
,十六进制下它们地址相差了
0x18
,也就是十进制下的24个字节。
struct __Block_byref_age_0 {
void *__isa; //8
struct __Block_byref_age_0 *__forwarding; //8
int __flags;//4
int __size;//4
int age;
};
8+8+4+4 = 24;
也就是说我们在oc代码里面完成了
block
的初始化以及
__block变量
的捕获之后,只能通过
age
访问到被封装在
__ Block_byref_age_0 *
内部的这个
int age
的内存空间。
我们思考一下苹果为什么这样设计?
因为苹果要隐藏它内部的实现,我们在修改
__block
修饰的
age
的值时,从表面看会以为真的是在直接修改
age
的值,如果不了解底层实现的话,根本就不知道被
__block
修饰的
age
已经被包装成了一个对象,而我们实际修改的是
age
结构体中的
age
成员的值.
__block变量的内存管理
block捕获
对象类型的auto变量
就会多出两个函数用于做内存管理操作(
__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->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}
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};
block也会使用这两个函数管理
__block
修饰的变量的内存,这也从侧面证明"编译器会将__block变量包装成一个对象",这句话是对的,因为只有对象才需要内存管理。
现在我们来研究一下,使用
__block
修饰的外部变量,在block内部是如何管理的.
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
int noblock = 30;
MJBlock myblock = ^{
age = 20;
NSLog(@"age is %d", age);
NSLog(@"noblock is %d", noblock);
};
myblock();
NSLog(@"%@", [myblock class]);
}
return 0;
}
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
结构体中同样有
copy,dispose
函数
为什么访问
__block int
时也会产生呢?
在上面我们说过,
block
在访问
__block
修饰的变量时,其底层会被封装成
__Block_byref_age_0
类型,在这个类型存在一个
void *__isa
成员,所以本质上它就是一个对象类型.
__block修饰的变量的内存管理:
-
当block在栈上时,并不会对__block变量产生强引用
-
当block被copy到堆时(自己拷贝的或者ARC下系统自动拷贝的)
会调用block内部的copy函数
copy函数内部会调用_Block_object_assign函数
_Block_object_assign函数会对__block变量形成强引用(retain)
关于内存管理问题这里,我们所讨论的问题需要考虑三个关键因素:
__block
、
__weak
、
对象变量
、
基本类型变量
,他们合法的组合有如下几种:
-
基本类型变量
-
对象变量
-
__weak
+
对象变量
-
__block
+
基本类型变量
-
__block
+
对象变量
-
__block
+
__weak
+
对象变量
在
OC-Block的本质(三)-对象类型的auto变量
这一篇里面详细分析了一个对象类型的
auto
变量被
block
捕获时的内存管理过程,上面的1、2、3这三种场景已经得到了说明。下面我们来分析一下4、5、6这三种场景。
__block
+
基本类型变量
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
int noblock = 30;
MJBlock myblock = ^{
age = 20;
NSLog(@"age is %d", age);
NSLog(@"noblock is %d", noblock);
};
myblock();
NSLog(@"%@", [myblock class]);
}
return 0;
}
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main-arm64.cpp
转化后的c++源码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int noblock;
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _noblock, __Block_byref_age_0 *_age, int flags=0) : noblock(_noblock), age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
int noblock = __cself->noblock; // bound by copy
(age->__forwarding->age) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_yh_qjzhl57s63j2m9l4frv27zjc0000gn_T_main_f68719_mi_0, (age->__forwarding->age));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_yh_qjzhl57s63j2m9l4frv27zjc0000gn_T_main_f68719_mi_1, noblock);
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}
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};
我们知道
__block int age = 10;
这句代码的作用,是将
int age
包装在
struct __Block_byref_age_0
内部,这样
block
实际上捕获的是这个
struct __Block_byref_age_0
,它可以被当作一个对象来看待,所以内存管理上面,最终仍然是通过
_Block_object_assign
和
_Block_object_dispose
这两个函数来处理,但是可以看到这两个函数的最后一个参数是
8
(对于对象类型的捕获,传递的参数是
3
),这个参数表明了即将要处理的是一个
struct __Block_byref_age_0
,因为它是没有
__weak
和
__strong
标记的,所以处理方式很简单,就是copy到堆上的时候,同时需要进行
retain
,dispose的时候同时需要进行
release
。
而当block从堆中移除时:
1:调用block内部的dispose函数
2:dispose函数会调用
_Block_object_dispose
函数
3:
_Block_object_dipose
函数会自动释放引用的
__block
变量(release)
既然 block 访问对象类型的变量和访问使用 __block 修饰的变量都会增加
copy,dispose
函数,那么他们之间有没有区别呢?
他们之间的区别就是:
-
如果使用__block修饰的变量,block内部直接对其强引用
-
如果是对象类型的变量,会根据变量的修饰符
__weak , __strong
来决定是否强引用
__block
+
对象变量
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject
@end
@implementation MJPerson
- (void)dealloc
{
NSLog(@"%s", __func__);
}
@end
typedef void (^MJBlock) (void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
__block MJPerson *blockPerson = person;
MJBlock block = ^{
NSLog(@"%p", blockPerson);
} ;
NSLog(@"%@", [block class]);
block();
}
return 0;
}
RUN>
*********************** 运行结果 **************************
2021-04-25 17:49:33.467580+0800 Interview01-__block[4123:185220] __NSMallocBlock__
2021-04-25 17:49:33.468013+0800 Interview01-__block[4123:185220] 0x10061e4d0
2021-04-25 17:49:33.468078+0800 Interview01-__block[4123:185220] -[MJPerson dealloc]
编译
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main-arm64.cpp
编译之后,
__block MJPerson *blockPerson
的底层结构如下
struct __Block_byref_blockPerson_0 {
void *__isa;
__Block_byref_blockPerson_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
MJPerson *__strong blockPerson;
};
可以看到使用
__block
修饰的对象底层被封装成了
__Block_byref_blockPerson_0
类型的对象,
__Block_byref_blockPerson_0
这个对象类型的结构体中有一个
MJPerson *__strong blockPerson
成员,强引用着我们在
main
函数中创建的
MJPerson
对象,而
__main_block_impl_0
中的
blockPerson
又强引用着
__Block_byref_blockPerson_0
对象,他们的关系如下图:
从这个结构可以看出两点变化:
-
相比较基本类型变量,对象类型的变量被
__block
修饰后,底层所生成的
__Block_byref_xxx_x
结构体里面多了两个函数指针,
__Block_byref_id_object_copy
和
__Block_byref_id_object_dispose
。
-
对象类型的变量被封装到
__Block_byref_xxx_x
内部以后,默认是被
__strong
修饰的。
上面发现的两个新函数指针
__Block_byref_id_object_copy
和
__Block_byref_id_object_dispose
就是当
__Block_byref_xxx_x
被拷贝到堆空间的时候,以及将要被系统释放的时候调用的。
它们的定义如下:
#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
最终还是调用了
_Block_object_assign
和
_Block_object_assign
这两个函数,二从参数可以看出,它们所要处理的对象就是
__Block_byref_id_object_dispose
内部所封装的对象类型变量,也就是我们代码中的
MJPerson *blockPerson
,因为默认
blockPerson
是被
__strong
修饰的,所以接下来对于
blockPerson
的内存管理方式就和我们之前所分析过的是一样的。
__block
+
__weak
+
对象变量
#import <Foundation/Foundation.h>
#import "MJPerson.h"
typedef void (^MJBlock) (void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
__block __weak MJPerson *blockPerson = person;
MJBlock block = ^{
NSLog(@"%p", blockPerson);
} ;
NSLog(@"%@", [block class]);
block();
}
return 0;
}
RUN>
2021-04-25 20:05:06.334104+0800 Interview01-__block[4591:204724] __NSMallocBlock__
2021-04-25 20:05:06.334480+0800 Interview01-__block[4591:204724] 0x10051edc0
2021-04-25 20:05:06.334534+0800 Interview01-__block[4591:204724] -[MJPerson dealloc]
查看底层代码:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main-arm64.cpp
typedef void (*MJBlock) (void);
struct __Block_byref_blockPerson_0 {// __block 底层对象
void *__isa;//8
__Block_byref_blockPerson_0 *__forwarding;//8
int __flags;//4
int __size;//4
void (*__Block_byref_id_object_copy)(void*, void*);//8
void (*__Block_byref_id_object_dispose)(void*);//8
MJPerson *__weak blockPerson;//这里变成了弱引用,说明这里的引用情况取决于外部变量的修饰符
};
// block 底层对象
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
//这里还是强引用,说明 __weak 关键字并不会影响这里
__Block_byref_blockPerson_0 *blockPerson; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_blockPerson_0 *_blockPerson, int flags=0) : blockPerson(_blockPerson->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
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};
查看底层代码我们发现,
block
内部对于
__Block_byref_blockPerson_0
的引用没有变化,强指针指向__Block_byref_weakPerson_0结构体,但是
__Block_byref_blockPerson_0
中的
blockPerson
已经从强引用变成了弱引用.如图:
总结:
对于block:
-
如果block是在栈上,将不会对__Block_byref_weakPerson_0产生强引用
-
如果栈上的block被拷贝到堆上
_Block_object_assign函数会对__Block_byref_weakPerson_0产生强引用
-
如果堆上的block被移除
_Block_object_dispose函数会对__Block_byref_weakPerson_0产生弱引用或者移除
对于__block修饰的对象类型:
-
当
__block
变量在栈上时,不会对指向的对象产生强引用
-
当
__block
变量被copy到堆时(自己拷贝的或者ARC下系统自动拷贝的)
会调用
__block
变量内部的copy函数
copy函数内部会调用
_Block_object_assign
函数
_Block_object_assign
函数会根据所指向对象的修饰符(
__strong
、
__weak
、
__unsafe_unretained
)做出相应的操作,形成强引用(retain)或者弱引用(
注意:这里仅限于ARC时会retain,MRC时不会retain,一直是弱的。这个特例只会在MRC并且是
__block
修饰对象类型才有
)
-
如果
__block
变量从堆上移除
会调用
__block
变量内部的dispose函数
dispose函数内部会调用_Block_object_dispose函数
_Block_object_dispose函数会自动释放指向的对象(release)
将项目切换成
MRC环境
使用__block修饰对象类型,如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block MJPerson *person = [[MJPerson alloc] init];
MJBlock block = [^{
NSLog(@"%p", person);
} copy];
[person release];
block(); //在此处打断点
[block release];
}
return 0;
}
RUN>
*********************** 运行结果 **************************
2021-04-25 20:25:01.684799+0800 Interview01-__block[4780:216050] -[MJPerson dealloc]
2021-04-25 20:25:01.685196+0800 Interview01-__block[4780:216050] 0x1004708c0
当block被拷贝到堆上的时候,__block修饰的person也会被拷贝到堆上,这时候会调用__block修饰的变量内部的copy函数,MRC环境下,copy函数内部只会对person对象产生弱引用。如下图:
如果把
__block
去掉,会怎么样?
如果把
__block
去掉,blcok 就会对 person 产生强引用,在 block 释放之前,person 是不会释放的,因为去掉
__block
后,就没有
__Block_byref_blockPerson_0
这个中间层,blcok 会直接强引用
person
对象
特别备注
本系列文章总结自MJ老师在腾讯课堂
iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化
,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!