每日一问01——block

2017-08-24  本文已影响31人  巫师学徒

小白篇——基本用法

block声明——格式

返回类型(^名字)(参数列表)

void(^blockName)(int a,bool b,NSString *c)
block表达式——格式

^返回类型(参数列表)

^(int)(int a)
block作变量
void (^block)(int a) = ^(int b) {
        NSLog(@"%d---变量",b);
    };
 block(123);

执行完block(123)后打印123---变量

block作函数参数
//参数block的声明
- (void)function:(int(^)(NSString *str))paramBlock {
    int i = paramBlock(@"function");
    NSLog(@"%d",i);
}
//调用
[self function:^int(NSString *str) {
        NSLog(@"%@---参数",str);
        return 123;
    }];

打印顺序:function---参数,123

使用typedef声明block
typedef int(^typeBlock)(int c);
//函数声明
- (void)function2:(typeBlock)paramBlock {
    int i = paramBlock(1);
    NSLog(@"%d",i);
}
//函数调用
[self function2:^int(int c) {
        NSLog(@"%d---typedef参数",c);
        return c++;
    }];

打印顺序 1---typedef参数,2

block作为函数返回值
//函数声明
(int(^)(int a))function3 {
    return ^(int count) {
            NSLog(@"%d",i);
            return count = 0;
        };
}
//调用
int(^retBlock)(int a) = [self function3];
//用作fuc2的参数
 [self function2:retBlock];

打印顺序 1,0

block与外部变量

在使用block中很重要的一点就是可以通过block获取上文。即block的外部变量可以放在block内部使用。

临时变量
    int num = 10;
    void(^block1)() = ^() {
        NSLog(@"%d",num);
    };
    num = 20;
    block1();

打印结果为10而不是20。从这里我们可以看出block在声明时是将num拷贝了一份到block内。外面num无论怎么变化也不会影响到内部。

全局变量和__block
__block int num = 10;
void(^block1)() = ^() {
     NSLog(@"%d",num);
};
num = 20;
block1();

int num;
- (void)function {
    num = 10;
    void(^block)() = ^() {
        NSLog(@"%d",num);
    };
    num = 20;
    block();
}

打印结果均为20。那么为什么全局变量拷贝到block内依然可以修改呢?

我们可以猜想,局部变量是存储在栈上,而全局变量是存储在堆上。block拷贝变量是将num拷贝到堆上。所以对于本来就在堆上的num,block可以直接使用。

先不说对不对,好像这个猜想挺靠谱的。和我们经常听到的
栈上的block会自动copy到堆上
block截取的_block变量也会同时copy到堆上
说法貌似是一致的。

深入了解block

一个最简单的block

typedef int (^Block)(void);
int main(int argc, char * argv[]) {
    // block实现
    Block block = ^{
        return 0;
    };
    // block调用
    block();
    return 0;
}

使用clang工具翻译代码后,这个是整体的,拆分在下面

// block内部结构体
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
typedef int (*Block)(void);
// 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;
  }
};
// block方法实现
static int __main_block_func_0(struct __main_block_impl_0 *__cself) {
        return 0;
 }
