1.__block修饰自动变量
(1)block块内使用自动变量
如我们所知,我们在block中使用的自动变量是捕获的外部自动变量,即通过向生成block对象时构造函数里传如自动变量的值,并不能真正地对自动变量进行赋值操作,也不能读取到运行时的自动变量状态。当我们需要这么做时,可以通过__ block 修饰符告知编译器,这样我们就可以在block访问到真正的自动变量。
如
//test.m
#import <Foundation/Foundation.h>
int main(){
int __block a = 0;
int (^blk)(int) = ^(int b){return a+b;};
a++;
int c = blk(5);
NSLog(@"%d",c);
return 0;
}
如果不加__block
则会输出5,加了输出6。不加的捕获机制我们上篇文章已经解析过,即通过在block中定义一个同名变量,并在构造函数中传参初始化。这一篇我们看如何实现真正访问外部变量,照旧用clang处理为.cpp文件查看实现方法,命令如下
clang -rewrite-objc test.m
得到test.cpp文件,打开查看main中的调用
int main(){
__Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 0};
int (*blk)(int) = ((int (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
(a.__forwarding->a)++;
int c = ((int (*)(__block_impl *, int))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, 5);
return 0;
}
首先,变量a被修改成为一个__ block类型对象,类型是__Block_byref_a_0
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
通过{}初始化isa为nil,__ forwarding为__Block_byref_a_0
类地址,flags为0,__ size为类大小,a为我们初始化的值。
然后定义一个block对象,类型是__main_block_impl_0
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
与我们上篇文章有所区别的是,block对象__main_block_impl_0
中不再是定义一个同名int变量a,而是改为获取我们标记为__block
的对象a的地址。我们上边提到a的地址存于其__forwarding
成员变量中,通过对象指针进行引用。block对象的构造函数中即传入(a->_ forwarding)在初始化列表中初始化对象指针__Block_byref_a_0 *a
。这下我们就清楚了有关初始化Block对象和__ block变量生成对象的过程了。
传入的flags
impl.Flags = 570425344;
//570425344 = (0010 0010 0...0)b,即0x2200 0000 ,
//25位为1,29位为1
//falgs bit位描述如下
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30) // compiler
};
//即BLOCK_HAS_COPY_DISPOSE,到堆中需要的类似retain/release方法。
//BLOCK_USE_STRET使用结构体变量
我们看下block匿名函数FuncPtr
的实现。其指向__main_block_func_0
static int __main_block_func_0(struct __main_block_impl_0 *__cself, int b) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
return (a->__forwarding->a)+b;}
通过指针访问编译器封装的__ block变量— — a对象实现访问真正的变量。对于a++也进行了重载实现
即变为
(a.__forwarding->a)++;
所以通过__ block修饰符对变量进行封装使得自动变量变为对象,通过传指针引用实现访问。
我们看到,还提供了两个方法
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
即copy方法,和dispose方法。这是干什么用的呢?
__main_block_copy_0
中调用了_Block_object_assign,其相当于retain的作用
__main_block_dispose_0
中调用了_Block_object_dispose,其相当于release的作用
当我们吧栈区域的block复制到堆上时,调用__ main_block_copy_0,堆上block被废弃时调用__ main_block_dispose_0。在ARC下因为编译器会进行将block对象及变量从栈复制到堆的操作,我们不需要对此进行太多关心。
代码如下
#import <Foundation/Foundation.h>
int main(){
int __block a = 0;
int __autoreleasing (^blk)(int) = nil;
blk = ^(int b){return a+b;};
_objc_autoreleasePoolPrint();
a++;
int c = blk(5);
NSLog(@"%d",c);
return 0;
}
结果
objc[2208]: ##############
objc[2208]: AUTORELEASE POOLS for thread 0x100393340
objc[2208]: 1 releases pending.
objc[2208]: [0x103005000] ................ PAGE (hot) (cold)
objc[2208]: [0x103005038] 0x102900050 __NSMallocBlock__
objc[2208]: ##############
被扔进了autorelease pool的block对象如同其他对象一般进行ARC引用计数管理。
管理不当,提前释放block对象导致crash的示例代码如下
#import <Foundation/Foundation.h>
extern void _objc_autoreleasePoolPrint(void);
int main(){
int __block a = 0;
int __autoreleasing (^blk)(int) = nil;
@autoreleasepool{
blk = ^(int b){return a+b;};
_objc_autoreleasePoolPrint();
a++;
}
NSLog(@"%p",blk); //0x10053e0e0
_objc_autoreleasePoolPrint();
int c = blk(5); //Thread 1: EXC_BAD_ACCESS (code=1, address=0x0)
NSLog(@"%d",c);
return 0;
}
在ARC中,我们定义在函数作用域内的block对象是堆对象,如同其他对象一样需要考虑避免循环引用的问题。
(2)block块内使用自动变量的内存
我们从生成的cpp代码中可以看出,__ block修饰的自动变量a被变为了一个对象,在ARC中打印其地址也显然是在堆区域。
但是我们在调试中的sizeof也罢,变量类型也罢,都会是int型,我们想要验证下其内存是否真的如CPP描述的那般呢?不得不说,OC毕竟是C的后代,在大概预测到其内部结构后,我们通过指针操作就可以获取其结构体的全部元素了。测试一下吧
#import <Foundation/Foundation.h>
extern void _objc_autoreleasePoolPrint(void);
typedef int (^blk_t)(int);
//struct __Block_byref_a_0 {
// void *__isa;
// __Block_byref_a_0 *__forwarding;
// int __flags;
// int __size;
// int a;
//};
blk_t func(){
int __block a = 0;
int * p = &a; //指向变量a
NSLog(@"the value address is %p",p); //变量a的地址0x7ffeefbff568
NSLog(@"%lu",sizeof(p)); //8字节
p--; //获得__Block_byref_a_0,size,
NSLog(@"%d",*p); //32,不是28,输出剩下的4字节是0,说明是为了对齐使用,多了4字节
p--; //flags,536870912 = 0x20 00 00 00,BLOCK_USE_STRET = 1;
NSLog(@"%d",*p);
p--;
p--; //指向__forwarding,即struct地址
unsigned long long * pt = p;
NSLog(@"%p",*pt); //struct地址0x7ffeefbff550,即与变量a差了24字节。
pt--;//指向isa
NSLog(@"%p",*pt); //isa =0x0;
int __autoreleasing (^blk)(int) = nil;
blk = ^(int b){
a++;
return a+b;};
return blk;
}
int main(){
blk_t blk = func();
int c = blk(5);
NSLog(@"%d",c);
c = blk(5);
NSLog(@"%d",c);
return 0;
}
符合预期的结果,ARC下自动变量生成的对象存活在堆区,由block对象持有,在block对象释放时被释放。
2.避免循环引用
(1)__ weak解决强引用循环
当我们在block内使用对象指针访问对象时,默认无修饰符或者__ strong 修饰符将会使得block对象通过该对象指针强持有对象。
产生循环引用的示例
@interface test:NSObject
{
blk_t blk;
}
@end
@implementation test:NSObject
-(id) init
{
self = [super init];
blk = ^{NSLog(@"the self is %p",self);};
//Warning: Capturing 'self' strongly in this block is likely to
//lead to a retain cycle
return self;
}
@end
如我们所知,block会捕获自动变量,即传入self变量的值初始化对象内同名变量,解决办法是用__ weak修饰符
self = [super init];
id __weak tmp = self;
blk = ^{NSLog(@"the self is %p",self);};
(2)__ block解决强引用循环
使用block后变为引用tmp变量,相当于持有二级指针,但是显然这个tmp也是强引用,我们的程序通过二级指针访问对象时只要tmp未消亡,依旧会强持有对象,所以我们在执行完该block()后通过nil置0,调用stroreStrong对tmp指向的对象进行release。即相当于强持有对象直到运行完一次。不调用blk()仍会导致强引用循环。
self = [super init];
id __block tmp = self;
blk = ^{
NSLog(@"the self is %p",tmp);
tmp = nil;
};
借用@autoreleasepool的托管释放特性,测试代码如下
#import <Foundation/Foundation.h>
extern void _objc_autoreleasePoolPrint(void);
typedef void (^blk_t)(void);
@interface test:NSObject
{
blk_t blk;
}
- (void)mustDo;
@end
@implementation test:NSObject
-(id) init
{
self = [super init];
id __block tmp = self;
blk = ^{
NSLog(@"the self is %p",tmp);
tmp = nil;
};
return self;
}
- (void)mustDo
{
blk();
}
@end
int main(){
id __weak weakP = nil;
@autoreleasepool{
_objc_autoreleasePoolPrint();
id __autoreleasing tes = [[test alloc] init];
weakP = tes;
NSLog(@"weak is %p",weakP);
[tes mustDo]; //注释掉和没有注释掉有什么不同
_objc_autoreleasePoolPrint();
}
_objc_autoreleasePoolPrint();
NSLog(@"really dealloc?");
NSLog(@"weakP is %p",weakP);
return 0;
}
可见运用__block变量修改时的好处是能够赋值对象指针为nil的操作前,强持有对象。我们需要某些对象必须执行这个方法才能释放时可以使用这一方法。