Blocks

这一篇主要是对Blocks的总结。

既然说是对Blocks的总结,那什么是Blocks?


Blocks是对C语言的扩充功能,一句话概括就是带有自动变量的匿名函数。顾名思义,匿名函数就是没有名称的函数。


好啦,接下来带着疑惑来看看Block的语法吧~


如它的定义所示,它没有函数名,它可以有自动变量也就是形参,还可以有返回值,另外Blocks带有插入记号“^”,该记号便于查找到Block。


一般的形式为:^ 返回值类型 (参数列表){ 表达式  };

^int (int a){
            return a;
        };


二般情况下,Blocks没有参数,参数列表也是可以省略的。

^int{
            return 66666;
    };


参数都可以省略,返回值类型当然也是可以省略的啦~


^(int a){
            NSLog(@"%d",a);
        };
/*这个block返回void类型*/
^(int a){
            return a;
        };
        /*
         这个block省略了返回值类型,但是有返回语句,故该block返回int类型
         */

※※※在这里需要注意的是,当省略返回值类型时,如果表达式有return语句就使用该返回值的类型,如果没有就使用void类型。如果表达式有多个return语句,那么所有return语句的返回值类型必须相同。


既然两个都可以省略,那可以一起省略不???当然可以啦~~~

^{
     NSLog(@"test");
 };
像这样,省略了返回值类型和参数列表的block形式应该最熟悉不过了吧~


Block语法就是这么简单啦~从记述方法来看,除了没有名称以及带有^记号外,其他的和C语言函数定义也大同小异啦~


在C语言中,可以把函数的地址赋给函数指针类型变量。同样的,在Block语法下,也可以将Block语法赋给声明为Block的类型变量。好的,那接下来就看看Block类型变量呐~(∩_∩)

int (^blk)(int);
像这样, Block类型变量的声明方式为:返回值类型 (^ 变量名)(参数列表);

看过它的声明方式,下面就看看如何将Block语法赋给它吧、

int (^blk)(int) = ^int (int count){return  count+1;};


Block类型变量也可以向其他的Block类型变量赋值。

int (^blk1)(int );
blk1 = blk;
Block类型变量也 可以作为自动变量、函数参数、静态变量、全局变量来使用。

这里以作为函数参数来举例。

int test (int (^blk)(int a))
{
    ...
}
大家是不是觉得Block类型变量作为函数参数使用有点长呢?恩,其实我也是这么觉得~

这时候呢,就可以像使用函数指针类型时那样,用typedef来解决这个问题啦~

typedef int (^block)(int );

int test(block blk){
    ...
}

这样是不是看着就舒服多啦~哈哈~

在使用Block时,其实就像调用函数那样,直接调用就可以啦~

int a = blk(1);
NSLog(@"%d",a);//输出结果为2
        
int b = test(blk);


既然说,Block是带有自动变量的匿名函数,那它是如何带有自动变量的呢?这就是接下来要说的啦~

{
        int a = 5;
        const char *s = "value is";
        
        void (^blk)(void) = ^{
            NSLog(@"%s:a = %d",s,a);
        };
        
        a = 10;
        s = "value changed";
        
        blk();
}
通过运行这段代码,发现输出的结果为 : value  is:a = 5;


恩?a的值居然没变?exo me???


真相只有一个!在Block中带有自动变量值表现为截获自动变量值,也就是说,Block表达式截获所使用的自动变量的值,即保存该自动变量的瞬间值。所以呢,在执行了Block语法后,即使改写自动变量的值也不会影响Block执行时所使用的值。


小明说,在外面改不行,那我在Block里面改总行了吧!哼哼~




like this?编译器会报错的,那该怎么办?不用担心,使用__block说明符就好啦~

int __block b = 0;
        void (^blk)(void) = ^{
            b = 1;
        };
像这样的,使用 __block说明符 的变量可以在Block中赋值,把这种变量称为 __block变量


刚刚说到在Block中向截获的自动变量赋值会报错,那截获OC对象呢?也会出错么?




从这个栗子可以看出,使用截获的OC对象没有任何问题,但是向其赋值会产生编译错误。这种情况,也是需要给截获的自动变量添加__block说明符才能对其赋值。


还有一个需要注意的,在使用C语言数组时需要小心使用指针。




诶?这次怎么只是使用它也报错?因为在现在的Block中,截获自动变量的方法并没有实现对于C语言数组的截获。但是可以使用指针来解决这个问题。

const char *str = "hello";
void (^blk)(void) = ^{
            NSLog(@"%c",str[2]);
        };


对于这一块的问题总结如下:

1.为什么Block中不能修改自动变量值?

答:自动变量以值传递的方式将自动变量的值保存在Block的结构体实例中,因为并没有传递自动变量的地址,故不能修改值。


2.为什么对OC对象增删元素可以但是对其赋值会报错?

答:修改OC对象可以,因为修改对象并没有改变对象指针。


3.为什么全局变量、静态全局变量、静态变量可以在Block中修改?

答:全局变量、静态全局变量的地址不变,作用域广,不会对Block的结构体产生影响,所以可以在Block中修改值。对于静态变量,虽然地址是唯一的,虽然Block超出了它的作用域,所以讲静态变量的指针传给了Block的结构体,故在Block中也可以修改值。


4.为什么使用__block说明符的自动变量可以在Block中修改值?

答:将 int __block a = 0; 通过clang命名查看源码如下(截取片段):

