vlambda博客
学习文章列表

浅谈Objective-C中的block那些事

block是什么

block对象可以理解为一个标准的C语言匿名函数,同时又具备运行时的特性。通过看它的源码可以理解为什么官方文档称它为对象。

/* Revised new layout. */
struct Block_descriptor {
   unsigned long int reserved;
   unsigned long int size;
   void (*copy)(void *dst, void *src);
   void (*dispose)(void *);
};

struct Block_layout {
   void *isa;
   int flags;
   int reserved;
   void (*invoke)(void *, ...);
   struct Block_descriptor *descriptor;
   /* Imported variables. */
};

其中isa有六种类型:

/* the raw data space for runtime classes for blocks */
/* class+meta used for stack, malloc, and collectable based blocks */
BLOCK_EXPORT void * _NSConcreteStackBlock[32];
BLOCK_EXPORT void * _NSConcreteMallocBlock[32];
BLOCK_EXPORT void * _NSConcreteAutoBlock[32];
BLOCK_EXPORT void * _NSConcreteFinalizingBlock[32];
BLOCK_EXPORT void * _NSConcreteGlobalBlock[32];
BLOCK_EXPORT void * _NSConcreteWeakBlockVariable[32];

一般情况下,我们只需要考虑其中三种即可:

  1. _NSConcreteStackBlock(ARC环境下会替换成_NSConcreteMallocBlock)

  2. _NSConcreteMallocBlock

  3. _NSConcreteGlobalBlock

block是怎么捕获变量的

这里关心的主要是静态变量和局部变量。(静态)全局变量因为它本身的作用域,所以一般不用特殊考虑。

  1. block只会捕获它需要的变量,不需要的它不会捕获。

  2. 静态变量捕获的是变量的指针,所以可以修改原值。

  3. 局部变量捕获到数据结构,所以不可以修改原值。

  4. block捕获Objective-C对象时,可以修改该对象内部值(dto这时候理解是一个指针)。

  5. block捕获Ojbective-C对象时,不可以修改该对象本身(dto这时候理解为指针本身的值,也就是value)。具体可以参考下面测试代码。

  6. 通过添加__block,可以改变3和5的结果。

测试代码:

    static int variable_static = 0;
   int variable = 0;
   __block int varibale_blcok = 0;
   KFABlockDto *dto1 = [[KFABlockDto alloc] initWithName:@"我是1"];
   KFABlockDto *dto2 = [[KFABlockDto alloc] initWithName:@"我是2"];
   KFABlockDto *dto = dto1;
   KFABlockDto *dto_c = dto1;
   __block KFABlockDto *dto_block = dto1;
   __block KFABlockDto *dto_block_c = dto1;
   void (^TestBlock)(void) = ^(){
       variable_static++;
//        variable++; // Variable is not assignable (missing __block type specifier)
       varibale_blcok++;
       dto.name = @"我是dto";
//        dto_c = dto2; // Variable is not assignable (missing __block type specifier)
       dto_block.name = @"我是dto_block";
       dto_block_c = dto2;
   };
   NSLog(@"variable_static: %d\nvaribale_blcok: %d\ndto: %@\ndto_block: %@\ndto_block_c: %@",variable_static,variable_static,dto,dto_block,dto_block_c);
   TestBlock();
   NSLog(@"variable_static: %d\nvaribale_blcok: %d\ndto: %@\ndto_block: %@\ndto_block_c: %@",variable_static,variable_static,dto,dto_block,dto_block_c);

console:

variable_static: 0
varibale_blcok: 0
dto: 名字:我是1,地址:0x7ffee8bb51e8
dto_block: 名字:我是1,地址:0x7ffee8bb51e8
dto_block_c: 名字:我是1,地址:0x7ffee8bb51e8

variable_static: 1
varibale_blcok: 1
dto: 名字:我是dto_block,地址:0x7ffee8bb51e8
dto_block: 名字:我是dto_block,地址:0x7ffee8bb51e8
dto_block_c: 名字:我是2,地址:0x7ffee8bb51e8

__block究竟干了啥

如果要修改外部变量的值,我们需要用__block来修饰,那__block这个为啥可以实现这个效果呢。

__block修饰之后,会先生成一个__Block_byref_i_0的结构体,block捕获的是这个结构体的指针。这样就达到了改变外部变量的效果了。

