开胃面试题
1.block的原理是怎样的?本质是什么?
2.__block的作用是什么?有什么使用注意点?
3.block的属性修饰词是什么?为什么?使用block有哪些需要注意?
4.block在修改NSMutableArray时,需要添加__block吗?
看这篇文章之前可以先回答一下这几个面试题,然后带着问题耐心看完这篇文章,再来回答一下看看
一、Block是什么?
Block,我们把它叫做代码块,^加上{}就组成了一个Block,{}后面加上()就可以执行{}里面的代码。
^{
NSLog(@"这就是Block");
}()
如果想在用到的时候再调用,就要用个变量把它存起来。
//定义一个Block,并用变量存起来
void (^Block)(void) = ^ {
NSLog(@"这就是Block");
}
//调用Block
Block();
Block本质上是一个OC对象,是封装了函数调用以及函数调用环境的OC对象,它跟其他OC对象一样,它的内部也有isa指针。我们还可以调用class方法来查看它的类型,以及调用superclass方法查看它的父类,我们发现Block最终继承自NSObject,这说明Block本质上是一个OC对象。
void (^block)(void) = ^{
NSLog(@"Hello Block!");
};
NSLog(@"%@", [block class]);
NSLog(@"%@",[ [block class] superclass]);
NSLog(@"%@",[[[block class] superclass] superclass]);
NSLog(@"%@",[[[[block class] superclass] superclass] superclass]);
2019-07-01 18:14:03.880499+0800 blockDemo[50007:626703] __NSGlobalBlock__
2019-07-01 18:14:03.880732+0800 blockDemo[50007:626703] __NSGlobalBlock
2019-07-01 18:14:03.880758+0800 blockDemo[50007:626703] NSBlock
2019-07-01 18:14:03.880772+0800 blockDemo[50007:626703] NSObject
Program ended with exit code: 0
二、Block的底层实现
写一个简单的block,使用终端命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m将OC代码转化为C++底层实现代码,查看其底层实现。
可以看到,定义代码部分那里,block调用了__main_block_impl_0函数,并且将__main_block_impl_0函数的地址赋值给了block。
查看函数__main_block_impl_0内部
可以看到,里面有一个同名构造函数__main_block_impl_0,构造函数中对一些变量进行了赋值,然后返回一个结构体。即最终将一个__main_block_impl_0结构体的地址给了block变量。
我们再看看构造函数__main_block_impl_0的参数
查看参数(void*)__main_block_func_0内部
可以看到,__main_block_func_0里面有我们熟悉的NSLog,而这刚好是我们在block里面写的打印代码,实际上这里就是存储我们在block中写的代码的地方。而__main_block_impl_0函数中传入的是(void*)__main_block_func_0,也就是说将我们写在block里面的代码封装成__main_block_func_0函数,并将__main_block_func_0函数的地址传入了__main_block_impl_0的构造函数中并保存在结构体内。
三、Block的3种类型
Block有3种类型
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block1)(void) = ^{
NSLog(@"Hello Block");
};
int a = 10;
void (^block2)(void) = ^{
NSLog(@"Hello - %d", a);
};
NSLog(@"%@ %@ %@", [block1 class], [block2 class], [^{
NSLog(@"%d", a);
} class]);
}
return 0;
}
2019-07-04 21:42:39.598026+0800 Test[61582:813701] __NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__
Program ended with exit code: 0
运行以上代码, 我们可以看到, 分别打印了3种不同的Block, 这就是Block的3种类型, 它们分别处于内存中不同的区域
NSGlobalBlock (_NSConcreteGlobalBlock) --->数据区
NSStackBlock (_NSConcreteStackBlock) --->栈区
NSMallocBlock (_NSConcreteMallocBlock) --->堆区
NSGlobalBlock
接下来, 我们一起看下3种不同的Block, 现在我们创建的项目默认都是在ARC环境下, ARC帮我们做了很多事情, 编译器会根据情况自动将栈上的Block复制到堆上, 所以我们需要先把ARC关闭, 再来分别看看不同类型的Block
关闭ARC执行以下代码, 然后再开启ARC执行以下代码
void (^block1)(void) = ^{
NSLog(@"Hello Block");
};
NSLog(@"%@", [block1 class]);
//MRC下调用打印结果
2019-07-05 10:39:20.638751+0800 Test[19407:2194390] __NSGlobalBlock__
Program ended with exit code: 0
//ARC下调用打印结果
2019-07-05 10:40:20.638751+0800 Test[19407:2194390] __NSGlobalBlock__
Program ended with exit code: 0
我们发现这个Block不管是在MRC环境下, 还是ARC环境下都是NSGlobalBlock类型 , 这是一个最简单的Block, 没有访问任何外部的东西, 它位于内存中的数据区, 这种类型的Block没有什么要注意的,意义不大, 我们几乎不会使用, 而且它在ARC环境和在MRC环境下都是一样的. 我们再分别在ARC和MRC环境下对它进行copy操作, 发现结果它还是NSGlobalBlock类型
void (^block1)(void) = [^{
NSLog(@"Hello Block");
} copy];
NSLog(@"%@", [block1 class]);
2019-07-05 11:09:53.011184+0800 Test[41517:2593491] __NSGlobalBlock__
Program ended with exit code: 0
NSStackBlock 和 NSMallocBlock
我们再来看另一种情况, 跟上面的Block相比, 下面的Block访问了外部auto变量a. 我们分别在MRC环境下和ARC环境下执行以下代码
int a = 10;
void (^block2)(void) = ^{
NSLog(@"Hello - %d", a);
};
NSLog(@"%@", [block2 class]);
//MRC下打印输出
2019-07-05 10:52:05.526881+0800 Test[20480:2209222] __NSStackBlock__
Program ended with exit code: 0
//ARC下打印输出
2019-07-05 10:59:29.135015+0800 Test[21071:2216830] __NSMallocBlock__
Program ended with exit code: 0
我们发现这个Block在MRC环境下是NSStackBlock类型, 但是在ARC环境下, 它变成了NSMallocBlock类型, 跟上一个Block比, 它只是多了访问了外部auto变量a. 这说明ARC对它进行了copy操作, 帮我们将它复制到了堆区.我们再分别在ARC和MRC环境下对它copy,看看它又是怎么样.
int a = 10;
void (^block2)(void) = [^{
NSLog(@"Hello - %d", a);
} copy];
NSLog(@"%@", [block2 class]);
//ARC下调用输出
2019-07-05 15:15:23.987181+0800 Test[42013:2599645] __NSMallocBlock__
Program ended with exit code: 0
//MRC下调用输出
2019-07-05 15:18:32.797398+0800 Test[42265:2602868] __NSMallocBlock__
Program ended with exit code: 0
我们发现, 无论是在ARC环境下, 还是在MRC环境下, 只要对访问了外部auto变量的Block进行copy, 都是NSMallocBlock类型.
我们来看下这种情况, 定义一个函数, 函数里面定义auto变量a, 在block里面访问auto变量a, 这时候我们分别调用函tes()t, block(), 会打印乱码.因为我们调用block的时候, auto变量a它被释放了.但是在ARC环境下就不会, Block会保住它, 这样我们才能正常使用auto变量a.
void(^block)(void);
void test() {
int a = 10;
block = ^{
NSLog(@"a = %d",a);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
return 0;
}
//MRC打印输出
2019-07-05 11:06:02.658670+0800 Test[21653:2223887] a = -272632440
Program ended with exit code: 0
//ARC打印输出
2019-07-05 11:07:05.658670+0800 Test[21653:2223887] a = 10
Program ended with exit code: 0
另外以下这几种情况, ARC也会帮我们进行copy操作
1.Block作为函数返回值时
2.将Block赋值给__strong指针时
3.Block作为 Cocoa API 中方法名含有 usingBlock 的方法参数时
4.Block作为 GCD API 的方法参数时
在MRC下, 对于3种Block的类型与环境, 我们可以总结得到下面这个表格(ARC下是编译器帮我们这么做了)
在上面的例子中, 我们也分别进行各种copy操作, 我们还可以得到下面这个表格
三、Block访问外部变量
Block访问Block外部的非对象类型变量
下面这段代码执行完毕后, 会输出多少呢?
int a = 100;
void (^Block)() = ^ {
NSLog(@"a is %d",a);
}
a = 200;
Block();
上面这段代码, 打印后输出100, 为什么会输出100, 而不是200呢? 这就是我们今天要讨论的Block捕获变量.
我们定义的Block, 可以在其他要用的地方进行调用, 可是局部变量a出了它的作用域, 就被销毁了, 于是在我们调用Block的时候, 它要访问的局部变量a可能就不存在了, 也就没法使用这个局部变量a了. 所以Block为了能够在使用局部变量a的时候确保能够用, 它把局部变量a赋值到它内部存了一份.
下面我们再来看另一段代码, 打印后输出的值是多少呢?
static int b = 100;
void (^Block)() = ^ {
NSLog(@"b = %d",b);
}
b = 200;
Block();
上面这段代码, 打印后输出的值是200. 那么问题来了, 为什么会出现这种差异呢?
因为使用static修饰的变量b, 会一直存在内存中, Block可以随时访问static修饰的变量b. 变量b也会存放到Block里面去, 不过变量b跟变量a不一样, 变量b是把指针传进了Block, 而变量a是把它的值传进了Block. 这样无论外面的变量b是否发生改变, Block都能使用真正的变量b.
我们再来看这段代码, 打印后输出的值是多少呢?
int a = 100;
static int b = 100;
int main() {
@autoreleasepool {
void (^Block)(void) = ^{
NSLog(@"a = %d, b = %d",a,b);
}
a = 200;
b = 200;
Block();
}
return 0;
}
上面这段代码输出的值是200, 200. 为什么会这样呢?
因为这两个都是全局变量, 它会一直存在程序中, 不会像局部变量一样出了作用域就销毁, Block在任何地方访问它们, 都是可以的. 并且Block不会捕获全局变量, 因为block可以随时访问它们, 没必要对它们进行捕获, 而且将它们捕获到Block里面, 还要花费资源把它们存起来, 这对程序来说简直是一种浪费.
总结上面的示例代码, 我们发现, 只要是局部变量, Block都会捕获它. 我们没有主动使用任何修饰符修饰的变量(系统默认使用auto帮我们修饰了,也叫自动变量), Block会对它进行值捕获, static修饰的静态变量, Block会对它进行指针捕获. 而全局变量, Block不会对它进行捕获.我们把以上总结做成下面这个表格
有了上面的总结, 我们再来看一段代码, 调用下面的test, Block会不会对self进行捕获?
#import "Test.h"
@implementtation Test
- (void)test {
void (^Block)(void) = ^{
NSLog(@"test:%p",self);
}
Block();
}
Block会对这个self进行捕获, 在我们的iOS中, 所有方法的调用, 底层都是给调用者发送消息, 所有的方法默认都会带两个参数, 方法调用者slef, 和方法名. 所以slef它是一个参数,参数都是局部变量, 所以slef它是一个局部变量. 我们上面已经总结出, 只要是局部变量, Block就会捕获, 所以Block会对这个self进行捕获.
__Block
对于静态变量和全局变量, 可以直接Block内部进行修改, 但是如果是非对象类型的auto变量, 则需要使用__block修饰, 才能在Block里面进行修改.
编译器会将__block变量包装成一个对象, __block不能修饰静态变量和全局变量.
__block修饰的变量, 当block在栈上时, 并不会对__block变量产生强引用. 但是当Block被copy到堆上时, 会调用Block内部的copy函数, 然后会对这个变量产生强引用, 当Block被销毁的时候, 这个变量才会销毁, 这里的内存管理一般不用我们来操心, __strong和__weak也只能修饰对象类型, 不能修饰非对象类型.
__block也可以用来修饰对象类型的auto变量, 这个时候可以使用__strong和__weak一起来用.使用__block修饰的对象, 也会被包装成一个对象到Block内部, Block对这个对象也是强引用. 但是不同的是, 对象类型的auto变量, 会根据外部__strong和__weak, 在__block的包装里面分别生成强弱引用.
Block访问对象类型的auto变量
在ARC环境下新建一个Person类, 里面定义一个int类型的age属性, 在.m里面打印___func____, 方便查看Person对象的销毁, 然后我们在main.m里面执行以下代码, 在NSLog(@"-------");处打上断点
#import
#import "Person.h"
typedef void(^YYBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
YYBlock block;
{
Person *person = [[Person alloc] init];
person.age = 18;
block = ^{
NSLog(@"%d",person.age);
};
}
NSLog(@"-------");
return 0;
}
(lldb)
发现程序运行到断点处的时候什么都没有打印,也就是person对象没有销毁, 默认情况下person对象是强指针指着的, 现在我们再将person对象使用__weak进行处理, 同样的也是在NSLog(@"-------");处打上断点
#import
#import "Person.h"
typedef void(^YYBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
YYBlock block;
{
Person *person = [[Person alloc] init];
person.age = 18;
__weak Person *weakPerson = person;
block = ^{
NSLog(@"%d",weakPerson.age);
};
}
NSLog(@"-------");
return 0;
}
}
2019-07-05 16:45:41.636998+0800 Test[49499:2692798] -[Person dealloc]
(lldb)
我们发现person对象销毁了, 之所以会这样, 是因为Block内部会将要使用的外部对象复制进去, 并根据是__strong还是__weak修饰做不同的处理. 如果是__strong, Block内部会对它进行强引用,所以在Block没有被释放的时候, 被强引用的对象也不会释放, 如果是__weak,Block内部会对它进行弱引用, 所以即使Block还没有释放, 被弱引用的对象也会释放.当然以上情况都是Block在堆上的情况, 若是Block在栈区, 它连自己的生命都不能照顾周全, 别说其他了, 所以在栈区的Block, 不会对auto变量产生强引用.
关于循环引用的问题
在ARC环境下, 访问OC对象的成员变量时, 默认是__strong修饰的, 再根据前面所述, 我们知道Block访问外部变量的时候, 会根据是__strong还是__weak分别进行强引用和弱引用.
当我们直接在Block外部对Block进行了强引用的时候, 如果这个时候还使用默认的__strong修饰访问的外部OC对象的成员变量, Block内部就会对self进行捕获并强引用, 这样就形成了self对Block的强引用, Block内部对self的强引用这样的循环引用.
使用__weak解决循环引用问题, 只要在Block外部是用__weak修饰Block访问的变量就可以解决循环引用问题. 使用__weak修饰, 不会产生强引用, 指向的对象销毁时, 会自动让指针置为nil.
使用__unsafe_unretained解决循环引用问题, 使用__unsafe_unretained也可以解决循环引用的问题, 但是它是不安全的, 指向的对象销毁时, 指针存储的地址值不变, 可能发生野指针错误. 所以一般不会使用它.
使用__block解决循环引用问题, 这种方式比较麻烦, 需要在block里面将self置为nil, 还需要要调用Block, 没有调用Block就会造成内存泄漏, 这种方案不建议使用.
四、Block属性使用copy和使用strong的区别
在ARC环境下, Block属性使用copy和使用strong并没有区别, 最终Block都会拷贝到堆区. 在MRC下, 使用strong修饰则不会拷贝到堆区.所以一般情况下, 使用copy要多, 因为它在ARC和MRC下都可以, 不用考虑太多问题.
MRC下 Block 属性的建议写法
@proerty (nonatomic, copy) void(^block)(void);
ARC下 Block 属性的建议写法
@proerty (nonatomic, copy) void(^block)(void);
@proerty (nonatomic, strong) void(^block)(void);