开发必备-Block

361 阅读9分钟

简介

什么是Block?

Block是将函数及其执行上下文封装起来的对象。iOS4之后推出。C语言的扩展

void(^blockName)(参数列表) = ^(参数列表) {执行内容};

Block的类型

三种类型

全局block

不使用外部变量的block 或者 用到静态/全局变量。

_NSConcreteGlobalBlock,存储在数据区.data。

NSLog(@"%@",[^{
    NSLog(@"globalBlock");
} class]);

控制台输出__NSGlobalBlock__

栈block

使用了外部变量,但是没有进行过copy操作的block。

_NSConcreteStackBlock,存储在栈区。

NSInteger num = 10;
NSLog(@"%@",[^{
    NSLog(@"stackBlock:%zd",num);
} class]);

控制台输出__NSStackBlock__

堆block

对栈中的block进行过copy操作后,就是堆block。

_NSConcreteMallocBlock,存储在堆区。

怎样算copy操作?根据《Objective-C高级编程》,有以下4种情况。 1.调用copy 2.Block是函数的返回值 3.Block被强引用,Block被赋值给__strong或者id类型 4.方法名中含有usingBlock的Cocoa框架方法或GCD的API入参中含有usingBlcok的方法

上面的情况,系统会默认调用copy操作。

NSInteger num = 10;
void (^mallocBlock)(void) = ^{
    NSLog(@"stackBlock:%zd",num);
};
NSLog(@"%@",[mallocBlock class]);

控制台输出__NSMallocBlock__

但是对全局block进行copy,仍是全局block。

void (^globalBlock)(void) = ^{
    NSLog(@"globalBlock"); 
};
NSLog(@"%@",[globalBlock class]);

控制台输出__NSGlobalBlock__

对栈blockcopy之后,并不代表着栈block就消失了,左边的mallock是堆block,右边被copy的仍是栈block。

[self testWithBlock:^{
    NSLog(@"%@",self);
}];
- (void)testWithBlock:(dispatch_block_t)block
{
    block();
    dispatch_block_t tempBlock = block;
    NSLog(@"%@,%@",[block class],[tempBlock class]);
}

控制台输出__NSStackBlock__,__NSMallocBlock__

返回block的场景。

NSLog(@"%@",[[self testReturnBlock] class]);
- (dispatch_block_t)testReturnBlock {
    NSInteger testNum = 5;
    return ^{NSLog(@"returnBlock%@",@(testNum));};
}

控制台输出为__NSMallocBlock__

对栈Block进行copy,将会copy到堆区。

对堆Block进行copy,将会增加引用计数。

对全局Block进行copy,因为是已经初始化的,所以什么也不做。

Block的实现

block实际上是作为c语言源代码来处理的。

使用clang -rewrite-objc **.m转换为c++源代码(本质还是c语言源代码)

Block的结构

void(^blk)(void)=^{printf("aaa");};
blk();

转换后。

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("aaa");}

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[]) {

    void(*blk)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

开始分析。将

void(*blk)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

去掉类型转换后,得到

void(*blk)(void)=&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

通过构造函数,初始化blk,构造函数的入参为__main_block_func_0__main_block_desc_0_DATA

其中__main_block_func_0赋值给impl,这是一个__block_impl类型的结构体。保存了函数指针,isa等信息。查看__main_block_func_0,可以看到内容为block的执行内容。

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *           ;
};

__main_block_desc_0_DATA则赋值给Desc,这是一个__main_block_desc_0结构体,里面存储了保留字段以及blokc的size。

我们再看下block的调用。

