iOS 原理探索-Block(一)

2020/3/8 23:01:43

本文主要是介绍iOS 原理探索-Block(一),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

BlockC语言的扩充功能。用一句话来形容Block的扩展功能:带有自动变量(局部变量)的匿名函数。

Block概述

Block类型变量

Block语法单从其记述方式上来看,除了没有名称以及带有**“^”外,其他的都与C语言的函数定义相同。在C**语言定义函数时,就可以将函数的地址赋值给函数指针类型的变量。

int function(int count) {
		return count+1;
}
int (* functionptr)(int) = &function;
复制代码

声明Block类型的变量的示例如下:

int (^block)(int par);
复制代码

通过和函数指针的对比可以知道,声明Block类型的变量仅仅是将函数指针类型变量的**"*"修改为^,所以该Block类型的变量与一般的C**语言变量完全相同,可以作为以下用途使用。

  • 自动变量(局部变量)。

  • 函数参数。

  • 静态变量。

  • 静态全局变量。

  • 全局变量(属性)。

接下来我们定义一个Block并将其赋值给Block类型的变量。

int (^block)(int) = ^(int count){
    return count+1;
};
复制代码

由**^开始的Block语法生成的Block被赋值给变量block中,因为和普通变量相同,所以当然也可以由一个Block类型的变量向另一个Block**类型的变量赋值。

int (^block1)(int) = block;
int (^block2)(int);
block2 = block1;
复制代码

另外我们也可以在函数的参数和函数的返回值中使用Block

通过上述我们可以看出,在声明Block类型的变量和用Block作为函数的参数和返回值中,记述的方式及其复杂。这是我们可以像使用函数指针那样使用typedef来解决问题。

typedef int (^block_t)(int);
复制代码

原来的声明方式

int (^block)(int);
复制代码

现在的声明方式

block_t blk;
复制代码

通过使用typedefBlock类型的变量的定义变得更简单了。

捕获局部变量的值

int var = 10;
const char *fmt = "var = %d\n";
void(^block)(void) = ^(){
    printf(fmt, var);
};

var = 2;
fmt = "These value ware change. val=%d.";

block();
复制代码

上述代码运行后的结果。

var = 10
复制代码

执行的结果并不是修改之后的值,而是Block自动捕获的值。

Block实现

Block的实质

通过**“clang -rewrite-objc ”将含有Block语法的代码转换为C++代码。说是C++,其实也就是使用了struct而已,其本质是C**语言代码。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        void (^block)(void) = ^(){
            printf("block\n");
        };
        block();
    }
    return 0;
}
复制代码

接下来我们通过clang将这份代码转换为**C++**的形式。

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

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

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        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;
}
复制代码

接下来我们来逐步分析这段代码。

首先我们可以看到转换后的源代码也有着相同的表达式。

__main_block_func_0

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
		printf("block\n");
}
复制代码

如转换后的代码所示,使用Block使用的匿名函数,实际上被当做C语言中的简单函数处理。

该函数参数中的**__cself相当于OC实例方法中指向自身的实例变量self**,即参数**__cself为指向Block**值的变量。

struct __main_block_impl_0 *__cself
复制代码

__main_block_impl_0

OC中的self相同,参数**__cself__main_block_impl_0**结构体的指针。该结构体声明如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
};
复制代码

由于转换后的代码一并写入了构造函数(可以简单的认为是OC中的初始化方法)。在这里我们将其去掉,那么看起来将会十分简单。第一个成员变量为impl,我们看下impl的声明。

__block_impl

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
复制代码

__main_block_desc_0

第二个成员变量是Desc指针,以下为其**__main_block_desc_0**结构体的声明:

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)};
复制代码

__main_block_impl_0构造函数

接下来我们来看看**__main_block_impl_0**结构体的构造函数。

__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;
}
复制代码

当前这个构造函数接收参数:

  • fp:是由Block语法转换的C语言函数指针,也就是我们外界传入的**__main_block_impl_0C**语言函数。
  • desc:类型为**__main_block_desc_0**结构体的实例。

接下来我们来看看改构造函数的调用:

void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
复制代码

由于转化后的代码较长,所以我们去掉了一部分,具体如下:

struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *block = &tmp;
复制代码

这样就容易理解了,该源代码是将**__main_block_impl_0类型的局部变量,也就是栈上生成的__main_block_impl_0结构体实例的指针,赋值给__main_block_impl_0结构体指针类型的变量block**,以下为这部分源代码对应的最初源代码。

void (^block)(void) = ^(){
    printf("block\n");
};
复制代码

由此可以该源代码使用的block变量,是由**__main_block_impl_0**结构体实例的大小,来进行初始化的。

Block的调用

block();
复制代码

这部分代码被转换成了如下部分:

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
复制代码

去掉类型强转部分:

(*block->impl.FuncPtr)(block);
复制代码

这就是使用了函数指针来调用函数,由Block语法转换的**__main_block_func_0的函数的指针赋值给了成员变量impl的成员变量FuncPtr**。

__main_block_func_0的参数**__cself指向Block的值(也就是实例)。在函数调用的源代码中,我们可以看到Block**正是作为参数传递的。

_NSConcreteStackBlock

在我们的**__main_block_impl_0**的构造函数中我们看到这样一句代码。

impl.isa = &_NSConcreteStackBlock;
复制代码

看到这句代码时,是不是觉得和我们OC中类的结构很相似。所以说把Block作为OC对象处理时关于该类的信息都会被放置于**_NSConcreteStackBlock**中。

