Objective-C Block的剖析

2019-06-28  本文已影响0人  字节码

Block在开发中常用的,要想解决Block在开发中遇到的问题,我们需要了解Block的本质、截获变量的特性、__block修饰符、block的内存管理和循环引用问题。通过clang编译器深度剖析block底层实现。

为了便于理解使用了较多白话,原文在我的blockObjective-C Block的剖析中,查看demo

通过clang编译器剖析block

@implementation XYBlock

- (void)method {
    int multiplier = 6;
    int (^block)(int) = ^int(int num){
        return num * multiplier;
    };
    
    multiplier = 4;
    NSLog(@"result is %d", block(2));
}

@end

通过clang编译器生成cpp实现。

clang -rewrite-objc XYBlock.m 

执行完成后,在当前目录下生成同名的.cpp文件

XYBlock.cpp中,我们找到编译后_I_XYBlock_method函数就是XYBlock类中-method的方法:

static void _I_XYBlock_method(XYBlock * self, SEL _cmd) {
    int multiplier = 6;
    int (*block)(int) = ((int (*)(int))&__XYBlock__method_block_impl_0((void *)__XYBlock__method_block_func_0, &__XYBlock__method_block_desc_0_DATA, multiplier));

    multiplier = 4;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_9n_2_q48v8j049bnt5m4k7c_4rh0000gq_T_XYBlock_69f118_mi_0, ((int (*)(__block_impl *, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 2));
}

-method方法没有参数的,所以_I_XYBlock_method函数的两个参数为objcetive-c方法的默认隐士参数(self 和 _cmd选择器)。

_I_XYBlock_method函数内:
第一行代码int multiplier = 6;XYBlock中的-method方法的第一行保持一致。
第二行代码int (*block)(int) = ((int (*)(int))&__XYBlock__method_block_impl_0((void *)__XYBlock__method_block_func_0, &__XYBlock__method_block_desc_0_DATA, &multiplier));block编译后的结果,__XYBlock__method_block_impl_0是一个结构体,其中第一个参数__XYBlock__method_block_func_0是一个void *类型的函数指针,第二个参数为block的描述,第三个参数为传入block的变量multiplier
第三行代码主要为block`的调用

编译后block的结构体 __XYBlock__method_block_impl_0

为了了解block的本质,这里需要先了解XYBlock-method方法中的定义的blockclang编译后的 __XYBlock__method_block_impl_0这个结构体。

struct __XYBlock__method_block_impl_0 {
  struct __block_impl impl; // __block_impl 类型的结构体
  struct __XYBlock__method_block_desc_0* Desc; // block的描述
  int multiplier; // block 中使用外部的变量,由于block使用的`multiplier`变量为局部变量基本数据类型的变量,所以截获的是其值
  /// 构造函数
  /// @param fp void * 类型的函数指针
  /// @param desc block的描述
  /// @param _multiplier block中使用外部的变量 _multiplier 对应`XYBlock`中`-method`方法中的`int multiplier = 6;`
  /// @param flags 标记
  __XYBlock__method_block_impl_0(void *fp, struct __XYBlock__method_block_desc_0 *desc, int _multiplier, int flags=0) : multiplier(_multiplier) {
    impl.isa = &_NSConcreteStackBlock; // block 的类型
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

先看下这个结构体的成员变量:

__block_impl结构体

struct __block_impl {
  void *isa; // isa 指针,通过这个isa 指针,由于`objc_class`和`objc_object`都具备这一特性,可以把Block理解是一个对象
  int Flags; // 标记
  int Reserved; 
  void *FuncPtr; // 是一个无类型的函数指针,在定义block的`{}`中的定义的执行体,最终就会产生这样一个函数,`block`通过一个指针指向这样的实现
};

block{}定义的执行体

以下为编译后产生的函数体实现

/// 根据定义的block生成的函数体实现
/// @param num 定义block时的参数
static int __XYBlock__method_block_func_0(struct __XYBlock__method_block_impl_0 *__cself, int num) {
  int multiplier = __cself->multiplier; // block使用的外部变量`multiplier`为局部变量基本数据类型的变量,所以这里可以看到block将使用的外部变量的值copy到函数体内
  // block的 {} 内执行的代码
  return num * multiplier;
}

什么是Block

Block是一个对象,这个对象封装了函数以及函数执行的上下文。

什么是Block的调用

Block的调用就是函数的调用的本质
要想了解为什么Block的调用就是函数的调用这个问题,我们需要到XYBlock.cpp(clang编译XYBlock.m的结果)文件中,并找到static void _I_XYBlock_method(XYBlock * self, SEL _cmd)这个函数的实现,上面我们以及介绍过这个函数,它是对XYBlock.m-method方法编译后的实现:

static void _I_XYBlock_method(XYBlock * self, SEL _cmd) {
    int multiplier = 6;
    int (*block)(int) = ((int (*)(int))&__XYBlock__method_block_impl_0((void *)__XYBlock__method_block_func_0, &__XYBlock__method_block_desc_0_DATA, multiplier));

    multiplier = 4;
    // 调用block,实际上就是函数调用
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_9n_2_q48v8j049bnt5m4k7c_4rh0000gq_T_XYBlock_69f118_mi_0, ((int (*)(__block_impl *, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 2));
}

下面的函数实现是通过执行__block_impl结构体中FuncPtr函数指针执行block
((int (*)(__block_impl *, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 2)
首先进行强制类型转换,然后在执行FuncPtr时,传入了两个参数,一个是block本身,另一个是2,实际上执行的__XYBlock__method_block_func_0函数。

Block截获变量特性

要想了解Block截获变量的特性,我们先从一道笔试题开始着手:

    int multiplier = 6;
    int (^block)(int) = ^int(int num){
        return num * multiplier;
    };
    
    multiplier = 4;
    NSLog(@"result is %d", block(2));

答案:result is 12

当然要知道这道题为什么是12,我们需要了解Block截获变量的类型,哈哈。

Block对以下类型的变量截获是不一样的。

关于block的截获特性你是否了解

针对不同变量类型,Block是如何截获的

如果要理解什么是所有权一起截获或者其他截获的原理,需要查看使用clang编译block后的c++文件。

截获变量的源码解析

为了验证以上类型的变量截获区别,并了解不同变量类型block是如何截获的,我们使用clang -rewrite-objc -fobjc-arc xxx.m编译block的c++实现,将xxx.m替换后为需要编译的.m文件。比之前用的命令多了-fobjc-arc参数

我们创建XYBlock1,并在其-method方法的block中使用以上类型的变量,以验证block对变量的截获。

@implementation XYBlock1

// 全局变量
int global_var = 4;
// 静态全局变量
static int static_global_var = 5;

- (void)method
{
    // 基本数据类型的变量
    int var = 1;
    // 对象类型的局部变量
    __unsafe_unretained id unsafe_obj = nil;
    __strong id strong_obj = nil;
    // 静态局部变量
    static int static_var = 3;
    void (^ block)(void) = ^{
        NSLog(@"局部变量<基本数据类型> var is %d", var);
        NSLog(@"局部变量<__unsafe_unretained 对象类型> unsafe_obj is %@", unsafe_obj);
        NSLog(@"局部变量<__strong 对象类型> strong_obj is %@", strong_obj);
        NSLog(@"静态局部变量 static_var is %d", static_var);
        NSLog(@"全局变量 global_var is %d", global_var);
        NSLog(@"静态全局变量 static_global_var is %d", static_global_var);
    };
    
    block();
}
@end

clang -rewrite-objc -fobjc-arc XYBlock1.m

执行完成后,会生成XYBlock1.cpp文件。

查看经过编译之后block对应的结构体:


// 全局变量不会被截获
int global_var = 4;
// 全局静态变量不会被截获
static int static_global_var = 5;

struct __XYBlock1__method_block_impl_0 {
  struct __block_impl impl;
  struct __XYBlock1__method_block_desc_0* Desc;
  // var 是截获局部变量的值
  int var;
  // unsafe_obj 是连同所有权修饰符一起截获
  __unsafe_unretained id unsafe_obj;
  __strong id strong_obj;
  // static_var 是局部静态变量,截获其指针
  int *static_var;
  __XYBlock1__method_block_impl_0(void *fp, struct __XYBlock1__method_block_desc_0 *desc, int _var, __unsafe_unretained id _unsafe_obj, __strong id _strong_obj, int *_static_var, int flags=0) : var(_var), unsafe_obj(_unsafe_obj), strong_obj(_strong_obj), static_var(_static_var) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

通过以上可以结构体,看到在XYBlock1.m-method方法中,block中的使用的外部变量是通过__XYBlock1__method_block_impl_0结构体的构造函数的参数传递给这个block的结构体。
XYBlock1.m中的变量是以什么样的形式被block捕获的:

当我们理解以上的变量类型在block中捕获以后,就会对Block产生循环引用有更好的理解。

__block修饰符

当被截获的变量在block内赋值时,需要使用__block修饰符。

问题:下面两段代码,分别定义了array变量,并在block中操作array变量,哪段需要__block修饰array变量呢?

    {
        NSMutableArray *array = [NSMutableArray array];
        void (^block)(void) = ^{
            [array addObject:@123];
        };
        block();
    }
    {
        NSMutableArray *array = nil;
        void (^block)(void) = ^{
            array = [NSMutableArray array];
        };
        block();
    }

答案:第一段在block内对array变量的操作只是使用而非赋值,所以不需要__block修饰。而第二段在block内对array变量进行了赋值操作,所以需要使用__block修饰符,不然编译阶段无法通过。

变量赋值时,__block的特点

这与block截获变量的特性有关,上面我们已经介绍了各种类型变量在block内截获的区别。

__block的作用、机制、原理

创建XYBlock2类,并在-method方法中实现以下__block的例子:

@implementation XYBlock2
- (void)method {
    __block int multiplier = 6;
    int (^block)(int) = ^int(int num){
        return num * multiplier;
    };
    
    multiplier = 4;
    NSLog(@"result is %d", block(2));
}
@end

上面的_method方法中,在block中对外部变量multiplier和其内部变量num执行乘法操作,而multiplier__block修饰的整型变量。

输出结果:result is 8
上面的代码块中使用__block修饰了multiplier变量,输出的结果为8,如果不使用__block修饰则输出12。这是为什么呢?
这是因为__block修饰的整型变量变成了对象。

我们可以通过clang指令查看__block修饰的变量在block结构体中发生了什么,以及它的作用。

clang -rewrite-objc -fobjc-arc XYBlock2.m 

在编译完成后XYBlock2.cpp文件中,找到原-method方法编译后的函数实现_I_XYBlock2_method():

static void _I_XYBlock2_method(XYBlock2 * self, SEL _cmd) {
    // __block int multiplier = 6; 使用__block修饰的整型变量使用clang编译后变成了__Block_byref_multiplier_0类型的结构体
    __attribute__((__blocks__(byref))) __Block_byref_multiplier_0 multiplier = {(void*)0,(__Block_byref_multiplier_0 *)&multiplier, 0, sizeof(__Block_byref_multiplier_0), 6};
    int (*block)(int) = ((int (*)(int))&__XYBlock2__method_block_impl_0((void *)__XYBlock2__method_block_func_0, &__XYBlock2__method_block_desc_0_DATA, (__Block_byref_multiplier_0 *)&multiplier, 570425344));

    (multiplier.__forwarding->multiplier) = 4;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_9n_2_q48v8j049bnt5m4k7c_4rh0000gq_T_XYBlock2_620611_mi_0, ((int (*)(__block_impl *, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 2));
}

_I_XYBlock2_method()函数内第一行代码可以得知, 使用__block修饰的multiplier整型变量使用clang编译后变成了__Block_byref_multiplier_0类型的结构体。
第二行代码就是multiplier = 4;编译后的代码,最终变成了(multiplier.__forwarding->multiplier) = 4;,就是找到multiplier对象的__forwarding,并对其multiplier成员变量赋值为4。

struct __Block_byref_multiplier_0 {
  void *__isa;
__Block_byref_multiplier_0 *__forwarding; // 指向同类型的指针
 int __flags;
 int __size;
 int multiplier; // block 外部使用`__block`的变量
};

可以看到__Block_byref_multiplier_0结构体内部有一个isa指针,由于objc_objectobjc_class结构体内都具备这一特性,所以我认为被__block修饰的变量不管是基本数据类型还是对象,它都是一个对象。

我们找到clang编译后的block结构体__XYBlock2__method_block_impl_0:

struct __XYBlock2__method_block_impl_0 {
  struct __block_impl impl;
  struct __XYBlock2__method_block_desc_0* Desc;
  __Block_byref_multiplier_0 *multiplier; // by ref
  __XYBlock2__method_block_impl_0(void *fp, struct __XYBlock2__method_block_desc_0 *desc, __Block_byref_multiplier_0 *_multiplier, int flags=0) : multiplier(_multiplier->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

通过__XYBlock2__method_block_impl_0结构体的构造函数可以说明,multiplier(_multiplier->__forwarding)_multiplier->__forwarding赋值给了该结构体的multiplier结构体变量:

__Block_byref_multiplier_0 *_multiplier, int flags=0) : multiplier(_multiplier->__forwarding) {}
block在栈上时,__block结构体中的__forwarding指向__block结构体自己

当我们给multiplier赋值时,实际上是对__block结构体中的__forwarding指针,实际上还是这个__block结构体本身,所有还是对这个__block结构体的multiplier成员进行赋值。

block在堆上时,__block结构体中的__forwarding指向其他地方。

这个问题在下面的【栈上__block的copy操作】中会讲到。

Block内存管理

block有三种类型:_NSConcreteStackBlock_NSConcreteGlobalBlock_NSConcreteMallocBlock

内存分区
内核区
栈区(stack) _NSConcreteStackBlock在栈上
堆区(heap) _NSConcreteMallocBlock放在堆区
未初始化数据区(.bss)
已初始化数据区(.data) _NSConcreteGlobalBlock放在已初始化代码区。
代码段(.txt)
保留
Block类别 Copy结果
_NSConcreteStackBlock 栈区 堆区
_NSConcreteMallocBlock 堆区 增加引用计数
_NSConcreteGlobalBlock 数据区 什么都不做

由此就产生一个问题:在MRC环境下当我们对栈上的block进行copy操作之后是否会引起内存泄露?
答案是肯定的,这与我们手动通过alloc方法创建一个对象,没有调用release的效果是一样的。

(multiplier.__forwarding->multiplier) = 4;

当我们对栈上的block已经做完copy操作后,实际上我们修改的不是栈上__block变量的值,而是通过栈上__block结构体里面的__forwarding指针找到堆上的__block变量,然后对堆上的multiplier进行赋值比如4。
那么同样的,如果__block变量由于被成员变量block所持有的话,当我们在另一个方法或者其他的地方调用这个__block的修改的情况下,那实际上是通过自身的__forwarding指针来进行修改的。
经过编译器的编译,(multiplier.__forwarding->multiplier) = 4;代码不管是出现在栈上还是堆区的调用,实际上都是针对堆上的__block进行修改的。

如果我们没有对栈上的block进行copy操作,(multiplier.__forwarding->multiplier) = 4;修改的就是栈上block的__block变量

__forwarding的总结

@interface XYBlock3 ()

@property (nonatomic, copy) int(^blk)(int num);

@end

@implementation XYBlock3

- (void)method {

    __block int multipliter = 10;
    _blk = ^int(int num) {
        return num * multipliter;
    };
    
    multipliter = 6;
    [self executeBlock];
}

- (void)executeBlock {
    // 这是百度的笔试的真题,这道题实际上是考验我们对`__forwarding`的理解
    int result = _blk(4);
    NSLog(@"result is %d", result);
    // 输出结果 result is 24
}

@end

输出结果 result is 24
-method方法中的代码分析:
第一行代码__block int multipliter = 10;初始化了multipliter是一个__block修饰的局部变量,那么实际上在clang编译后,他就变成了对象。所以实际上multipliter = 6;multipliter的赋值不是对这个局部变量的赋值,而是通过multipliter__forwarding指针,然后对其成员变量multipliter进行赋值。
第二行代码创建一个block并赋值给_blk_blkXYBlock3对象的成员变量,并且属性修饰符为copy,当对他进行赋值操作时,实际上会对其进行copy,那么_blk这个block就会在堆区有另一份副本。
第三行代码multipliter = 6;代表的含义就是通过栈上的multipliter__forwarding指针找到堆上的所对应的__block变量,然后对堆上的__block结构体的multiplier属性进行赋值比如6。

-executeBlock方法中执行_blk(4)并传入参数为4,在block代码块内执行了num * multipliter;,实际上这里使用的multipliter变量是堆上的__block变量,所以之后block之后是4乘以6的,结果就是24。

Block循环引用的问题

@interface XYBlock4 ()

@property (nonatomic, strong) NSMutableArray *array;
@property (nonatomic, copy) NSString *(^strBlock)(NSString *num);

@end

@implementation XYBlock4

- (void)method {
    _array = [NSMutableArray arrayWithObject:@"block"];
    
    _strBlock = ^NSString *(NSString *num) {
        // 在此block内使用成员变量`_array`,会产生警告: Block implicitly retains 'self'; explicitly mention 'self' to indicate this is intended behavior
        return [NSString stringWithFormat:@"helloOC_%@", _array[0]];
    };
    _strBlock(@"hello");
}

@end

分析以上代码产生循环引用的原因:
以上代码中,XYBlock4类有两个成员变量strong特性的arraycopy特性的strBlock,此时XYBlock4对象持有了array变量和strBlock变量,而在strBlock的block代码块中又持有了XYBlock4对象的array成员变量。
这样会产生循环引用,并且是自循环形式的循环引用,由于XYBlock4是通过copy关键字声明的strBlock成员,所以当前对象对这个block是强引用的,而block的表达式中又使用了当前对象的array成员变量,那么通过block截获变量的特性,关于block中使用对象类型的局部变量或成员变量,会连同其所有权及关键字一同截获,而array属性在当前对象中是使用strong修饰的,所以在block的结构体中有一个strong类型的指针指向原来的对象或当前对象,由此就产生了一个循环引用。

@interface XYBlock4 ()

@property (nonatomic, strong) NSMutableArray *array;
@property (nonatomic, copy) NSString *(^strBlock)(NSString *num);

@end

@implementation XYBlock4

- (void)method {
    _array = [NSMutableArray arrayWithObject:@"block"];
    
    // 解决循环引用
    __weak NSMutableArray *weakArray = _array;
    _strBlock = ^NSString *(NSString *num) {
        // 在此block内使用成员变量`_array`,会产生警告: Block implicitly retains 'self'; explicitly mention 'self' to indicate this is intended behavior
        return [NSString stringWithFormat:@"helloOC_%@", weakArray[0]];
    };
    _strBlock(@"hello");
}

@end

为什么通过__weak修饰对于的成员变量就可以达到避免循环引用的目的呢?
这个答案实际在讲block截获变量的特性时就给出了答案,由于block对截获的变量,如果这个变量是对象类型的,连同其所有权修饰符一起截获,当我们在block使用的外部变量是__weak修饰符的,那么在block当中所 产生的结构体中的变量也是__weak修饰的。

__block引发的循环引用

先看示例,看看以下代码有什么问题:

@interface XYBlock5 ()

@property (nonatomic, assign) int var;
@property (nonatomic, copy) int (^ blk)(int num);

@end

@implementation XYBlock5

- (void)method {
    __block XYBlock5 *blockSelf = self;
    _blk = ^int (int num) {
        return num * blockSelf.var;
    };
    _blk(3);
}

@end

解决方案:
在block表达式内部加入blockSelf = nil;的赋值操作,就可以规避循环引用,也就是说当我们调用_blk之后就会断开这个环,然后就可以得到内存的释放和销毁,这种解决方案有一个弊端,如果这个block未被调用时,这个环就一直存在,导致无法释放该对象。

@interface XYBlock5 ()

@property (nonatomic, assign) int var;
@property (nonatomic, copy) int (^ blk)(int num);

@end

@implementation XYBlock5

- (void)dealloc {
    NSLog(@"%s", __func__);
}

- (void)method {
    __block XYBlock5 *blockSelf = self;
    _blk = ^int (int num) {
        
        int ret = num * blockSelf.var;
        // 在block表达式内部加入`blockSelf = nil;`的赋值操作,就可以规避循环引用
        blockSelf = nil;
        return ret;
    };
    _blk(3);
}

@end
上一篇 下一篇

猜你喜欢

热点阅读