((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

变换为

(*)blk->FuncPtr(blk)

使用函数指针调用函数。

理解了简单场景,下面开始增加难度,block的变量截取。

Block的变量截取

局部变量

NSInteger num = 3;
NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
    return n*num;
};
num = 1;
NSLog(@"%ld",block(2));

控制台输出为6,说明block中捕获的num值为3。我们转化看下。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSInteger num;  //【多了一个成员变量num】
  //【通过构造方法,捕获到num,具体值是多少,看main中block的初始化部分】
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger _num, int flags=0) : num(_num) { 
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static NSInteger __main_block_func_0(struct __main_block_impl_0 *__cself, NSInteger n) {
    //【通过__cself,找到num】
    NSInteger num = __cself->num; // bound by copy
    return n*num;
}

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[]) {

    NSInteger num = 3;
    NSInteger(*block)(NSInteger) = ((NSInteger (*)(NSInteger))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, num));
    num = 1;
    ((NSInteger (*)(__block_impl *, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 2);
    return 0;
}

block的初始化,传入num的值,此时num的值为3,所以__main_block_impl_0num的值为3。

调用block时,__main_block_func_0通过__cself,找到的num为3。

所以最终打印的为6。

如果我们在block内部,修改num的值呢?

此时编译器会报错,提示我们Variable is not assignable(****)。因为block捕获的仅仅是值,没有办法通过内部的修改,从而修改外部的值,对内部捕获到的值,只能是只读操作。

如果希望修改呢?

改为静态变量全局变量或使用__block,接下来会介绍。

静态局部变量

static  NSInteger num = 3;
NSInteger(^myBlock)(NSInteger) = ^NSInteger(NSInteger n){
    return n*num;
};
num = 1;
NSLog(@"%ld",myBlock(2));

控制台打印2。查看转换后的源码。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSInteger *num;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger *_num, int flags=0) : num(_num) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static NSInteger __main_block_func_0(struct __main_block_impl_0 *__cself, NSInteger n) {
  NSInteger *num = __cself->num; // bound by copy
  return n*(*num);
}

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[]) {
    static NSInteger num = 3;
    NSInteger(*myBlock)(NSInteger) = ((NSInteger (*)(NSInteger))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &num));
    num = 1;
    ((NSInteger (*)(__block_impl *, NSInteger))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 2);
    return 0;
}

发现__main_block_impl_0中捕获的是静态变量的指针。在构造函数中,传递的入参为&num。在block中执行取值时,取到变量的指针,然后通过*取值。所以,后面修改num的值,会直接影响到block。

超出作用域使用变量。由Block语法生成的值Block,可以存有超过其变量作用域的被截获对象的自动变量。变量作用域结束的同时,原来的自动变量被废弃。这时将不能通过指针访问原来的自动变量。可以采用__block。后文介绍。

全局变量/静态全局变量

int num3 = 1;
static int num4 = 2;
int main(int argc, const char * argv[]) {
    void(^myBlock)(void) = ^(){
        printf("%d",num3);
        printf("%d",num4);
    };
    myBlock();
    return 0;
}

转换后查看。

int num3 = 1;
static int num4 = 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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("%d",num3);
    printf("%d",num4);
}

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[]) {
    void(*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    return 0;
}

可以看到,作用域广,直接使用值。不存在转换。

__block

int main(int argc, const char * argv[]) {
    
    __block int val = 10;
    void(^blk)(void) = ^{
        val = 5;
    };
    return 0;
}

转换后。

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_val_0 *val = __cself->val; // bound by ref
    (val->__forwarding->val) = 5;
}

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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(int argc, const char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
    void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
    return 0;
}

发现,main中的val,被转换成了结构体__Block_byref_val_0,初始值为10。__forwarding的值是自己,也就是说,__forwarding指向了自己。再看下__main_block_func_0,里面的取值方式。如下图所示。

现在我们知道了,为什么__block可以在block内部修改值,但是,为什么要有__forwarding?出现了copydispose方法?

这一切都是因为变量作用域的问题。

设置在栈上的block,会随着其所属的变量作用域的结束而结束,同时__block变量也会被废弃。

Blocks提供一种将栈上block复制到堆上block的方法来解决这个问题。即使block所属的变量作用域结束,堆上的block还可以继续存在,__block变量也不会受影响。