所以说Block其实就是OC对象。

自动捕获局部变量的值

构造函数探索

int var = 10;
const char *fmt = "var = %d\n";
void (^block)(void) = ^(){
    printf(fmt, var);
};
复制代码

和之前一样,我们将这段代码通过clang编译后来进行研究:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *fmt;
  int var;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _var, int flags=0) : fmt(_fmt), var(_var) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt; // bound by copy
  int var = __cself->var; // bound by copy
	printf(fmt, var);
}

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)};
复制代码

这与之前的代码稍有差异,我们注意到在Block语法表达式中使用的局部变量被作为成员变量追加到了**__main_block_impl_0**结构体中。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *fmt;
  int var;
};
复制代码

__main_block_impl_0结构体中声明的成员变量的类型和局部变量的类型完全相同。

注意:Block代码块中没有使用到的局部变量不会被自动追加。Block只捕获在其代码块中使用到的局部变量。

接下来我们来看看初始化该结构体实例的构造函数之间的差异:

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _var, int flags=0) : fmt(_fmt), var(_var)
复制代码

在初始化结构体实例的同时,根据传递给构造函数的参数对局部变量追加的成员变量进行初始化。

: fmt(_fmt), var(_var)
复制代码

这句代码就是对结构体中自动追加的成员变量的初始化。

匿名函数

接下来我们来看看使用Block的匿名函数的实现,最初的源代码的Block语法的实现如下:

^(){
    printf(fmt, var);
}
复制代码

该源代码被转换成了以下函数:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt; // bound by copy
  int var = __cself->var; // bound by copy
    printf(fmt, var);
}
复制代码

总的来说,所谓的自动捕获局部变量就是在执行Block时,Block代码块使用到的局部变量被保存到了Block的结构体实例(也就是Block自身)中。

__block修饰符

众所周知当我们的局部变量没有任何修饰符的时候,是不能在Block的代码块中被修改的,如果对局部变量执行了修改操作,那么它将会报出一个编译时错误。

Variable is not assignable (missing __block type specifier)
复制代码

解决这个问题有两种办法。第一种:使用下面几种类型的变量。

  • 静态变量
  • 静态全局变量
  • 全局变量

接下来我们来看看这段代码:

int global_var = 1;
static int static_global_var = 2;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        static int static_val = 3;
        void (^block)(void) = ^(){
            global_var += 1;
            static_global_var += 2;
            static_val += 3;
        };
            block();
    }
    return 0;
}
复制代码

该代码在Block代码块的内部改变了global_var、static_global_var、static_val三个变量的值。该源代码转换后的代码如下:

int global_var = 1;
static int static_global_var = 2;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *static_val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *static_val = __cself->static_val; // bound by copy
    global_var += 1;
    static_global_var += 2;
    (*static_val) += 3;
}
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)};
复制代码

对静态全局变量和全局变量的访问与转换前相同,那么局部静态变量又要如何转换呢?

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int *static_val = __cself->static_val; // bound by copy

    global_var += 1;
    static_global_var += 2;
    (*static_val) += 3;
}
复制代码

使用静态变量static_val的指针对其进行访问,将静态变量static_val的指针传递给**__main_block_impl_0**结构体的构造函数并保存。这是超出作用域使用变量最简单的办法。

第二种方法是使用**__block**:

接下来我们就使用**__block**来修饰我们的局部变量:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        __block int var = 10;
        void (^block)(void) = ^(){
            var = 1;
        };
        block();
    }
    return 0;
}
复制代码

该源代码转换后的代码如下:

struct __Block_byref_var_0 {
  void *__isa;
__Block_byref_var_0 *__forwarding;
 int __flags;
 int __size;
 int var;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_var_0 *var; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_var_0 *_var, int flags=0) : var(_var->__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_var_0 *var = __cself->var; // bound by ref
    (var->__forwarding->var) = 1;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->var, (void*)src->var, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->var, 8/*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};
复制代码

只是增加了**__block**代码量就急剧增加,我们来看看它是如何转换的。

__block int var = 10;
复制代码

转换后的代码:

__Block_byref_var_0 var = {
    0,
    &var,
    0,
    sizeof(__Block_byref_var_0),
    10
};
复制代码

我们发现被**__block**修饰的变量被转换成了结构体实例,这个实例存储在栈区。

__Block_byref_var_0结构体声明如下:

struct __Block_byref_var_0 {
  void *__isa;
__Block_byref_var_0 *__forwarding;
 int __flags;
 int __size;
 int var;
};
复制代码

该结构体的最后一个成员变量var就相当于原来的局部变量var

那么下面这段个**__block**修饰的变量赋值时的情况怎么样呢?

^(){
		var = 1;
};
复制代码

该源代码的转换如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_var_0 *var = __cself->var; // bound by ref
    (var->__forwarding->var) = 1;
}
复制代码

__Block_byref_var_0结构体实例的成员变量**__forwarding持有指向该实例自身的指针,通过成员变量__forwarding访问成员变量var**。(成员变量var是该实例自身持有的变量,相当于原来的局部变量var)。

如下图所示:

总结

  • Block其实就是OC对象。
  • Block会自动捕获局部变量,全局变量不会捕获。
  • 如果要在Block的代码块里修改变量的值,有两种方案:
    • 使用全局静态变量、静态变量、全局变量。
    • 使用__block修饰符来修饰局部变量。


这篇关于iOS 原理探索-Block(一)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程