Block学习-关于Block是如何实现的,以及block中参数传递

原创 2015年11月21日 12:18:46

先看下面的一段代码,判断输出结果:
int a = 0;  
void (^block)(void) = ^{printf("a = %i\n",a);};
a=10;
block();
经过运行之后我们发现,输出结果为“a = 0”,而不是"a = 10"。

如果感觉答案是“a = 10”的同学看下面的代码或许会有所感觉,明显会报错,提示“Use of undeclared identifier 'a' ”。

void (^block)(void) = ^(){printf("a = %i\n",a);};
int a=10;
block();

我们发现block块在初始化的时候就要获取局部变量(局部变量)a的值,之后a的值的变化并不会影响block的运行结果。

*所说的局部变量为block外的局部变量,而非block内定义的。

Block结构:

到底block是如何实现的呢?下面是一个最简单的block的定义与使用

int main(int argc, const char * argv[]) {
    void (^block)(void) = ^{printf("This is a block!");};
    block();
}
通过终端,我们找到main.m文件所在路径,然后输入

clang -rewrite-objc main.m
(通过clang编译器,我们可以获得在编译过程中生产的中间代码,看block时如何实现的)

在当前目录下找到main.cpp文件,打开后能看到block生成了很多结构体如下:(简化后代码,可查找关键字),

struct __block_impl {
    void *isa;//block存放位置,取值为_NSConcretGlobalBlock(全局区)、_NSConcretStackBlock(栈区)、_NSConcretMallocBlock(堆区)
    int Flags;//用于按bit位表示一些block的附加信息,block copy的实现代码可以看到对该变量的使用。
    int Reserved;//保留变量。
    void *FuncPtr;
};

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) {
    printf("This is a block!");
}

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

int main(int argc, const char * argv[]) {
    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);
}

看上去很多,其实没那么难理解,一个一个来分别介绍他们的意思:

__block_impl:block的一些基础属性,像是block的基类。

__main_block_desc_0:block的描述,他有一个实例__main_block_desc_0_DATA 

__main_block_impl_0:block变量(包含上面两个结构体对象)。

__main_block_func_0:block的匿名函数。存放block内的语句。

命名规则:“main”为block所在函数名,如果定义在函数外为block的名字,末尾的“0”表示为当前函数中第“0”个block。

在末尾的main函数中,我们可以看到block的初始化是调用__main_block_impl_0的构造函数,所以block的内容简化后为:

struct __main_block_impl_0 {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
    size_t reserved;
    size_t Block_size;
    __main_block_impl_0(#block匿名函数的指针#,#__main_block_desc_0的实例指针#, int flags=0);
};
由此可见:block其实就是一个Object-c对象。

局部变量:

通过clang -rewrite-objc 指令编译本文开头的那段代码我们找出与之前的编译结果的不同:

struct __main_block_impl_0 {
    struct __block_impl impl;       
    struct __main_block_desc_0* Desc;
    int a; 
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
//block的匿名函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself){
    int a = __cself->a; // bound by copy
    printf("a = %i\n",a);
}
//主方法
int main(int argc, const char * argv[]) {
    int a = 0;
    //block初始化
    void (*block)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a);
    a=10;
    //block调用
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}</span>

比较后发现:

__main_block_impl_0的属性中多了一个变量a;

__main_block_impl_0初始化方法的参数中也有接收变量对a赋值的方法。

__main_block_func_0而在匿名函数中多了一行“int a = __cself->a;”

       局部变量的传递的实现方法,当block在初始化的时候,通过构造函数把a的值通过值传递传入block,而在__main_block_func_0方法中,通过int a创建变量来接收。最后在匿名函数中使用时又创建临时变量类获得a。所以在block执行时,输出的a为block初始化时获得的a的值,使用的是a的副本。

       这也能说明为什么,block中不让修该局部变量。首先在block中修改局部变量的值本是没问题的,但是修改的只是和局部变量名字一样的副本变量的值,这并不会影响局部变量本来的值。为了不让开发者迷惑,所以就不让赋值。

那么如果获取的局部变量是OC对象呢

int main(int argc, const char * argv[]) {
    NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:0];
    void (^block)(void) = ^{
        [array addObject:@"block"];
    };
    block();
}
这么写是能运行的,因为array是一个指针,我们并没有改变指针的值。

static修饰符与全局变量

        全局变量或静态变量都在全局区,在内存中的地址是固定的,Block在初始化时读取的是该变量值地址,当调用的时候是直接从其所在内存读出,获取到的是最新值,而不是在定义时copy的常量。而且可以在block中修改他们的值。

int a=0;
int main(int argc, const char * argv[]) {
    static int b=0;
    void (^block)(void) = ^{
        NSLog(@"a=%i,b=%i",++a,++b);
    };
    a = 10;
    b = 10;
    block();
}
运行的结果为(不是“a=1,b=1”):
a=11,b=11

__block修饰符

        之前了解了block中用到局部变量时,使用的是局部变量的副本,当我们要想局部变量能在block中进行修改时,我们通常会在局部变量前写上__block修饰符,那么它又是如何实现的呢。对如下代码进行编译:

int main(int argc, const char * argv[]) {
    __block int a=0;
    void (^block)(void) = ^{printf("a = %i",++a);};
    block();
}
编译后的代码简化为(只列出有变化的部分)

struct __Block_byref_a_0 {
    void *__isa;
    __Block_byref_a_0 *__forwarding;
    int __flags;
    int __size;
    int a;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_a_0 *a; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__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_a_0 *a = __cself->a; // bound by ref
    printf("a = %i",++(a->__forwarding->a));}

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

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

