Block 原理
Block 本质上也是 OC 的对象,那么也就是一个结构体,它内部也有一个
isa
指针
Block 是封装了函数调用(主要是函数的地址 FunPtr)以及函数调用环境的 OC 对象。
最简单的 block :^{}
这种是指封装了内容,但是没有执行,相当于做了方法的实现,但是没有调用方法。 而方法的调用是这样doSomeThing();
,所以执行 block 是这样的^{}()
;
Block 底层结构
Block 变量捕获
为了保证 block 内部能够正常访问外部的变量, block 有个变量捕获机制(capture)
局部变量
auto
: 离开作用于自动销毁,所以称为自动变量,又叫做局部变量(只能是局部,不能是全局)。在函数被调用时,系统为其分配空间,存储在动态存储区,函数执行后释放存储空间
static
:在编译时,系统为其分配存储空间,存放在静态存储区(全局数据区)。
1. static 只能在当前文件中使用,哪怕被修饰了 extern ,外面也是无法访问。外面的其他文件可以拥有同样用 static 修饰的相同名字的变量,互不影响。
2. static 只被初始化一次,程序运行期间不消失,所以下次值是根据上次的值改变的。
3. static 修饰的全局变量和常规的静态全局变量,存储的位置都是全局数据区的静态存储区,但是 static 修饰的全局变量规定了作用于,只能在当前文件使用。常规的静态全局变量,施加了 static 之后会变成静态全局变量。
4. static 修饰的局部变量,相较于没有用 static 修饰的局部变量,只是改变了存储区域,也就是改变了它的生命周期。
5. static 定义的函数也是如此,只能在当前文件中使用。
无论是 auto
还是 static
类型的局部变量,都会被捕获,只不过前者是捕获值,后者是捕获指针。主要原因是因为局部变量的作用域,因为有可能存在跨函数访问,而局部变量只有在当前函数的作用域内有效,所以需要捕获,然后存起来。
全局变量
全局变量(无论是否是 static)并没有 capture
到 block 内部,也就是说没有在 block 结构体内部生成对应的新的变量或者指针变量。
主要是因为全局变量谁都可以访问,所以没必要捕获,直接访问即可。
Block 类型
Block 由于也是
OC对象
,它底层内部也有一个isa
指针(继承于 NSObject 的),所以它也是有类型的,而它的ISA指针就指向了它的类型。
Block 一共有3
种类型:
__NSGlobalBlock__(__NSConcreteGlobalBlock)
__NSStackBlock__(__NSConcreteStackBlock)
__NSMallocBlock__(__NSConcreteMallocBlock)
- 无论哪种类型,它们都继承于
NSBlock
这个基础类型,而NSBlock
的父类就是NSObject
,所以说它本质上是一个OC对象。可以通过调用 class 方法或者查看 isa 指针指向来看其具体类型。- 继承关系:
__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
,其他几个也类似
应用程序内存分布图
- .text 区,也称为代码区,是内存的低地址地区
- .data 区,一般存放全局变量
- 堆区,动态分配内存,程序员通过 alloc/malloc 这种方式申请的内存,一般在这里。动态分配的特点,需要程序员去申请和销毁的。
- 栈区,放的局部变量。 系统会自动分配对应的内存,并且会自动回收。比如在离开大括号这种作用域之后,会自动回收内存。
Block 类型的区别
- 不同的类型的 Block 调用了 copy 操作之后:
Global 类型的 block
没有访问 auto 类型的变量的 block ,也就是没有访问全局类型的 block 。这种 Block 存放在数据区,也就是全局区。
//1. 没有访问局部变量的 block 为全局类型的 block :__NSGlobalBlock__
void(^block1)(void)=^{
NSLog(@"Block1 ------------- ");
};
Stack 类型的 block
访问了 auto 类型变量的 block 。 这种 block 会存放在 Stack 栈区,代码执行完会自动销毁(MRC下),ARC下会自动进行
copy
操作,拷贝到堆区
。
//2. 访问了 auto 变量的,为 stack 的类型的 block:__NSStackBlock__ (非ARC下,如果是ARC下,则为 malloc ,这是因为 ARC 帮程序员做了一些 copy 的操作)
// stack 类型的 block 会放在栈段,在超出作用域外,其实 block 被销毁了,所以调用可能会出现垃圾数据或异常。
int age = 10;
void (^block2)(void) = ^{
NSLog(@"Block2 ----------- age is %d",age);
};
Malloc 类型的 block
对 stack 类型的 block 进行
copy
操作,就得到了 Malloc 类型的 block,存放在堆区。
void(^block3)(void) = [block2 copy];
block 中对象类型的 auto 变量强弱引用
先看示例代码
bchBlock baiBlock;
{
Person * p = [[Person alloc]init];
p.age = 10;
bchBlock baiBlock =^{
NSLog(@"p.age is %d",p.age);
};
}
NSLog(@"------");
- 在最下面的 NSLog 打印时,person 对象没有被释放。
- 原因是:p 虽然是person的局部实例对象,但是 block 会捕获这个局部变量,而且捕获的是指针,而且这个 block 是在
ARC
下的堆空间内,因为大括号外面有个强制真引用,所以是从栈拷贝到了堆,从而没有释放,而它内部又通过捕获指针的方式强持有这个对象,所以 person 对象没有被释放。 - 栈空间上的 block 没有能力保住外面的对象,但是堆空间上的 block 有能力保住外面的对象。
MJPerson *p = [[MJPerson alloc] init];
__weak MJPerson *weakP = p;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"1-------%@", p);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"2-------%@", weakP);
});
});
- 看 P 的强指针引用在哪个 block 结束,就在什么时候销毁。
- 如果第一层是weak,但是最后一层是强引用,那么也不会在第一层销毁。
Block 修改变量
修改 block 外部的局部变量的时候,由于捕获机制,(block 内部产生一个一毛一样的变量,然后捕获的是值),所以不能直接修改。
如果要修改,要么改成 static 类型,这样捕获指针,就可以修改,要么改成全局变量(但是这样一直存在在内存中也不好)。
或者通过__block
的形式
__block 原理及注意点
__block 不能修饰全局变量、static 变量
,仅能修饰auto
类型的变量- 编译器会将
__block
修饰的变量(基础变量或者是对象变量
)包装成一个对象(结构体),实际上用到的这个变量是在这个结构体内部(内部有一个名字一毛一样的名字的变量),我们要改,也是改这个结构体内部的变量。 - 外面修改的是结构体内部的那变量,打印的话,也是这个内部变量的地址。
- 这里有个
__forwarding
指针指向这个结构体本身
__block 内存管理
- copy 到堆上的时候 调用 copy 函数(多个 block 使用一个变量,则该变量只会被 copy 一次)
- 移除强引用
Block 循环引用的问题
造成循环引用的原因
Block 造成循环引用的话,主要是因为: Block 是封装了环境和函数,其中环境就是捕获的变量(这里主要是指针),这个指针强指向了某个对象A。 而这个对象A里有对block的操作,所以A也强持有了Block,导致了它们之间互相强引用,无法释放。
如果 Block 使用了 self
或者成员变量
,也有可能造成循环引用,因为 self 本质上是局部变量,block 捕获的是这个变量的指针,同理,成员变量,本身也是通过 self 调用的,所以也会造成循环引用。
解决方案
- 用
__weak
、__unsafe_unretain
来解决
让 block 内部强引用外面对象的那个变为弱引用就可以了。 外面的不能变为弱引用,因为外面要持有 block 对象才能使用 block 。
__weak
: 不会产生强引用,对象销毁后,原来的指针会自动变为nil
,所以不会产生野指针
.
__unsafe_unretain
:也不会产生强引用。但是对象销毁后,原来的指针不会变为 nil ,还是原来的地址值 ,所以会产生野指针。 - 用
__block
(block 必须执行,block 内部必须要对对象进行置为nil的操作来打破循环)
用__block
修饰的对象变量会被包装成一个结构体,而 block 捕获的就是这个结构体,这个结构体内部有个 指针指向这个变量。外面看到的这个变量就是指这个结构体内部的指针变量。
缺点:就是必须要执行 block ,如果没机会执行 block 还是会内存泄露。
MRC 下不支持弱指针的,所以只能通过 __unsafe_unretain 来操作,但这个终究不安全
小结
变量捕获
- block 外的局部变量(无论是 auto 还是 static),会被自动捕获进 block 结构体内部,(准确的说,应该是 auto 类型的局部变量的值被捕获进去,static 的则是捕获其指针;对象类型的,则是捕获对应的指针,是强指针就捕获强指针,是弱指针就捕获弱指针),block 内部会产生一个一毛一样的变量,将这个值存储起来,block 函数体内用的就是这个自己产生的变量,和外面的没关系。
- auto 变量由于会自动销毁,所以需要将变量值存起来,不然存地址的话,以后调用,由于auto变量已经消失了,就会导致访问内存的错误。
self
、_cmd
是 OC 的方法底层实际每个都会有的参数:self 是当前对象的指针,_cmd 是当前方法的一个 SEL 指针,所以这2个是局部变量,只要是局部变量都会被 block 捕获,- 同上,访问实例变量的时候,比如 _name ,实际上是相当于访问
self->_name
,捕获的,通过属性访问也是如此。
copy 操作
- ARC 下声明的属性,属性的类型是 block ,用的是 copy 关键字修饰,那么编译器会自动帮我们将 block copy 到堆。
- (MRC 下)copy 到堆上的 block ,要进行 release 操作,防止内存泄露(ARC不需要这样操作)。
- 在
ARC
的环境下,编译器会根据情况将 stack 区的 block 拷贝至 栈区 :
- block 作为函数返回值时。
- 将 block 赋值给
__strong
指针时- block 作为 Cocoa API 中方法名含有
usingBlock
的方法参数时- block 作为 GCD API 里的参数的时候
对象类型的 auto 变量
- 如果 block 内部访问的对象类型的 auto 变量时:
- 如果 block 是在栈上,无论 ARC 还是 MRC,都不会对 auto 变量产生强引用。
- 如果 block 被拷贝到堆上,会调用 block 内部的 copy 函数,copy 函数内部又会调用
_Block_object_assign
函数,而这个函数会根据 auto 变量的修饰符(__strong/__weak/__unsage_unretained)做出对应的操作(类似于 retain,只是计数器 +1),形成强引用或弱引用。 - 如果 block 从堆上移除,则会调用内部的 dispose 函数,而这个函数会调用
_Block_object_dispose
函数,而这个函数会自动release(只是计数器-1)之前引用的 auto 变量。变量是否释放,还是根据这个变量的引用计数是否为 0。 - 只有 block 内部访问的是
对象类型
,block 底层的结构的 DESC 才会有上面2个函数来对这个对象进行一个内存管理的操作。基础数据类型则不用。
__block 内存管理
- 当 block 在栈上的时候,对局部变量(基础类型或者对象类型)都不会产生强引用。
- 当 block 拷贝到堆上的时候,都会调用 block 内部的
assign
函数,来进行引用(基础数据类型都是强引用。对象类型的变量,则根据外部传来的是weak还是strong进行对应的引用,这是ARC下。如果是 MRC 下,无论怎么写,对象类型,最终结构体内部都是弱引用。 - 当 block 从堆上移除的时候,都会调用 block 内部的
dispose
函数,来进行释放,类似于 release 。
__forwarding 指针的作用
当 block 在栈上的时候,auto 变量也是在栈上,所以 __forwarding 指针指向自己没问题。
但是,当 block 被从栈拷贝到堆上的时候,这个 block 引用的auto变量也被拷贝到了堆上。
其实就是 __block 修饰的变量,也就是包装的对象(结构体A)中,有一个实际保存变量的结构体(结构体B),当结构体B在栈上的时候,那么 __forwarding 指针指向的就是它本身,外部可以正常访问里面的变量。
但是当从栈上拷贝到堆上的时候,那么原来 A 结构体中的B中的 __forwarding 指针指向就改成了堆中那份结构体B的地址,而B中 __forwarding 指针指向的是B在堆中的自己。
这个改变栈上 __forwarding 指针指向的操作是在 copy 到堆上的时候做的,也就是通过对这个变量封装的结构体内部的 __Block_object_assign 实现的
Block 解决循环引用
- ARC 下使用
__weak
、__unsafe_unretain
、__block(但是必须执行 block ,而且 block 内部要将对象置为 nil。不推荐)
来打破循环引用,但是推荐__weak
- MRC 下推荐使用
__block
,因为 MRC 下,用__block
修饰的、封装的结构体对象内部的变量指针都是弱引用。 __weak
是为了打破循环引用。而在 block 内部,再次用__strong
修饰weak对象,是防止外面将 weak 对象提前释放掉。用__strong
就保证了 block 内可以正常持有该对象,然后执行完毕,否则 block 内访问这个被置为 nil 的对象可能会出错。
面试题
block 的原理是怎样的? 本质是什么?
答:
Block 是封装了函数调用及函数调用环境的 OC 对象,也就是结构体。
_block的作用是什么? 有什么使用注意点?
答:
- 将所修饰的局部变量对象封装成一个结构体,但是不能修饰全局变量和 static 变量。
- 解决 block 内部不能修改变量的问题。
- 主要是注意内存管理,有哪几种不同类型的 block ,它们在内存的什么位置,有什么特性。
- 在 MRC 下,__block 包装的对象,不会对内部的变量有强引用。ARC 下根据情况。
block 的属性修饰词为什么是 copy ? 使用 block 有哪些使用注意点?
答:
block 一旦没有进行 copy 操作,就不会在堆上。
需要注意循环引用的问题。
block 在修改 NSMutableArray ,需不需要加上 _block ?
答:
不需要
增加或删除,仅仅是调用了这个数组的指针地址,向其发送消息。 本身并没有修改这个指针地址。