block系列3-- __block变量

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的操作前,强持有对象。我们需要某些对象必须执行这个方法才能释放时可以使用这一方法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容