Block高级篇

__block说明符

前面讲到Block会捕获外部变量,但是当你试图在Block里面修改捕获的外部变量时。就是出现编译错误,解决的一种办法是将外部变量使用__block修饰符修饰。下面是添加__block修饰的外部变量代码:

#include <stdio.h>

int main(int argc, const char * argv[]) {

    __block int val = 10;
    void (^blk)(void) = ^{ val = 1; };

    return 0;
}

该代码可进行编译。变换后如下:

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
}; // [1]


struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; 

  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};


static void __main_block_func_0(struct __main_block_impl_0 *__cself) 
{
      __Block_byref_val_0 *val = __cself->val; // bound by ref
     (val->__forwarding->val) = 1; 
} // [2]


static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) 
{
    _Block_object_assign((void*)&dst->val, 
    (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) 
{
    _Block_object_dispose((void*)src->val, BLOCK_FIELD_IS_BYREF*/);
}

static struct __main_block_desc_0 
{
    size_t reserved;
    size_t Block_size;
    void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
      void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 
    0, 
    sizeof(struct __main_block_impl_0), 
    __main_block_copy_0, 
    __main_block_dispose_0
};


int main(int argc, const char * argv[]) 
{
    __Block_byref_val_0 val = {        
        (void*)0,
        (__Block_byref_val_0 *)&val,
         0, 
         sizeof(__Block_byref_val_0), 
         10
    };

    blk = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344);

    return 0;
}

我们可以发现加上__block说明符,源码量就急剧的增加了(已做简化)。

  • 我们可以发现含有__block修饰的变量变成了__Block_byref_val_0结构体。[1]
  • __Block_byref_val_0结构体实例的成员变量__forwarding持有该实例自身的指针。
  • Block转化而来的的__main_block_func_0结构体实例持有指向__block变量的__Block_byref_val_0结构体实例的指针。__Block_byref_val_0结构体实例的成员变量__forwarding持有指向该实例自身的指针。通过成员变量__forwarding访问成员变量val的地址,从而可以修改自动变量。
  • 我们需要负责Block_byref_i_0结构体相关的内存管理,所以main_block_desc_0中增加了copydispose函数指针,对于在调用前后修改相应变量的引用计数。

Block存储域

__block变量转换成了__block变量的结构体类型的自动变量。所谓结构体类型的自动变量,即栈上生成的该结构体的实例。如下表所示:

名称 实质
Block 栈上Block的结构体实例  
__block变量 栈上__block变量的结构体实例  

前面我们看到Block的类型说明_NSConcreteStackBlock.虽然该类没有出现已变换源代码中,但还有与之相识的类,如:

  • _NSConcreteStackBlock
  • _NSConcreteGlobalBlock
  • _NSConcreteMallocBlock

他们分别对应的存储区域如下所示:

设置对象的存储区域
_NSConcreteStackBlock
_NSConcreteGlobalBlock 程序的数据区域(.data区域)
_NSConcreteMallocBlock

定义Block时期内存区域分配在栈中,其Block类型为__NSConcreteStackBlock类对象.
那么_NSConcreteMallocBlock类型的对象由何而来?这里肯定有人还在疑惑为什么__block变量转化而来的结构体为生成指向自身的__forwarding指针变量。其目的是为了能然超出范围的Block也有效。有人会说设置个全局的Block不就可以搞定了么。

行不行先来段代码看看:

void (^blk)();
if (/* some condition */) {
    blk = ^{ NSLog(@"Block A"); };
}else {
    blk = ^{ NSLog(@"Block B"); };
}
blk();

看这段代码我声明了全局的Block变量blk,然后在if语句中定义。如果你不理解block那么就很容易写出这样的代码,其实这段代码是很危险的。因为全局的blk变量是分配在栈上的。在if和else语句中定义的blk内容,编译器会给每个块分配好栈内存,然后等离开了相应的范围之后,编译器有可能把分配给块的内存覆写了。如果编译器未覆写这块栈内存则程序照常运行,如果这块内容被覆写那么程序就会崩溃。

解决上面问题的方法就是使用copy方法,将block拷贝到堆中。拷贝完之后就是接下来要将的_NSConcreteMallocBlock类型。该类型是带有引用计数的对象,如果在ARC下,只要引用计数不为0,可以随意的访问,后继的内存管理就交给编译器来完成了。

还有一种类型是_NSConcreteGlobalBlock类型,这类Block不会捕捉任何状态的外部变量。块所使用的整个内存区域,在编译器已经完全的确定了,不需要每次调用时在栈中创建,如下就是一个全局快:

void (^blk)() = {
    NSLog("This is a global block");
}

void main() {
}

__block变量存储域

如果Block配置在栈中,则在Block中使用的__block变量也分配在栈中。当Block被复制到堆中时,__block变量也一并被复制在堆中,并被Block所持有。如果非配在堆中的Block被废弃,那么它所使用的__block变量也就被释放了。下面来看堆和栈上__block混用的例子

__block int val = 0;

void (^blk) (void) = [^{val++;} copy];

++val;

blk();

NSLog(@"val:%d",val);

执行结果为:

val: 2

在Block中和在Block外修改__block变量完全等效,它是怎么实现的呢?
是因为执行copy方法之后,Block被复制到堆中,其内部捕获的__block变量也一并被复制。而此时分配在栈上的val任然存在的,栈上的__block变量val会将原本指向自身的__forwarding指针指向复制到堆中的__block变量val的地址。这样堆中的__block变量被修改之后就等同于栈上的block被修改。

通过该功能,无论是在Block的语法中、Block语法外使用__block变量,还是__block变量配置在栈上还是堆上,都可以顺利地访问一个__block变量。

截获对象

先来看一段Blcok截获可变数组对象的例子:

blk blk;
{
    NSMutableArray *array = [[NSMutableArray alloc] init];
    blk = ^(id obj){

        [array addObject:obj];
        NSLog(@"arrayCount = %lu",(unsigned long)array.count);
    };
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);

执行该段代码的结果为

arrayCount = 1;
arrayCount = 2;
arrayCount = 3;

从表面上看是没什么问题,运行的结果也是正确的。而实际上如果我们大量的调用block向可变数组中添加对象元素程序会强制结束。原因是block截获的NSMutableArray对象是分配在栈上的,随着当可变数组元素增加到一定程度会造成栈溢出。

解决方法是调用copy方法,形式如下:

blk = [^(id obj){
   [array addObject:obj];

   NSLog(@"arrayCount = %lu",(unsigned long)array.count);
} copy];

Block循环引用

在实际项目中对于Block最常见的问题应该是循环引用。如果Block中使用了__strong修饰符的对象,那么当block从栈复制到堆时,该对象为Block所持有。这样容易造成循环引用,比较明显的我想大家肯定遇到过,我们来看一个比较隐蔽的,源代码如下:

typedef void (^blk_t)(void);
@interface MyObject : NSObject


@property (nonatomic, strong) id obj;
@property (nonatomic, strong) blk_t blk;

@end

@implementation MyObject

- (id)init
{
    self = [super init];

    _blk = ^{NSLog(@"obj = %@",_obj);};

    return self;
}

@end

通过编译器我们可以看到造成了循环引用,即Block语法内部使用了_obj变量,是因为_obj变量实际上截获了self。对编译器来说,_obj变量只不过是对象的成员变量罢了。

解决的方法便是便是通过__weak修饰符来修饰会被Block捕获的变量:

id __weak obj = _obj;

_blk = ^{NSLog(@"obj = %@",obj);};

还有一种定义设置weak变量的方式,可以用于宏定义。代码如下:

#define WS(weakSelf)  __weak __typeof(&*self)weakSelf = self;

调用WS(ws)之后ws就变成为了__weak修饰符修饰的self了。

如果你觉得我写的东西对你有点价值的话,希望你能为我增加一个关注量。我的公众号「iOSTalk」。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值