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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容