深入浅出 block

前言

由于笔者前段时间一直忙着上家公司交接和找工作,近期文章很少有更新。简单说一下前几天的面试感触,总共面试了七家公司,第一家奇虎360面试,同两个技术聊了两小时左右,有些基本东西回答的不是很好,没有准备充分,最后不得而终;第二家公司规模一般,拿到了offer但是没去;第三家公司便是目前所在的公司,笔试➕面试➕人事大概面了六轮;第四家是苏宁,过了第一轮面试,复试因技术总监出差,耽误了一周,最终通知复试笔者已经入职现在的公司;另外三家公司面试都很一般,其中一家公司挂羊头卖狗肉,说招聘iOS开发工程师,结果去了一谈原来是做游戏开发,最终笔者建议该家公司把招聘岗位改为 Cocos-2d 工程师。此外接收到京东、优行二手车以及一一五的面试邀请,最终都没去面试。

面试的公司中有大公司也有小公司,其中有一点最深的感触,大公司和小公司面试的问题的差异确实很大。小公司偏重于项目经验和业务,而大公司偏向于技术深度,什么线程、运行时、block、性能优化、runloop等都会往深处去问。另外,很多人说大公司面试对算法要求很高,实际面试过程中笔者的感觉是,算法要求不是很高,可能会问到一丁点算法的问题,但是很少也很基础,能熟练掌握基本的数据结构,知道一些基础的算法应该足够应对面试。当然也可能是笔者的眼界太低。

发现在之前的一些面试中,有很多知识点掌握的还是太浅,所以最近打算集中抽一些时间来研究面试中被往深处问的一些问题。废话就到此为止,看文章!!!!!

概述

一、block是什么?

先看看 block 的官方定义。

In programming languages, a closure is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.
翻译: 闭包是一个函数(或指向函数的指针),再加上该函数执行的外部的上下文变量(有时候也称作自由变量)。

深入浅出 block_第1张图片
block 结构

上图是 block 数据结构定义,block 实际上也是一个对象。原因就在于 isa 指针。所有对象的都有isa 指针,用于实现对象相关的功能。关于 isa 指针这里不做深入讲解,如果想深入了解请看 runtime 相关的知识点,同时文章的末尾会推荐相关链接。

如果想进一步深入了解 block 的底层实现,推荐这两篇文章(涉及 C++代码)。谈Objective-C block的实现和《Objective-C 高级编程》干货三部曲(二):Blocks篇

二、关于__block

关于 block iOS 开发者应该知道:

  • block 中不允许修改外部变量的值(栈中指针的内存地址)。
  • 对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的。
  • __block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。
    可以结合下面的代码,以及分析理解这三句话。
//针对第一句和第二句去分析
int age = 10;
myBlock block = ^{
    NSLog(@"age = %d", age);//结果为:10
};
age = 18;
block();
//针对第三句去分析
__block int age = 10;
myBlock block = ^{
    NSLog(@"age = %d", age);//结果为:18
};
age = 18;
block();
2.1 为什么不允许修改 block 中修改外部变量的值?

要知道 block 和函数实际非常类似,本身也是属于"函数"范畴。变量进入block,实际就是已经改变了作用域。在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。

试想这样一个场景,block 内声明了一个与外部同名的变量,此时是允许修改还是不允许呢?只有加上了这样的限制,这样的情景才更容易控制吧。

2.2 ARC下,访问外界变量的 block 为何要自动从栈区拷贝到堆区?

栈上的 block,如果其所属的变量作用域结束,该 block 就被废弃,如同一般的局部变量。同时,block中的 __block 变量也被废弃。为了解决栈块在其变量作用域结束之后被废弃(释放)的问题,我们需要把 block 复制到堆中,延长其生命周期。开启ARC时,大多数情况下编译器会恰当地进行判断是否有需要将Block从栈复制到堆,如果有,自动生成将 block 从栈上复制到堆上的代码。block 的复制操作执行的是 copy 实例方法。block 只要调用了copy方法,栈块就会变成堆块。

通常在ARC中,block 都是通过copy 属性修饰符修饰的,实际上如果不写这个 copy 也是没有关系的,ARC情况下对block的处理比较特殊,默认是直接 执行 copy 操作的,写上 copy 也无所谓,写上copy的话可以时刻提醒我们block从栈区copy到堆区上。

2.3 使用__block后,栈区和堆区的验证问题
   __block int a = 0;
   NSLog(@"1、%p", &a);         //栈区
   void (^testBlock)(void) = ^{
       a = 1;
       NSLog(@"2、%p", &a);    //堆区
   };
   NSLog(@"3、%p", &a);         //堆区
   testBlock();
//打印结果

2 和 3 两者地址时一样的, block 内部的变量被 copy 到堆区,所以可以知道 3 的地址也是堆地址。把上述答应的地址由16进制转为10进制,则对应的转化结果为:

  • --->
  • --->
    两个十进制地址相差438851376个字节,大约是 418.5M 的空间。因为堆地址要小于栈地址,又因为 iOS 中一个进程的栈区内存只有 1 M,OS X 也只有 8 M,显然 2 和 3 地址属于堆区。