block为什么用copy修饰

先看下面的测试代码及打印结果。

@property (nonatomic, copy) KFABasicParamBlock block_copy;
@property (nonatomic, strong) KFABasicParamBlock block_strong;
//@property (nonatomic, retain) KFABasicBlock block_retain; // Retain'ed block property does not copy the block - use copy attribute instead
@property (nonatomic, assign) KFABasicParamBlock block_assign;
@property (nonatomic, weak) KFABasicParamBlock block_weak;
______
//   ①
//    void(^TestBlock)(NSString *) = ^(NSString *type){NSLog(@"我只是个%@类型的block",type);};
   // ②
   int a = 2;
   void(^TestBlock)(NSString *) = ^(NSString *type) {
       NSLog(@"%d我只是个%@类型的block",a,type);
   };

   self.block_copy = TestBlock;
   self.block_strong = TestBlock;
   self.block_assign = TestBlock;
   self.block_weak = TestBlock;
    NSLog(@"%@\n%@\n%@\n%@\n%@",TestBlock,self.block_copy,self.block_strong,self.block_assign,self.block_weak);

   [self performSelector:@selector(testMemory) withObject:nil afterDelay:2.0];
______
- (void)testMemory {

   self.block_copy(@"copy");
   self.block_strong(@"strong");
//    self.block_assign(@"assign"); // Thread 1: EXC_BAD_ACCESS (code=2, address=0x600000ea5d70)
//    self.block_weak(@"weak"); // Thread 1: EXC_BAD_ACCESS (code=1, address=0x460)
}

console:

MRC && ①:
TestBlock:<__NSGlobalBlock__: 0x10e7d6c98>
block_copy:<__NSGlobalBlock__: 0x10e7d6c98>
block_strong:<__NSGlobalBlock__: 0x10e7d6c98>
block_assign:<__NSGlobalBlock__: 0x10e7d6c98>
block_weak:<__NSGlobalBlock__: 0x10e7d6c98>

MRC && ②:
TestBlock:<__NSStackBlock__: 0x7ffeed3e2f08>
block_copy:<__NSMallocBlock__: 0x6000016ead90>
block_strong:<__NSMallocBlock__: 0x6000016eb540>
block_assign:<__NSStackBlock__: 0x7ffeed3e2f08>
block_weak:<__NSStackBlock__: 0x7ffeed3e2f08>

ARC && ①:
TestBlock:<__NSGlobalBlock__: 0x10af01c90>
block_copy:<__NSGlobalBlock__: 0x10af01c90>
block_strong:<__NSGlobalBlock__: 0x10af01c90>
block_assign:<__NSGlobalBlock__: 0x10af01c90>
block_weak:<__NSGlobalBlock__: 0x10af01c90>

ARC && ②:
TestBlock:<__NSMallocBlock__: 0x6000020f5f80>
block_copy:<__NSMallocBlock__: 0x6000020f5f80>
block_strong:<__NSMallocBlock__: 0x6000020f5f80>
block_assign:<__NSMallocBlock__: 0x6000020f5f80>
block_weak:<__NSMallocBlock__: 0x6000020f5f80>
  1. __NSGlobalBlock__不管是ARC还是MRC,不受修饰词的影响,本身就是全局的。

  2. 在MRC下,block在创建的时候在栈上,用copy修饰是为了把它copy到堆上。也就是__NSStackBlock__->__NSMallocBlock__。用retain修饰的话,Xcode会报警告:

    Retain’ed block property does not copy the block - use copy attribute instead

  3. 在ARC下,block创建之后打印发现,它已经变成了__NSMallocBlock__。所以用strong还是copy效果是一样的,但是为了保留传统以及避免出现一些意外的问题,还是习惯继续使用copy。

    Blocks “just work” when you pass blocks up the stack in ARC mode, such as in a return. You don’t have to call Block Copy any more.

参考

  • Blocks Programming Topics

  • BlocksRuntime

  • Transitioning to ARC Release Notes

  • 深入研究Block捕获外部变量和__block实现原理

  • 深入研究Block用weakSelf、strongSelf、@weakify、@strongify解决循环引用

  • A look inside blocks: Episode 1

  • A look inside blocks: Episode 2

  • A look inside blocks: Episode 3 (Block_copy)

  • 谈Objective-C block的实现