大赏程序员iOS学习笔记

Block的三次探索

2017-04-24  本文已影响358人  iOS_aFei

Block是带有自动变量的匿名函数。
匿名函数的含义是Block没有函数名,另外Block带有插入记号“^”,插入记号便于查找到Block。

一般形式的Block:

^int (int cs) {
    return cs * 2;
};

如果Block没有参数,参数列表可省略,形式如下:

^int{
    return 666;
};//参数列表的括号也可以省略

在C语言中,函数返回值类型省略则默认为int类型;在Block中也可以省略返回值类型,不过与C语言函数不同,Block省略返回值类型时,如果表达式中有return语句就使用该返回值的类型,没有return语句就使用void类型。
返回值为void类型时的Block:

^{
    printf("iam Block”);
};

返回值为int类型时的Block:

^{
    return 666;
};

上面定义了Block语句块,要想使用还需要Block类型变量:

int (^block1)(int) = ^int (int cs) {
    return cs * 2;
};
printf("%d",block1(666));

声明Block类型变量:

int (^block2)(int);

Block类型变量可以作为自动变量、函数参数、静态变量、全局变量使用。以函数参数为例:

int test(int (^block1)(int)) {
    return 1;
}

除了看起来长一点也没啥,typedef来解决_

typedef int (^Block)(int);
int test(MyBlock block1) {
    return 1;
}

这样就舒服了、看起来。

接下来就是高潮部分了,就是”带有自动变量“以及如何带有自动变量?

int var1 = 10;
int var2 = 20;
void (^block3)(void) = ^{
     printf("%d",var1);
};
var1 = 20;
block3();

情况1:打印结果是10,而不是20。
情况2:在Block中修改var1的值会报错。
情况3:如果修改的是Objective-C对象,例如NSArray对象,不可以对其赋值但是可以增删元素。
情况4:对于全局变量、静态全局变量、静态变量在块中可以修改。
情况5:使用__block修饰var1变量后,打印的结果是20,而且var1可以在块中被修改。
另外注意:不能在block中访问C语言字符数组,但是可以访问C语言字符串,也就是说Block并没有截获字符数组。

解释

要想明白为什么会出现上述情况,我们必须深入了解Block,打开终端进入项目main.m所在的文件夹,使用命令clang -rewrite-objc main.m,执行之后该文件夹下会出现一个main.cpp的C++文件,文件中代码很长,我们只需看重要的一些代码。
这是一个没有使用局部变量的block:

#import <Foundation/Foundation.h>
int main() {
    void (^block)(void) = ^{
        printf("block\n");
    };
    block();
    return 0;
}

在main.cpp中我们看这些代码:

struct __block_impl {
    void *isa;   
    int Flags;
    int Reserved;
    void *FuncPtr;
};//^^^^结构体2^^^^

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;
  }
};//^^^^结构体1^^^^

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        printf("block\n");
    }

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)};
//^^^^结构体3^^^^

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

有三个结构体(代码中已标记)我们必须铭记于心,或许你看到这里就不想往下看了,但是你对一个知识点的理解深度与面试官对你的好感成正比,Block几乎每个iOS面试官都会问到。

结构体1:

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;
  }
};

这个结构体包含结构体2和结构体3,另外还有一些在该block中使用到的局部变量,最后是一个构造函数,对结构体的成员(上述结构2、结构3、局部变量)进行初始化。请注意构造函数的第一个参数,下面还会说。

结构体2:

struct __block_impl {
    void *isa;      //block也是对象
    int Flags;      //标志
    int Reserved;   //版本升级所需的区域
    void *FuncPtr;  //函数指针
};

结构体3:

static struct __main_block_desc_0 {
  size_t reserved;      // 版本升级所需的区域
  size_t Block_size;    // Block的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

然后会发现上面还有这个函数:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        printf("block\n");
    }

对于上面这段代码是不是很熟悉,很明显对应于我们定义的block,也就是Block使用的匿名函数被转换为了C语言函数,命名的根据Block所在的函数名和出现的顺序。需要注意的是还有一个struct __main_block_impl_0 *__cself指针,为什么要这样设计Block呢?结合类、对象、方法、自己思考。
上面说注意结构体1中构造函数的第一个参数,现在看下main函数中void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));就是用上面的函数的地址作为参数的。
第二个参数时作为静态全局变量传入的,在结构体3最后可以看到使用__main_block_impl_0的大小进行初始化的。
这段代码就是将栈上生成的变量赋值给block变量。

上面看了最简单的Block的转换代码,下面来看一个使用到了局部变量的block:

#import <Foundation/Foundation.h>
int main() {
    int var1 = 10;
    int var2 = 10;
    void (^block)(void) = ^{
        printf("%d\n", var1);
    };
    block();
    return 0;
}
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  
  int var1;
  
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _var1, int flags=0) : var1(_var1) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