int main(int argc, const char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 0};
    void (*block)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344);
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
       我们发现多了很多东西,看上去很乱,我们一个一个来,首先我们看主函数main里第一行,由__block int a=0;编译而来。当我们为局部变量加上__block修饰符时,编译器会自动创建一个关于这个变量的结构体__Block_byref_a_0,而变量的初始化,变为结构体的初始化,将局部变量对象化了。

       在关于变量a的结构体__Block_byref_a_0中,有“int a;”用于存放变量值,有“__Block_byref_a_0 *__forwarding;”一个关于自己的指针,以及其他一些属性。而block初始化时,将对象化后的a作为了构造函数的参数。由之前分析我们已经得知如果block获取的局部变量是OC对象,是可以对它进行操作的。

   为什么不直接创建变量a的指针

       到这里应该理解了__block的实际工作,但是肯定会有人疑惑,说到底,将a对象化的目的只是因为对象传递得时候传的是指针,那么为什么不直接创建一个指针指向变量a,而要将他对象化。说到这里就要先说说内存中栈和堆,函数内局部变量的初始化是分配到栈中,而对象的初始化是分配在堆中(ios中alloc都是存放在堆中),变量a是分配在栈中,如果创建一栈变量的指针很危险,栈相对于堆而言是编译器自动释放,当函数结束时,栈就会被释放,而block对象通常分配在堆上,还没有被释放,而这时候原来的指针就会变成野指针,对block而言是不安全的,

block的内存分配
  初始 copy后
arc  堆       堆 
非arc   栈     堆   

   block对局部变量与对象的区分

     在之前的编译结果中,还多出了两个方法:

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

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


      block会持有捕获的对象,包括局部变量(加了__block修饰符的)和oc对象,编译器通过BLOCK_FIELD_IS_BYREF和BLOCK_FIELD_IS_OBJECT来区分。

      上面两个方法的最后一个参数都为8,表示的是BLOCK_FIELD_IS_BYREF。分别为局部变量的copy方法与释放方法。


OC坑集: block作为函数参数

对于 OC 的争论一直不休, 对于一门语言,  我觉得只要能解决你的问题就够了. 评论一门编程语言之前, 你需要真正的去用它,  OC 孰好孰坏, 需要你自己去尝试. OC 坑集, 主要记载使用 OC...
  • AndroidBluetooth
  • AndroidBluetooth
  • 2015年06月06日 11:02
  • 7067

IOS 代码块之block的声明、创建、传参的基本使用

原因是系统证书WWDR在2016年2月14日失效,需要更新WWDR系统证书 证书下载地址https://developer.apple.com/certificationauthority/Apple...
  • LXL_815520
  • LXL_815520
  • 2016年02月15日 16:19
  • 1032

OC坑集: block作为函数参数

对于 OC 的争论一直不休, 对于一门语言,  我觉得只要能解决你的问题就够了. 评论一门编程语言之前, 你需要真正的去用它,  OC 孰好孰坏, 需要你自己去尝试. OC 坑集, 主要记载使用 OC...
  • AndroidBluetooth
  • AndroidBluetooth
  • 2015年06月06日 11:02
  • 7067

Objective-c - block作为函数的参数

/* Objective-c - block作为函数的参数       1> block做为一种数据类型,可以做为方法或者函数的参数.       2> 将b...
  • xuezhangjun0121
  • xuezhangjun0121
  • 2017年01月11日 21:18
  • 510

iOS Block的基本使用以及Block传值

block为我们提供了一个非常便捷的方法去实现各种传值以及回调 合理的使用block可以减少代码量以及更加优雅的实现功能 现做个小整理如下: #pragma mark About Block ...
  • Scott_cc
  • Scott_cc
  • 2016年04月14日 15:52
  • 3407

编写带有block参数的函数

目前在编写IOS程序的过程中,对于block的使用比较多。一般都是使用iOS写好的API自带的block,但是自己也想写一些block作为参数的函数。如果是在同一个类中,可以异步编写一些代码。如果是在...
  • dfman1978
  • dfman1978
  • 2016年08月10日 15:01
  • 2585

iOS编程——OC viewController 传值常用方法Delegate和Block的使用

写了个例子,简单介绍了下Delegate和Block的使用,注意点都在注释里,直接上代码了: 1.FirstViewController.h: #import @interface First...
  • wc455287693
  • wc455287693
  • 2015年08月24日 13:05
  • 1581

OC坑集: block作为函数参数

对于 OC 的争论一直不休, 对于一门语言,  我觉得只要能解决你的问题就够了. 评论一门编程语言之前, 你需要真正的去用它,  OC 孰好孰坏, 需要你自己去尝试. OC 坑集, 主要记...
  • intheair100
  • intheair100
  • 2015年10月30日 17:51
  • 1375

Aspects源码解读:动态Block调用(不定参数的Block)

Aspects是iOS开发中比较著名的AOP开源库,体积小巧而功能强大,其使用的时候有个地方比较有意思:usingBlock参数居然是可以写不定个数个参数的,换句话说,可以调用一个不定参数的Block...
  • u010124617
  • u010124617
  • 2015年10月13日 17:53
  • 2495

iOS-OC-iOS传值大全(代理传值,block 传值,单例传值,通知传值,属性传值)

1、代理传值 有A \B 两个页面需要将B页面的值传个A 总结: 1.首先应该在b页面写一个协议,协议里有带参数的方法,并创建实现该协...
  • u014220518
  • u014220518
  • 2016年03月21日 11:52
  • 805
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Block学习-关于Block是如何实现的,以及block中参数传递
举报原因:
原因补充:

(最多只允许输入30个字)