此时,__forwarding的作用体现出来了。

有时__block变量是配置在堆上,我们想访问栈上的__block变量。只要栈上的__forwarding指向的是堆上的,就能正确访问到。无论__block变量是在栈还是在堆,都可以通过__forwarding访问到正确的__block变量

什么时候复制到堆上? 之前在block类型中指出了4种场景,那4种场景系统已经帮我们调用了copy,什么情况下我们需要手动呢?

向 方法或函数 的参数中 传递 block的时候。

堆上的block----copy,仅仅是引用计数的增加

借图。

截获对象

上面的例子中,截获的都是数值。如果是对象呢?

NSMutableArray *arr = [NSMutableArray arrayWithObjects:@"1",@"2", nil];
void(^block)(void) = ^{
    NSLog(@"%@",arr);
    [arr addObject:@"4"];
};
[arr addObject:@"3"];
block();

代码正常执行,1,2,3。转化一下。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSMutableArray *arr;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_arr, int flags=0) : arr(_arr) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSMutableArray *arr = __cself->arr; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_ld_6b0zns190957flvr5h_6vgwc0000gn_T_block_97ed65_i_2,arr);
    ((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)arr, sel_registerName("addObject:"), (id _Nonnull)(NSString *)&__NSConstantStringImpl__var_folders_ld_6b0zns190957flvr5h_6vgwc0000gn_T_block_97ed65_i_3);
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->arr, (void*)src->arr, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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

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(int argc, const char * argv[]) {
    NSMutableArray *arr = ((NSMutableArray * _Nonnull (*)(id, SEL, ObjectType _Nonnull, ...))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("arrayWithObjects:"), (id _Nonnull)(NSString *)&__NSConstantStringImpl__var_folders_ld_6b0zns190957flvr5h_6vgwc0000gn_T_block_97ed65_i_0, (NSString *)&__NSConstantStringImpl__var_folders_ld_6b0zns190957flvr5h_6vgwc0000gn_T_block_97ed65_i_1, __null);
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, arr, 570425344));
    ((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)arr, sel_registerName("addObject:"), (id _Nonnull)(NSString *)&__NSConstantStringImpl__var_folders_ld_6b0zns190957flvr5h_6vgwc0000gn_T_block_97ed65_i_4);
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

发现此处有 copydispose方法。为什么?涉及到内存管理。

首先,arr是个对象,默认为strong类型,C语言的机构体中是不能含有__strong修饰符的变量,编译器是不清楚何时对结构体进行初始化和废弃,不能很好的管理内存。

于是,oc的runtime机制出场了(强大的runtime),它能够准确把握栈block复制到堆上的时机,为此需要增加__main_block_impl_0成员变量的__main_block_copy_0__main_block_dispose_0

如果我们对对象增加__block修饰符呢?

struct __Block_byref_arr_0 {
  void *__isa;
__Block_byref_arr_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSMutableArray *arr;
};

循环引用

self持有blockblock内部使用到self

解决方案weak以下self。

有时我们会看到在block内部会strong一下,为什么呢?

weak的原理我们知道,一个对象的释放,会引起指向它的弱引用指针置位nil。

如果我们在block内部执行一段延迟方法。block调用完成后结束,作用域结束,对象释放,对象的释放,引起weakself的释放。在延迟方法中再使用weakself,此时是nil。所以,我们会在block外部weak一下,block内部strong一下,防止提前释放。

总结

block只会捕获到它内部使用到的变量,使用不到的变量,不会捕获。

想要修改捕获到的值,可以通过两种方式,一种是改变作用域,一种是通过传递内存指针。

注意循环引用的问题。

为什么会有__block? 是为了可以在block中修改外部变量值。原理是通过__block,将变量转换为结构体,通过指针的方式。

在ARC下,我们经常见到的是堆block,是因为我们通过=将block赋值给了blk变量,而blk默认是strong修饰符。