浅谈Objective-C中的block那些事

1,684 阅读3分钟

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

  1. 在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.

参考