在结构体1中加入了使用到的局部变量。现在就可以解释情况1-3了:
情况一和情况二解释:自动变量会以值传递的方式拷贝到Block的结构体中,因为并没有传递自动变量的地址,所以不能修改自动变量的值。
情况三解释,修改NSArray对象可以,重新赋值则不行,因为修改对象并没有改变对象指针。

下面再来说说__block说明符:
通过上面的方法可以对非静态全局变量、静态全局变量、静态局部变量、__block变量进行测试,可以得到:
情况四解释:对于访问全局变量和静态全局变量,对于Block的结构体没有任何影响,因为其地址是不变的、作用域也足够广。对于静态局部变量,虽然地址是唯一的,但是Block超出了其作用域,所以将静态局部变量的指针传递给了Block的结构体。
为什么不将自动变量的地址拷贝到Block中呢?
自动变量超出作用域之后会被废弃,而Block可能被拷贝到堆上。所以我们看下__block变量的实现。

struct __Block_byref_var1_0 {
  void *__isa;
__Block_byref_var1_0 *__forwarding;
 int __flags;
 int __size;
 int var1;
};

变换后变成了结构体。
其初始化为:

__attribute__((__blocks__(byref))) __Block_byref_var1_0 var1 = {(void*)0,(__Block_byref_var1_0 *)&var1, 0, sizeof(__Block_byref_var1_0), 10};

从初始化可以看出:__forwarding指针指向自己(不一定,后面会说),var1变量相当于原自动变量。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_var1_0 *var1 = __cself->var1; // bound by ref

        printf("%d\n", (var1->__forwarding->var1));
    }

访问var1时为什么兜圈子呢?后面会说。

另外还有结构体2中的isa指针,我们知道在类与对象中:对象的isa指针指向所属的类,类的isa指针指向元类,元类的isa指针指向根元类,根元类的isa指针指向自己。
而Block的isa指针的指向有三种,而且我们暂时给出它们的特点:
NSConcreteStackBlock:存储在栈上的的Block。特点:使用了局部变量,可以被赋值到堆上。
NSConcreteMallocBlck:存储在堆上的BlocK。特点:由NSConcreteStackBlock拷贝到堆上,持有对象
NSConcreteGlobalBlock:存储在程序的数据区。特点:不使用局部变量,整个程序中只需一个实例。

为什么,第一个事例没有使用局部变量,isa指针却:

impl.isa = &_NSConcreteStackBlock;

虽然说isa指针指向的是_NSConcreteStackBlock,但它的实现是NSConcreteGlobalBlock,因为它没有使用局部变量。

设置在栈上的Block超出作用域会被废弃,__block也会被废弃。在以下几种情况Block会被复制到堆上:
1、调用copy方法(_NSConcreteStackBlock调用copy方法会从栈复制到堆,_NSConcreteGlobalBlock什么也不做,_NSConcreteMallocBlock引用计数加一);
2、作为函数返回值;
3、将Blcok赋值给类的成员变量时;
4、向方法名中含有usingBlock的Cocoa框架方法或GCD的API传递Block时。

__block变量会跟随Block从栈复制到堆、并被Block持有。栈上的__blcok变量的__forwarding指针也指向堆上的结构体,保证了可以访问同一个__block变量。

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

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

这两个函数就是保证__block变量被复制到堆上被Block持有和释放。
这个实例中我们使用__block修饰的int类型,如果修饰的对象呢?
对象有四种修饰符:__strong、__weak、__unsafe_unretained、__autorelealeasing四种修饰符,对于````__strong同样是使用上述的两个函数进行持有和释放。而对于__weak修饰的对象,当超出作用域后被废弃,也就是即使同时使用__block __weak修饰了对象,但是当对象废弃后,Block持有的对象也会变成nil。同理__unsafe_unretained需要注意悬垂指针的问题。最后同时使用__block __autoreleasing```会编译错误。

上面还曾说过一句话:堆上的Block持有对象,现在说完:堆上的对象会持有__strong对象,而栈上的Blcok不持有。

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}

当栈上的Block复制到堆上时,会调用这copy函数持有对象,使用dispose函数释放对象。
Blcok通过参数区分copy和dispose函数的对象类型是对象还是__block变量。

最后就是Block的循环引用:
循环引用这里就不在细说,解决方式其一就是使用__weak修饰符来解决。
现在主要说一下另外一种方式:
使用__block变量手动置nil:

   __block id tmp = self;
    _myblock = ^{
        [tmp doSomething];
        tmp = nil;
    };

但是需要注意此Blcok必须要的执行

最后补充:我们在将Blcok从栈copy到堆上时都是使用copy,这是因为栈上的Block使用retain是无效的,只有使用copy函数可以。但是对于在堆上的Block可以通过copy和retain持有,所以还是推荐使用copy。

对于Block的探索,这不是第一次也绝对不是最后一次。

上一篇下一篇

猜你喜欢

热点阅读