// block内部结构体
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函数
int main(int argc, char * argv[]) {
    // block实现
    Block block = ((int (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    // block调用
    ((int (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

block内部结构

impl结构体
struct __block_impl {
  void *isa;  // 存储位置,_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock
  int Flags;  // 按位承载 block 的附加信息
  int Reserved;  // 保留变量
  void *FuncPtr;  // 函数指针,指向 Block 要执行的函数,即__main_block_func_0
};

1.isa 指针,所有对象都有该指针,用于实现对象相关的功能。
这里block有3种类型。具体有什么不同下面再说。

impl.isa = &_NSConcreteStackBlock 全局的静态 block,不会访问任何外部变量。; 
impl.isa = &_NSConcreteMallocBlock 保存在栈中的 block,当函数返回时会被销毁; 
impl.isa = &_NSConcreteGlobalBlock 保存在堆中的 block,当引用计数为 0 时会被销毁;

2.flags,用于按 bit 位表示一些 block 的附加信息
3.reserved,保留变量。
4. funcPtr,函数指针,指向具体的 block 实现的函数调用地址。

block结构体
struct __main_block_impl_0 {
  // impl结构体,即block的主结构体
  struct __block_impl impl;
  // Desc结构体,即block的描述结构体
  struct __main_block_desc_0* Desc;
  // block结构体的构造函数
  __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;
  }
};

impl :block 实现的结构体变量,该结构体就是前面说的那个;
desc : 描述 block 的结构体变量,主要是 size 大小,以及 copy 和 dispose 函数的指针
__main_block_impl_0 :结构体的构造函数,初始化结构体变量 impl、Desc;
从这里就看出block至少是将变量,函数地址通过构造函数都保存到了结构体内部了。

block执行流程.png

回头再看block截获变量

我们截获变量的类型基本是以下几种
1.全局变量 2.全局静态变量 3.局部静态变量 4.局部变量 5._block修饰的变量
其中1.2属于一类,作用域是全局,block可以直接使用它们。而第3.4种的作用域在block外面。block想使用它就需要进行拷贝。然后通过clang工具翻译后。

typedef int (^Block)(void);
int a = 0;
static int b = 0;
int main(int argc, char * argv[]) {
    static int c = 0;
    int i = 0;
    NSMutableArray *arr = [NSMutableArray array];
    Block block = ^{
        a = 1;
        b = 1;
        c = 1;
        [arr addObject:@"1"];
        return i;
    };
    block();
    return 0;
}
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *c;
  NSMutableArray *arr;
  int i;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_c, NSMutableArray *_arr, int _i, int flags=0) : c(_c), arr(_arr), i(_i) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
// block实现的方法
static int __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *c = __cself->c; // bound by copy
  NSMutableArray *arr = __cself->arr; // bound by copy
  int i = __cself->i; // bound by copy
  a = 1;
  b = 1;
  (*c) = 1;
  ((void (*)(id, SEL, ObjectType))(void *)objc_msgSend)((id)arr, sel_registerName("addObject:"), (id)(NSString *)&__NSConstantStringImpl__var_folders_k__6b9p9yt96y9dq8ds8_kvf3kh0000gn_T_main_3c9752_mi_0);
  return i;
}

我们发现静态变量传入的是指针。可以直接修改。
而局部变量拷贝进去是const类型的,无法被修改。
于是如果我们希望在block内部改变外部变量就需要用到__block这个关键字。

__block

block源码

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

clang后

struct __block_impl
{
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};
struct __Block_byref_intValue_0
{
    void *__isa;
    __Block_byref_intValue_0 *__forwarding;
    int __flags;
    int __size;
    int intValue;
};
struct __main_block_impl_0
{
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_intValue_0 *intValue; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_intValue_0 *_intValue, int flags=0) : intValue(_intValue->__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_intValue_0 *intValue = __cself->intValue; // bound by ref
    (intValue->__forwarding->intValue) = 1;
}
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src)
{
    _Block_object_assign((void*)&dst->intValue, (void*)src->intValue, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0 *src)
{
    _Block_object_dispose((void*)src->intValue, 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
                             };
int main()
{
    __attribute__((__blocks__(byref))) __Block_byref_intValue_0 \
    intValue = 
    {
        (void*)0,
        (__Block_byref_intValue_0 *)&intValue, 
        0, 
        sizeof(__Block_byref_intValue_0), 
        0
    };
    void (*blk)(void) = (void (*)()) &__main_block_impl_0   \
                (
                    (void *)__main_block_func_0,            \
                    &__main_block_desc_0_DATA,              \
                    (__Block_byref_intValue_0 *)&intValue,  \
                    570425344                               \
                );
    return 0;
}

当block截获的变量为__block 修饰的变量时,多发生了下面几件事。
__Block_byref_intValue_0 结构体:用于封装 __block 修饰的外部变量。 _Block_object_assign 函数:当 block 从栈拷贝到堆时,调用此函数。 _Block_object_dispose 函数:当 block 从堆内存释放时,调用此函数。
__block修饰的变量被拷贝到了堆上并且变成通过指针传入block内部的。
然后就是block结构体中多出了一个_forwarding指针。并且_forwarding指针指向外部变量自己。
于是block结构体中的__Block_byref_intValue_0 *intValue和变量结构体中的__Block_byref_intValue_0 *__forwarding都指向了外部变量。
这特么是不是冲突了????为了说明这个有啥用,现在开始讲block的内存管理。

block内存管理

吐槽##学到这真的好累啊。。。平时用惯了的block背后还有那么多事儿。不查不知道,一查吓一跳

在前面,提到过了block的类型有三种。NSConcreteGlobalBlock_NSConcreteStackBlock_NSConcreteMallocBlock这三种block在内存中的分布如下:

block内存分布.png
_NSConcreteGlobalBlock

1、当 block 字面量写在全局作用域时,即为 global block;
2、当 block 字面量不获取任何外部变量时,即为 global block
除了这两种方式以外,其他形式创建的block均为stack block

创建一个简单的_NSConcreteGlobalBlock
int main()
{
    ^{ printf("Hello, World!\n"); } ();
    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;
    }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("Hello, World!\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()
{
    (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA) ();
    return 0;
}

由于这个block没有引入外部变量,所以它的类型为_NSConcreteGlobalBlock。

但clang后显示为_NSConcreteStackBlock,这里查资料原因是clang改写的实现方式和LLVM不一样,在LLVM中,开启ARC时,block应该是_NSConcreteGlobalBlock

_NSConcreteStackBlock

_NSConcreteStackBlock 类型的 block 处于内存的栈区,如果其变量作用域结束,这个 block 就被废弃,block 上的 __block 变量也同样会被废弃。
所以,为了解决这个问题,block提供了copy功能,将 block 和 __block 变量从栈拷贝到堆。此时block类型变为_NSConcreteMallocBlock

从栈拷贝到堆上

此时impl.isa = &_NSConcreteMallocBlock;
再看这个构造函数!

__block修饰变量的构造函数

block访问的永远是__forwarding指向的那块内存。所以进行 copy 操作时,会将栈上的 __forwarding 指针指向了堆上的 block 结构体实例

小结:当block执行了copy以后,block的类型变为NSConcreteMallocBlock
参考代码:

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        int i = 1024;
        void (^block1)(void) = ^{
            printf("%d\n", i);
        };
        block1();
        NSLog(@"%@", block1);
    }
    return 0;
}
会输出 <__NSMallocBlock__: 0x100109960>
block的自动拷贝与手动拷贝

在ARC情况下,基本都会讲创建在栈上的block拷贝到堆上。除了以下这种情况:
block 作为方法或函数的参数传递时,编译器不会自动调用 copy 方法;
例子如下:(来自狮子书)

- (id)getBlockArray
{
    int val = 10;
    return [[NSArray alloc] initWithObjects: 
                            ^{NSLog(@"blk0:%d", val);}, 
                            ^{NSLog(@"blk1:%d", val);}, nil];
}

调用时

id arr = getBlockArray();
Block block = [arr objectAtIndex:1];//此处crash
block();

crash的原因就是block出作用域后已经被回收了,正好验证了作为参数时不进行自动拷贝的说法。

参考的文章

谈Objective-C block的实现
block没那么难(一)
block没那么难(二)
iOS block,你要看的这都有
感谢以上优秀文章,本文很多地方都是摘录自其中,但重要的还是得要自己理解。

上一篇下一篇

猜你喜欢

热点阅读