三、block 的类型

block 有三种类型:

  • NSConcreteGlobalBlock
  • NSConcreteStackBlock
  • NSConcreteMallocBlock
深入浅出 block_第2张图片
三种 block 对应的存储区域

通过上图我们可以看到,三种 block 对应的存储区域。存储在栈中的Block就是栈块、存储在堆中的就是堆块、既不在栈中也不在堆中的块就是全局块。

如果碰到一个 block 怎么知道它是三种类型的哪一种,我们先做一个简单的总结,接下来再针对每种情况细说。

  • block不访问外界变量(包括栈中和堆中的变量)
    此时 block 既不在栈也不在堆中,而是在代码段中,ARC和MRC下都是如此。此时为全局块。>>>>NSConcreteGlobalBlock
  • block访问外界变量
    1、MRC 环境下:访问外界变量的 block 默认存储栈中。>>>>NSConcreteStackBlock
    2、ARC 环境下:访问外界变量的 block 默认存储在堆中(实际是放在栈区,然后ARC情况下自动又拷贝到堆区),自动释放。>>>>NSConcreteMallocBlock
3.1 NSConcreteGlobalBlock

如果一个 block 中没有引用外部变量并且没有被其他对象持有,就是NSConcreteGlobalBlock。

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"%@",^{printf("NSConcreteGlobalBlock");});
}
3.2 NSConcreteStackBlock

可以这么理解,形式上NSConcreteStackBlock同NSConcreteGlobalBlock相比,引用了外部变量的block。如下代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    int a = 10;
    NSLog(@"%@",^{
        printf("NSConcreteStackBlock");
        printf("%d", a);
    });
}

除此之外,还要知道 NSConcreteStackBlock 内部会有一个结构体__main_block_impl_0,这个结构体会保存外部变量,使其体积变大。这就导致了NSConcreteStackBlock并不像宏一样,而是一个动态的对象。而它由于没有被持有,所以在它的内部,它也不会持有其外部引用的对象。下面的代码可以验证这一切:

- (void)test{
    NSObject *obj = [[NSObject alloc]init];
    NSLog(@"1、%lu",obj.retainCount);
    void(^bloc)(void) = ^{
        NSLog(@"2、%lu",obj.retainCount);
    };
    bloc();
    NSLog(@"3、%lu",obj.retainCount);
}

//打印结果,引用计数始终未发生变化(注意仅在MRC环境下才能使用retainCount属性)
Test[17014:1040370] 1、1
2017-12-23 15:30:59.560310+0800 Test[17014:1040370] 2、1
2017-12-23 15:30:59.560434+0800 Test[17014:1040370] 3、1
3.3 NSConcreteMallocBlock

当一个block被copy时,将生成 NSConcreteMallocBlock。同 NSConcreteStackBlock 相比,不同的是 NSConcreteMallocBlock 会持有外部对象!

- (void)test{
    NSObject *obj = [[NSObject alloc]init];
    NSLog(@"1、%lu",obj.retainCount);
    void(^bloc)(void) = [^{
        NSLog(@"2、%lu",obj.retainCount);
    } copy];
    bloc();
    NSLog(@"3、%lu",obj.retainCount);
}
//打印结果
Test[17064:1046117] 1、1
2017-12-23 15:40:05.311172+0800 Test[17064:1046117] 2、2
2017-12-23 15:40:05.311301+0800 Test[17064:1046117] 3、2

3.4 小结
有人认为:在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。其实这种说法是错误的,ARC下默认的赋值操作是strong的,到了 block 身上自然就成了copy,所以常常打印出来的 block 就是NSConcreteMallocBlock了。只是在 ARC 中,系统默认做了 copy 操作,我们无法看到而已。

四、关于 block 的生命周期(Strong Weak Dance)

下面的写法可以避免循环引用。但是有发生崩溃的可能,假设block被放在子线程中执行,而且执行过程中self在主线程被释放了。由于wself是一个弱引用,因此会自动变为nil。在 KVO 中这会导致崩溃。

__weak MyViewController *wself = self;
self.completionHandler = ^(NSInteger result) {
    [wself.property removeObserver: wself forKeyPath:@"pathName"];
};

解决办法,先将强引用的对象转为弱引用指针,防止了Block和对象之间的循环引用。再在Block的中,将weakSelf的弱引用转换成strongSelf这样的强引用指针,防止了多线程和ARC环境下弱引用随时被释放的问题。

__weak MyViewController *wself = self;
self.completionHandler = ^(NSInteger result) {
    __strong __typeof(wself) sself = wself; // 强引用一次
    [sself.property removeObserver: sself forKeyPath:@"pathName"];
};

五、参考

  • 谈Objective-C block的实现
  • 《Objective-C 高级编程》干货三部曲(二):Blocks篇
  • 笔者推荐的runtime文章

你可能感兴趣的:(深入浅出 block)