前言
由于笔者前段时间一直忙着上家公司交接和找工作,近期文章很少有更新。简单说一下前几天的面试感触,总共面试了七家公司,第一家奇虎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 数据结构定义,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 对应的存储区域。存储在栈中的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文章