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)。

如下图所示:


image

总结

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

推荐阅读更多精彩内容