iOSiOS进阶之路

iOS block变量截获的那些事

2019-05-14  本文已影响159人  尘埃Wang

1、前言

对于初中级iOS开发工程师来说,面试的时候手写block是比较常见的问题,那对于高级及以上在问到block的使用的时候,不得不提block的变量截获本质了。在此我对此问题做一些总结,仅供各位大佬借鉴、斧正。

2、实例探究

在开始之前,我们先来总结一下变量的几种类型:
a.局部变量(基本数据类型)
b.局部变量(对象类型)
c.静态局部变量
d.全局变量和静态全局变量

下面开始我们一一分析;

a.局部变量 -- 基本数据类型

来看下面一段代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    int a = 10;
    void (^varBlock)(void) = ^{
        
        NSLog(@"%d", a); // 只会打印10
    };
    a = 20;
    varBlock();
}

然后我们使用clang命令clang -rewrite-objc ViewController.m来观察,编译后的ViewController.cpp文件代码。

注:
如果上述命令报错fatal error: 'UIKit/UIKit.h' file not found,可换成clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m

我们会在文件中找到以下代码:

局部变量 -- 基本数据类型 其中__ViewController__viewDidLoad_block_impl_0方法,为viewDidLoad编译后的完整方法,在此可以看到我们定义的varBlock编译为(后续重点关注的结构体):
struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  int a;
  __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

我们可以看下__block_impl这个结构体:

struct __block_impl {
  void *isa; // 指向class的指针
  int Flags; // 标识
  int Reserved; // 保留的变量
  void *FuncPtr; // 函数指针
};

所以我们可以说Block是将 函数 及其 执行上下文 封装起来的对象。上述的字段就不一一介绍了,感兴趣的可以留言讨论。
还记得我刚刚提到的重点么,可以看到最后截获的变量int a就是已经作为参数传递进去了,并且是截获的是变量a的值,所以后续无论怎么修改,我们block中的变量a的值,都是我们在定义时已经传递进去的值。也就是10。

补充:

接下来在这个地方补充一个面试比较常问的问题,也就是修改变量a的值,我们需要在block的声明之前把int a = 10;修改为__block int a = 10;,下面来看一下__block修饰后的,变量截获的问题,同样的使用clang编译文件,看一下重点代码:

__block修饰下的变量截获.jpg 我们看到,之前传递的变量int a变成了__Block_byref_a_0 *a,我们在看看__Block_byref_a_0这个是什么东西:
struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

找到了,原来__Block_byref_a_0也是一个含有isa的结构体,也就是说,用__block修饰后,变量a变成了一个对象,并且把对象的地址传递给了block,所以可以在block中修改变量的值。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __block int a = 10;
    void (^varBlock)(void) = ^{
        
        NSLog(@"%d", a); // 打印结果为20
    };
    a = 20;
    varBlock();
}
b.局部变量 -- 对象类型

测试代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __strong NSNumber *a = @10;
    void (^varBlock)(void) = ^{
        
        NSLog(@"%@", a); // a = 10
    };
    varBlock();
}

我们看下上述代码的编译结果:

局部变量 -- 对象类型1.jpg 截获的就仅仅是NSNumber *a;所以我们接着往下看 局部变量 -- 对象类型2.jpg 在图2中可以看到__attribute__((objc_ownership(strong))) NSNumber *a = ...这段代码可以看到其修饰符为strong。我们修改strong为__unsafe_unretained来看下结果: 局部变量 -- 对象类型3.jpg 其中修饰符变为__attribute__((objc_ownership(none))) NSNumber *a = ...,由此,我们可以得出结论:对于对象类型的局部变量是连同其修饰符一起截获的。因此,在这里截获了对象的修饰符,所以强引用对象,在使用的时候可能为造成循环引用,导致内存泄漏。
c.静态局部变量

测试代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    static NSNumber *a;
    static int b = 20;
    void (^varBlock)(void) = ^{
        
        NSLog(@"%@ -- %d", a, b); // 10 -- 30
    };
    a = @10;
    b = 30;
    varBlock();
}

编译结果如下图:

静态局部变量.jpg 可以清晰的看出,截获的是其指针。结论:对于静态局部变量是以指针的形式截取的
d.全局变量和静态全局变量

测试代码:

@implementation ViewController

// 全局变量
int global_a = 10;

// 静态全局变量
static int static_global_a = 20;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    void (^varBlock)(void) = ^{
        
        NSLog(@"%d -- %d", global_a, static_global_a);
    };
    varBlock();
}

@end

编译结果如下图:

1557831286486.jpg 可以看出,没有截获。结论:对于全局变量和静态全局变量不截获

3、总结

总结.jpg

最后,祝大家早日成为大牛。欢迎留言交流。

上一篇下一篇

猜你喜欢

热点阅读