标签 Objective-C 下的文章

本文主要讲述以下几个问题:

  1. 什么是block
  2. block有多少种类型
  3. block如何捕获外部变量
  4. 循环引用如何解决
  5. block copy 的不同结果
  6. __block修饰的变量在底层是什么样的

一、 block是什么

查看c++代码

在 xcode 中生成两个文件:TestBlock.h 、TestBlock.m 。其中 TestBlock.m 添加如下代码:

-(void)blockFunc {
    int n = 10;
    int (^myBlock)(int m) = ^(int m){
        return n + m ;
    };
    NSLog(@"%d",myBlock(1));
}

然后通过 clang -rewrite-objc TestBlock.m 命令将 TestBlock.m 转换出 TestBlock.cpp 文件,该文件中是底层如何实现 block 的c++代码。
我们看到,blockFunc 方法在底层代码中变成了 _I_TestBlock_blockFunc 函数:

static void _I_TestBlock_blockFunc(TestBlock * self, SEL _cmd) {
    int n = 10;
    //定义 myBlock
    int (*myBlock)(int m) = ((int (*)(int))&__TestBlock__blockFunc_block_impl_0((void *)__TestBlock__blockFunc_block_func_0, &__TestBlock__blockFunc_block_desc_0_DATA, n));
    //调用 myBlock
    ((int (*)(__block_impl *, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 1);
}

再看 __TestBlock__blockFunc_block_impl_0 的定义:

struct __TestBlock__blockFunc_block_impl_0 {
  struct __block_impl impl;  //block 主体
  struct __TestBlock__blockFunc_block_desc_0* Desc;  //block描述
  int n;  //block 捕获的外部变量 n
  //构造函数
  __TestBlock__blockFunc_block_impl_0(void *fp, struct __TestBlock__blockFunc_block_desc_0 *desc, int _n, int flags=0) : n(_n) {
    impl.isa = &_NSConcreteStackBlock;   //初始化为栈 block
    impl.Flags = flags;
    impl.FuncPtr = fp;  //block实现的函数地址
    Desc = desc;
  }
};

block的实现部分被封装在 __TestBlock__blockFunc_block_func_0 函数中,通过 __TestBlock__blockFunc_block_impl_0 构造函数传递到 __block_impl 结构体中:

static int __TestBlock__blockFunc_block_func_0(struct __TestBlock__blockFunc_block_impl_0 *__cself, int m) {
    int n = __cself->n; // bound by copy

    return n + m ;
}
struct __block_impl {
  void *isa;  //isa指针表示block的类型: _NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock
  int Flags;
  int Reserved;
  void *FuncPtr; //函数指针,__TestBlock__blockFunc_block_func_0 函数
};

在上述代码中,我们发现,myBlock 实质上是 __TestBlock__blockFunc_block_impl_0 结构体的对象。

总结

block 是在底层是一个结构体对象,它封装了 block 的实现代码和捕获的外部变量。

二、block的三种类型

全局block

NSGlobalBlock 类型,位于全局区。
当在 block 内部不使用外部变量,或只是用静态变量和全局变量时,则是全局 block。
示例代码:

static int b = 100;
void (^Block)(void) = ^{
   NSLog(@"== %d",b);
};
NSLog(@"%@",Block);
输出结果为:<__NSGlobalBlock__: 0x10eaca050>

栈block

NSStackBlock 类型,位于栈区。
当使用 __weak 修饰 block 时,被弱引用的 block 没有被 copy 到堆上,则为栈区block。
示例代码:

NSString *str = @"haha";
void (^__weak Block)(void) = ^{
   NSLog(@"-- %@",str);
};
NSLog(@"%@",Block);
输出结果为:<__NSStackBlock__: 0x7ffee1136168>

堆block

NSMallocBlock 类型,位于堆区。
当 block 内部引用了外部变量,且block被强引用时,则为堆区block
示例代码:

NSString *str = @"haha";
void (^Block)(void) = ^{
   NSLog(@"-- %@",str);
};
NSLog(@"%@",Block);
输出结果为:<__NSMallocBlock__: 0x60000227ef10>

理解不同类型的 block

示例代码:

-(void)testBlock{
    NSString *str = @"hello";  //对象类型的 str 局部变量
    int num = 10;  //值类型的 num 局部变量
    void (^__weak weakBlock)(void) ;  //声明 weakBlock
    //代码块
    {
        void (^__weak myBlock)(void) = ^{
            NSLog(@"num : %d",num);
            NSLog(@"str : %@",str);
        };
        NSLog(@"myBlock : %@",myBlock);
        weakBlock = myBlock;
    }
    NSLog(@"weakBlock : %@",weakBlock);
    weakBlock();
}

输出结果:

myBlock : <__NSStackBlock__: 0x7ffeee7b2170>
weakBlock : <__NSStackBlock__: 0x7ffeee7b2170>
num : 10
str : (null)

通过代码与结果分析,我们发现 myBlock 和 weakBlock 都是栈block,并且都指向同一块内存空间 0x7ffeee7b2170 ,而这块内存的生命周期是在 testBlock 函数的作用域之内,即使 myBlock 出了代码块范围,也依然存在于该函数的栈空间中,因此调用 weakBlock() 依然有效。

示例代码:

-(void)testBlock{
    int num = 10;  //值类型的 num 局部变量
    void (^__weak weakBlock)(void) ;  //声明 weakBlock
    //代码块
    {
        void (^myBlock)(void) = ^{
            NSLog(@"num : %d",num);
        };
        NSLog(@"myBlock : %@",myBlock);
        weakBlock = myBlock;
    }
    NSLog(@"weakBlock : %@",weakBlock);
    weakBlock();
}

输出结果:

myBlock : <__NSMallocBlock__: 0x6000034e55f0>
weakBlock : (null)

程序崩溃:EXC_BAD_ACCESS (code=1, address=0x10)
myBlock 去掉__weak修饰后,变成堆 block,而它的生命周期只在代码块中,出了代码块就被释放掉了,因此再调用weakBlock() 程序崩溃。

示例代码:

-(void)testBlock{
    void (^__weak weakBlock)(void) ;  //声明 weakBlock
    //代码块
    {
        void (^myBlock)(void) = ^{
            NSLog(@"hello~");
        };
        NSLog(@"myBlock : %@",myBlock);
        weakBlock = myBlock;
    }
    NSLog(@"weakBlock : %@",weakBlock);
    weakBlock();
}

输出结果:

myBlock : <__NSGlobalBlock__: 0x10e4790d0>
weakBlock : <__NSGlobalBlock__: 0x10e4790d0>
hello~

不使用外部变量后,myBlock 变成全局 block,在内存中类似于单例,一直存在直到程序终止,因此调用 weakBlock()有效。

三、捕获外部变量

在实践中发现,block 对不同类型和不同作用域的外部变量捕获方式是不尽相同的,总结起来如下列表格:

变量类型是否捕获捕获内容
局部变量值类型:捕获值 ; 对象类型: 捕获变量地址和修饰符
静态局部变量捕获变量地址
全局变量/静态全局变量不捕获,直接访问

四、解决 block 循环引用的几个方法

循环引用的问题在 iOS 经常遇到:A对象引用了B对象,B对象中又引用了A对象,导致两个对象都无法释放。
循环引用经常发生在 block 中,下面我们来看个例子:

@interface TestObject()

@property(nonatomic,strong) void(^myBlock)(void);

@end

@implementation TestObject

-(void)blockFunc{
    self.myBlock = ^{
        NSLog(@"self : %@",self);
    };
    self.myBlock();
}

在上述代码中,self 持有了 myBlock ,而在 myBlock 中又捕获了 self,因而构成循环引用,下面我们再来看看解决办法。

1、__weak__strong

常用手法,不再赘述。

2、__block

使用 __block 将 block 中的 self 置为nil,从而使得 block 不再持有 self 。

-(void)blockFunc{
    __block TestObject *temp = self;
    self.myBlock = ^{
        NSLog(@"self:%@",temp);
        temp = nil;
    };
    self.myBlock();
}

3、将强引用对象写成 block 的入参

block 的入参不需要被捕获,因此通过参数传入可以使得 block 不对 self 进行持有。

//修改 myBlock 定义,使其可以传入参数
@property(nonatomic,strong) void(^myBlock)(TestObject *obj);
-(void)blockFunc{
    self.myBlock = ^(TestObject *obj){
        NSLog(@"self:%@",obj);
    };
    self.myBlock(self);
}

五、block copy

copy操作对于不同类型 block 的结果是不一样的,总结起来看表格:

block类型copy结果
NSGlobalBlockcopy无效,结果还是同一个block,不会增加引用计数
NSStackBlockcopy 之后变成 NSMallocBlock,得到新内存,类似深拷贝
NSMallocBlockcopy之后还是 NSMallocBlock,但是同一块内存,会增加引用计数,类似浅拷贝

在 MRC 模式下,以下这段代码将生成一个 NSStackBlock,然后通过 copy 变成 NSMallocBlock

int n = 0;
void (^block)(void) = ^{
    NSLog(@"n = %d",n);
};
NSLog(@"b : %@",block);
block = [block copy];
NSLog(@"bb : %@",block);  

在 MRC 模式下输出为:

b : <__NSStackBlock__: 0x7ffee701c158>
bb : <__NSMallocBlock__: 0x60000178b000>

在 ARC模式下输出为:

b : <__NSMallocBlock__: 0x600003854420>
bb : <__NSMallocBlock__: 0x600003854420>

通过上面的比较,我们发现,block 创建时是 NSStackBlock ,而在 ARC 模式下,block创建时就执行了copy操作变成了 NSMallocBlock,并且 NSMallocBlock copy 之后还是同一块内存。

六、__block修饰的变量

__block修饰的变量在底层是一个结构体,我们来看一下代码:

__block int number = 100;
    void (^myBlock)(void) = ^{
        number = 101;
    };
    number = 2;

通过 clang rewrite 命令转换为 c++代码:

// __block number 在底层是一个名为 __Block_byref_number_0 的结构体
__attribute__((__blocks__(byref))) __Block_byref_number_0 number = {(void*)0,(__Block_byref_number_0 *)&number, 0, sizeof(__Block_byref_number_0), 100};
// myBlock 定义
    void (*myBlock)(void) = ((void (*)())&__TestBlock__blockFunc_block_impl_0((void *)__TestBlock__blockFunc_block_func_0, &__TestBlock__blockFunc_block_desc_0_DATA, (__Block_byref_number_0 *)&number, 570425344));

//block外部修改number的值为 2
    (number.__forwarding->number) = 2;  

__Block_byref_number_0 结构体:

struct __Block_byref_number_0 {
  void *__isa;
__Block_byref_number_0 *__forwarding;
 int __flags;
 int __size;
 int number;
};

从上述代码中,我们可以看到,在block内部和外部,对 number 的赋值都是通过对 number.__forwarding->number 赋值完成的,那么我们来了解一下其缘由。

__forwarding 指针

还是使用上面 __block int number = 100; 的例子,__forwarding指针是 __Block_byref_number_0 结构体的成员,在栈block中,它指向了__Block_byref_number_0 结构体自身(如图1),当栈block 被 copy 变成堆block时,栈区block中的 __forwarding指针指向了堆区block 的__Block_byref_number_0 结构体,而堆区block 的__forwarding指针还是指向结构体自身(如图2)。
图1:
图1
图2:
图2

通过上述代码,我们看到对 number 复制或访问都是通过__forwarding 指针完成的,这样做的好处是无论是栈block还是堆block,通过__forwarding 指针,都能够正确的访问到 __block 修饰的变量。

总结

1、当没有应用外部变量时是全局Block。
2、当 __weak 修饰Block 时是栈区Block
3、其他情况属于堆区Block
4、Block的本质是一个封装了实现和应用外部变量的结构体
5、__block 修饰的变量会被封装成一个结构体,并且Block结构体中有一个指针始终指向它,所以可以修改变量。