学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

关于Block

几个关于block的问题

  • block的内部实现,结构体是什么样的
  • block是类吗,有哪些类型
  • 一个int变量被 __block 修饰与否的区别?block的变量截获
  • block在修改NSMutableArray,需不需要添加__block
  • 怎么进行内存管理的
  • block可以用strong修饰吗
  • 解决循环引用时为什么要用__strong、__weak修饰
  • block发生copy时机
  • Block访问对象类型的auto变量时,在ARC和MRC下有什么区别

block、函数指针、闭包

  • 和C语言的函数指针的定义对比
1
2
3
4
5
// Block
returnType (^blockName)(parameterTypes)

// 函数指针
returnType (*c_func)(parameterTypes)

例如输入和返回参数都是字符串:

1
2
(char *) (*c_func)(const char *);
(NSString *) (^block)(NSString *);
  • 闭包

闭包是一个函数(或指向函数的指针),再加上该函数执行的外部的上下文变量(有时候也称作自由变量)。

block实际上就是Objcective-C对于闭包的实现

block的内部实现,数据结构是什么样的?

1
2
3
4
5
int age = 20;
void (^block)(void) = ^{
NSLog(@"age is %d",age);
};
block();

通过clang编译成cpp文件
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

  • block结构
1
2
3
4
5
6
int age = 20;
// block的定义
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
// block的调用
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

删除掉一些强制转换的代码

1
2
3
4
5
6
7
8
9
int age = 20;
void (*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
age
);
// block的调用
block->FuncPtr(block);

block本质就是一个结构体对象,结构体__main_block_imp_0结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
//构造函数(类似于OC中的init方法) _age是外面传入的
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
//isa指向_NSConcreteStackBlock 说明这个block就是_NSConcreteStackBlock类型的
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

结构体中第一个变量是struct __block_impl impl

1
2
3
4
5
6
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

结构体中第二个变量是__main_block_desc_0;

1
2
3
4
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size; // 结构体__main_block_impl_0 占用的内存大小
}

block数据类型

block中有isa指针,block是一个OC对象

1
2
3
4
5
6
7
8
void (^block)(void) =  ^{
NSLog(@"123");
};

NSLog(@"block.class = %@",[block class]);
NSLog(@"block.class.superclass = %@",[[block class] superclass]);
NSLog(@"block.class.superclass.superclass = %@",[[[block class] superclass] superclass]);
NSLog(@"block.class.superclass.superclass.superclass = %@",[[[[block class] superclass] superclass] superclass]);

输出结果

1
2
3
4
iOS-block[18429:234959] block.class = __NSGlobalBlock__
iOS-block[18429:234959] block.class.superclass = __NSGlobalBlock
iOS-block[18429:234959] block.class.superclass.superclass = NSBlock
iOS-block[18429:234959] block.class.superclass.superclass.superclass = NSObject

继承关系可以表示为__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject

小结

  1. isa:由此可知,block也是一个对象类型,具体类型包括_NSConcreteGlobalBlock_NSConcreteStackBlock_NSConcreteMallocBlock
  2. block在Clang编译器前端得到实现,可以生成C中间代码。
  3. 实际上block就是指向结构体的指针
  4. block本质上也是一个OC对象,它内部也有isa指针,block是封装了函数调用以及函数调用环境的OC对象

一个int变量被 __block 修饰与否的区别?block的变量截获?

没有被__block修饰的int,block体中对这个变量的引用是值拷贝
通过__block修饰的int,block体中对这个变量的引用是指针拷贝

变量捕获

auto修饰的局部变量,离开作用域就销毁了,指针传递的话,可能导致访问的时候,该变量已经销毁了,程序就会出问题,而全局变量本来就是在哪里都可以访问的,所以无需捕获。

block怎么进行内存管理的?

block按照内存分布,分三种类型:全局内存中的block、栈内存中的block、堆内存中的block。

block的3种类型

可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型

在MRC和ARC下block的分布情况

  • MRC下:

当block内部引用全局变量或者不引用任何外部变量时,该block是在全局内存中的。

当block内部引用了外部的非全局变量的时候,该block是在栈内存中的。

当栈中的block进行copy操作时,会将block拷贝到堆内存中。

通过__block修饰的变量,不会对其应用计数+1,不会造成循环引用。

  • ARC下:

当block内部引用全局变量或者不引用任何外部变量时,该block是在全局内存中的。

当block内部引用了外部的非全局变量的时候,该block是在堆内存中的。

也就是说,ARC下只存在全局block和堆block

1
2
3
通过__block修饰的变量,在block内部依然会对其引用计数+1,可能会造成循环引用。

通过__weak修饰的变量,在block内部不会对其引用计数+1,不会造成循环引用。

block可以用strong修饰吗?

MRC环境中,是不可以的,strong修饰符会对修饰的变量进行retain操作,这样并不会将栈中的block拷贝到堆内存中,而执行的block是在堆内存中,所以用strong修饰的block会导致在执行的时候因为错误的内存地址,导致闪退。

ARC环境中,是可以的,因为在ARC环境中的block只能在堆内存或全局内存中,因此不涉及到从栈拷贝到堆中的操作。

解决循环引用时为什么要用__strong、__weak修饰?

__weak修饰的变量,不会出现引用计数+1,也就不会造成block强持有外部变量,这样也就不会出现循环引用的问题了。

但是,我们的block内部执行的代码中,有可能是一个异步操作,或者延迟操作,此时引用的外部变量可能会变成nil,导致意想不到的问题,而我们在block内部通过__strong修饰这个变量时,block会在执行过程中强持有这个变量,此时这个变量也就不会出现nil的情况,当block执行完成后,这个变量也就会随之释放了(自动变量,作用域只在block里)。

1
2
block内部的strongSelf仅仅是个局部变量,存在栈中,会在block执行结束之后回收,不会再造成循环引用,并且会使页面返回上一级
时,不执行dealloc方法,直到block执行完,控制器执行dealloc方法,内存释放!

block发生copy时机?

一般情况在ARC环境中,编译器将创建在栈中的block会自动拷贝到堆内存中,而block作为方法或函数的参数传递时,编译器不会做copy操作。如下情况,编译器会自动copy

  1. block作为方法或函数的返回值时,编译器会自动完成copy操作。
  2. 当block赋值给通过strong或copy修饰的id或block类型的成员变量时。
  3. 当 block 作为参数被传入方法名带有 usingBlockCocoa Framework 方法或 GCD 的 API 时。

Block访问对象类型的auto变量时,在ARC和MRC下有什么区别?

首先我们知道,在ARC下,栈区创建的block会自动copy到堆区;而MRC下,就不会自动拷贝了,需要我们手动调用copy函数。

我们再说说block的copy操作,当block从栈区copy到堆区的过程中,也会对block内部访问的外部变量进行处理,它会调用Block_object_assign函数对变量进行处理,根据外部变量是strong还会weak对block内部捕获的变量进行引用计数+1或-1,从而达到强引用或弱引用的作用。

因此

在ARC下,由于block被自动copy到了堆区,从而对外部的对象进行强引用,如果这个对象同样强引用这个block,就会形成循环引用。

在MRC下,由于访问的外部变量是auto修饰的,所以这个block属于栈区的,如果不对block手动进行copy操作,在运行完block的定义代码段后,block就会被释放,而由于没有进行copy操作,所以这个变量也不会经过Block_object_assign处理,也就不会对变量强引用。

简单说就是:

ARC下会对这个对象强引用,MRC下不会

参考

  1. 深入理解iOS的block
  2. 一道Block面试题的深入挖掘