block捕获变量
一般,block代码块内 使用的外部变量有3种类型。
一般的局部变量默认是auto
修饰(自动变量)离开作用域就销毁。
1.局部变量auto(自动变量)
int age = 20; // 或 auto int age = 20; auto可省略
void (^block)(void) = ^{
NSLog(@"age is %d",age);
};
age = 25;
block(); // 调用
运行后,打印的是 20。可见,直接把age的值 20传到了block中,后面再修改age的值,并不能改变block里面的age值。
- 结论:auto修饰的局部变量,是值传递。
其实也很好理解,因为auto修饰的局部变量,离开作用域就销毁了。
2.局部变量 static
static修饰的局部变量,不会被销毁。
static int age = 20;
void (^block)(void) = ^{
NSLog(@"age is %d",age);
};
age = 25;
block(); // 调用
运行后,打印的是 25。可见,用static
修饰后,同样的操作,block里的age值会产生变化。
- 结论:static修饰的局部变量,是指针传递。
指针传递可能导致访问的时候,该变量已经销毁,程序会出问题。
3.全局变量
int age1 = 11;
static int height1 = 22;
...
void (^block)(void) = ^{
NSLog(@"age1 is %d height1 = %d",age1,height1);
};
age1 = 31;
height1 = 33;
block(); // 调用
打印:age1 is 31 height1 = 33
这个情况,并没有捕获全局变量。访问的时候,是直接去访问的,根本不需要捕获。
全局变量本来就是在哪里都可以访问的,所以无需捕获。
关键字__block
上面讨论的是,在block的外部 修改捕获变量的值。那么,如果需要在block代码块内 修改捕获变量的值呢?有3种方法修改局部变量。
1.把局部变量写成全局变量
即,上面提到的第3种情况,这里不再累赘。
全局变量是所有地方都可访问的,在block内部也可以直接操作它的内存地址。调用完block之后,全局变量指向的地址的值已经被更改。
2.把局部变量用static修饰
即,上面提到的第2种情况,这里不再累赘。
当局部变量用static修饰之后,这个block内部会把变量的地址捕获了。这样的话,当然在block内部可以修改局部变量了。
以上两种方法,虽然可以达到在block内部修改局部变量的目的,但缺点是变量无法及时销毁,会一直存在内存中。而很多时候,我们只需要临时用一下,当不用的时候,能销毁掉。那么就需要第3种方法__block
。
3.关键字__block
把局部变量用__block
修饰。
__block int age = 20;
void (^block)(int a) = ^{
age = a;
NSLog(@"age is %d",age);
};
block(33); // 调用
打印 age is 33
注意:__block
不能修饰全局变量、静态变量static
——>下面来探索
__block
的底层:
因为捕获了__block
变量age,这个时候block的底层结构体为:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
// fp是函数地址 desc是描述信息 __Block_byref_age_0 类型的结构体 *_age flags标记
__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; //fp是函数地址
Desc = desc;
}
};
相比一般的block,发现多了__Block_byref_age_0
类型的结构体,其具体结构是:
struct __Block_byref_age_0 {
void *__isa; //isa指针
__Block_byref_age_0 *__forwarding; // 指向自身的指针
int __flags;
int __size;
int age; //使用值
};
这个结构是因为__block
变量产生的。其中第二个__forwarding
存放指向自身的指针,第五个age
里面存放局部变量。
- __block底层原理和逻辑:
编译器会将__block
变量 包装成一个对象。调用变量时,根据__Block_byref_age_0
里的__forwarding
,找到变量age所在的内存,然后修改值。
block访问OC对象
上面讨论的都是block访问变量。如果访问OC对象,又会如何?
NSObject *obj = [[NSObject alloc] init];
void (^block)(void) = ^{
NSLog(@"%@",obj);
};
block( ); // 调用
这时,block的结构体如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// ——>
NSObject *__strong obj;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _obj, int flags=0) : obj(_obj) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
内部 会根据代码中的修饰符(__strong、__weak、__unsafe_unretained)
而对其进行强引用或弱引用。
- 堆空间的block 对其捕获的
__block
变量会形成强引用。
因此,虽然栈上的局部变量随时会销毁。但对于__block
修饰的局部变量,却有了强引用。
栈上的block,并不会对 任何捕获的变量产生强引用。
block循环引用问题
typedef void (^YZBlock) (void);
@interface YZPerson : NSObject
@property (copy, nonatomic) YZBlock block;
@property (assign, nonatomic) int age;
@end
{
YZPerson *person = [[YZPerson alloc] init];
person.age = 10;
person.block = ^{
NSLog(@"person.age--- %d",person.age);
};
}
程序结束,person都没有释放,造成了内存泄漏。
原因:block会自动copy到堆上(block内部的变量person也会被copy到堆上)并且block对person强引用。而本来,block就是person的属性,person对block强引用。互相强引用,谁都释放不了。
解决循环引用
有3种方式来解决:
1.关键字__weak
{
YZPerson *person = [[YZPerson alloc] init];
person.age = 10;
__weak YZPerson *weakPerson = person;
person.block = ^{
NSLog(@"person.age--- %d", weakPerson.age);
};
}
当局部变量消失时候,对于YZPseson
来说,只有一个弱指针指向它,那它就销毁,然后block也销毁。
2.关键字__unsafe_unretained
跟上面的也类似,能够解决循环引用。但是不安全,指向的对象销毁时,指针存储的地址值不变。
3.关键字__block
__block YZPerson *person = [[YZPerson alloc] init];
person.block = ^{
NSLog(@"person.age--- %d",person.age);
person = nil; //这一句 不能少
};
这个方法的本质是,在block内部改变 局部变量的值(置nil
)
在ARC下,上面三种方式对比,最好的是
__weak
。
在MRC下,因为不支持弱指针__weak
,只能用__unsafe_unretained
或__block
来解决循环引用。