#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)
struct __Block_byref_var_0 {
  void *__isa;
__Block_byref_var_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

通过此段源码,可以看到__block修饰的变量生成了自己的结构体实例,__forwarding是指向自身的指针,a相当于原自动变量。

也就是说,这个结构体实例并不属于某个Block,因为这要保证多个Block可以使用一个__block 变量。

好啦。这几个问题就说到这,有不同见解的期待评论。


既然说到源码了,那我们也来看看Block的源码吧~

int main(){
    void (^blk)(void) = ^{
        
    };
    blk();
    return 0;
}

就这么几行源码,通过clang指令转换后是什么呢?

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  //结构体构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//__cself是指向Block值得变量。Block转换过来的函数,根据所属函数名以及出现的顺序命名
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
}
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(){
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

是不是有点惊讶,怎么这么多啊?我们重点看看最后一个函数,也就是main函数。

第一句转换部分有点难划分,做如下处理:

 struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA));
        struct __main_block_impl_0 *blk = &tmp;
这一句就是将__main_block_impl_0结构体类型的自动变量赋值给__main_block_impl_0结构体指针类型的变量blk.

第二句除去转换部分可变化为:

(*blk->impl.FuncPtr)(blk);
这一句就是简单的函数指针调用函数,在__main_block_impl_0结构体构造函数中,由Block转换的__main_block_func_0函数的指针被赋值给成员变量FuncPtr,另外__main_block_func_0函数的参数__cself指向Block值。所以说在函数调用中Block是作为参数进行传递。


在这里,还需要说明的一点是isa指针。在OC中,每一个对象都一个isa指针,指向对象的类。每个类也有一个isa指针,指向它的元类。元类也有一个isa指针,指向根元类。根元类的isa指针指向自己。


至此,应该已经说明了Block的实质,就是Objective-C对象。

__main_block_imp_0结构体就相当于OC对象的结构体。

将Block作为OC的对象处理,关于该类的信息放在_NSConcreteStackBlock中,对isa指针初始化。


OK~说到_NSConcreteStackBlock了,那就说说Block存储域吧

block根据在内存的位置可以分为三种类型:

__NSConcreteGlobalBlock : 一般是不使用自动变量或者使用全局变量的Block,存储在程序的数据域。不依赖与执行时的状态,所以整个程序中只有一个实例。不持有对象

_NSConcreteStackBlock : 使用自动变量的Block,存储在栈中。不持有对象

_NSConcreteMallocStack : 复制栈上的Block到堆中。不持有对象


提问 : 为什么要复制栈上的Block到堆上呢?

回答 : 在栈上的Block超出作用域就会被废弃,当然位于栈上的__block变量也一样。如果将Block和__block变量从栈上赋值到堆上,不就解决啦,这样即使block语法记述的变量作用域结束,堆上的Block仍然存在。在前面说过,__block实例结构体中的__forwarding指针是指向自己的,这样的话无论该变量位于栈上还是堆上都可以正确访问 。


从栈上复制到堆上的情况总结:

1.调用copy实例方法

2.将Block作为函数返回值返回

3.Block被赋值给__strong修饰符修饰的变量时

4.方法名中含有usingBlock的Cocoa框架方法或在GCD的API中传递Block时;


当Block从栈上复制到堆上时会持有对象,若此时对象也持有Block那就会造成循环引用的问题,举个栗子

typedef void (^block)(void);
@interface Object : NSObject
{
    block blk;
}
@end

@implementation Object
- (id)init{
    self = [super init];
    
    blk = ^{NSLog(@"self : %@",self);};
    return self;
}
@end

{
       
        id o = [[Object alloc]init];
        NSLog(@"o : %@",o);
        
        return 0;
 }

在此例中,Block持有self,self持有Block。这正是循环引用。可以使用__weak 或 __unretained_unsafe来解决。


1.使用__weak修饰符

- (id)init{
    self = [super init];
    
    id __weak tmp = self ;
    
    blk = ^{NSLog(@"self : %@",tmp);};
    return self;
}

此处,不用判断tmp是否为nil,因为Block存在时,self一定存在。


2.使用__unsafe_unretained

- (id)init{
    self = [super init];
    
    id __unsafe_unretained tmp = self ;
    
    blk = ^{NSLog(@"self : %@",tmp);};
    return self;
}

使用__unsafe_unretained不用担心垂悬指针的问题。


最后一个问题了。

当ARC无效时,一般需要手动将Block从栈复制待堆上,另外还需要手动是否复制到堆上的Block。这是,通过copy方法从栈上复制到堆上,通过release方法来释放。如果Block有一次复制并配置在堆上就可以通过retain实例方法持有,但是对于配置在栈上的Block使用retain方法没有作用。

当ARC无效时,__block说明符被用了避免Block中的循环引用、因为当Block从栈上复制到堆上时,如果Block使用的变量为富有__block修饰符修饰的变量,该变量不会被Block持有。还是拿上一个循环引用的栗子来解决~

- (id)init{
    self = [super init];
    
    __block id tmp = self ;
    
    blk = ^{NSLog(@"self : %@",tmp);};
    return self;
}

就是这样子的啦、


说了这么多,来总结一下吧、

1. Blocks是带有自动变量的匿名函数。所谓匿名函数,就是没有函数名称,但是有插入记号“^”。

2.Block类型变量,可以作为函数参数、函数返回值等使用。

3.Blocks截获自动变量值,Block中自动变量值不可修改,但是使用__block说明符的自动变量值可以被修改。截获OC对象,可以使用但是不能赋值。不能截获C语言数组,但是可以使用指针。

4.Block的实质就是Objective-C对象。

5.Block的存储域,分为三种:栈、堆、数据区。

6.Block的循环引用和解决办法。

7.ARC无效时Block的使用。

就这么多啦~有不妥当的地方,小伙伴们踊跃提出来呢~

如果对您有所帮助,点个赞哦、双击666~谢谢~



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值