一道Block面试题的深入挖掘
2020/4/30 23:02:45
本文主要是介绍一道Block面试题的深入挖掘,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
0. 序言
最近看到了一道Block的面试题,还蛮有意思的,来给大家分享一下。
本文从一道Block面试题出发,层层深入到达Block原理的讲解,把面试题吃得透透的。
题外话:
很多人觉得Block的定义很怪异,很难记住。但其实和C语言的函数指针的定义对比一下,你很容易就可以记住。
// Block returnType (^blockName)(parameterTypes) // 函数指针 returnType (*c_func)(parameterTypes) 复制代码
例如输入和返回参数都是字符串:
(char *) (*c_func)(const char *); (NSString *) (^block)(NSString *); 复制代码
好了,下面正式开始~
1. 面试题
1.1 问题1
以下代码存在内存泄露么?
- 不存在
- 存在
- (void)viewDidLoad { [super viewDidLoad]; NSNotificationCenter *__weak center = [NSNotificationCenter defaultCenter]; id token = [center addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { [self doSomething]; [center removeObserver:token]; }]; } - (void)doSomething { } 复制代码
答案是存在!
1.1.1 分析
-
block
中,我们使用到的外部变量有self
和center
,center
使用了__weak
说明符肯定没问题。 -
center
持有token
,token
持有block
,block
持有self
,也就是说token
不释放,self
肯定没法释放。 -
我们注意到
[center removeObserver:token];
这步会把token
从center
中移除掉。按理说,center
和self
是不是就可以被释放了呢?
我们来看看编译器怎么说:
编译器告诉我们,token
在被block捕获之前没有初始化![center removeObserver:token];
是没法正确移除token
的,所以self
也没法被释放!
为什么没有被初始化?
因为token在后面的方法执行完才会被返回。方法执行的时候token还没有被返回,所以捕获到的是一个未初始化的值!
1.2 问题2
以下代码存在内存泄露么?
- 不存在
- 存在
- (void)viewDidLoad { [super viewDidLoad]; NSNotificationCenter *__weak center = [NSNotificationCenter defaultCenter]; id __block token = [center addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { [self doSomething]; [center removeObserver:token]; }]; } - (void)doSomething { } 复制代码
这次代码在token
之前加入了__block
说明符。
提示:这次编译器没有警告说token没有被初始化了。
答案是还是存在!
1.2.1 分析
首先,证明token
的值是正确的,同时大家也可以看到token
确实是持有block
的。
那么,为什么还会泄露呢?
因为,虽然center
对token
的持有已经没有了,token
现在还被block
持有。
可能还有同学会问:
加入了__block说明符,token对象不是还是center返回之后才能拿到么,为什么加了之后就没问题了呢?
原因会在Block原理部分详细说明。
1.3 问题3
以下代码存在内存泄露么?
- 不存在
- 存在
- (void)viewDidLoad { [super viewDidLoad]; NSNotificationCenter *__weak center = [NSNotificationCenter defaultCenter]; id __block token = [center addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { [self doSomething]; [center removeObserver:token]; token = nil; }]; } - (void)doSomething { } - (void)dealloc { NSLog(@"%s", __FUNCTION__); } 复制代码
答案是不存在!
1.3.1 分析
我们可以验证一下:
可以看到,我们添加token = nil;
之后,ViewController
被正确释放了。这一步,解除了token
与block
之间的循环引用,所以正确释放了。
有人可能会说:
使用__weak typeof(self) wkSelf = self;就可以解决self不释放的问题。
确实这可以解决self不释放的问题,但是这里仍然存在内存泄露!
2. Block的原理
虽然面试题解决了,但是还有几个问题没有弄清楚:
- 为什么没有
__block
说明符token
未被初始化,而有这个说明符之后就没问题了呢? token
和block
为什么会形成循环引用呢?
2.1 Block捕获自动变量
刚刚的面试题比较复杂,我们先来看一个简单的:
Block转换为C函数之后,Block中使用的自动变量会被作为成员变量追加到 __X_block_impl_Y结构体中,其中 X一般是函数名, Y是第几个Block,比如main函数中的第0个结构体: __main_block_impl_0。
typedef void (^MyBlock)(void); int main(int argc, const char * argv[]) { @autoreleasepool { int age = 10; MyBlock block = ^{ NSLog(@"age = %d", age); }; age = 18; block(); } return 0; } 复制代码
顺便说一下,这个输出:age = 10
在命令行中对这个文件进行一下处理:
clang -w -rewrite-objc main.m 复制代码
或者
xcrun -sdk iphoneos clang -arch arm64 -w -rewrite-objc main.m 复制代码
区别是下面指定了SDK和架构代码会少一点。
处理完之后会生成一个main.cpp的文件,打开后会发现代码很多,不要怕。搜索int main就能看到熟悉的代码了。
int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; int age = 10; MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)); age = 18; ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); } return 0; } 复制代码
下面是main函数中涉及到的一些结构体:
struct __main_block_impl_0 { struct __block_impl impl; //block的函数的imp结构体 struct __main_block_desc_0* Desc; // block的信息 int age; // 值引用的age值 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) { impl.isa = &_NSConcreteStackBlock; // 栈类型的block impl.Flags = flags; impl.FuncPtr = fp; // 传入了函数具体的imp指针 Desc = desc; } }; struct __block_impl { void *isa; // block的类型:全局、栈、堆 int Flags; int Reserved; void *FuncPtr; // 函数的指针!就是通过它调用block的! }; static struct __main_block_desc_0 { // block的信息 size_t reserved; size_t Block_size; // block的大小 } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; 复制代码
有了这些信息,我们再看看
MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)); 复制代码
可以看到,block
初始化的时候age
是值传递,所以block
结构体中age=10
,所以打印的是age = 10。
2.2 __block说明符
Block中修改捕获的自动变量有两种方法:
-
使用静态变量、静态全局变量、全局变量
从Block语法转化为C语言函数中访问静态全局变量、全局变量,没有任何不同,可以直接访问。而静态变量使用的是静态变量的指针来进行访问。
自动变量不能采用静态变量的做法进行访问。原因是,自动变量是在存储在栈上的,当超出其作用域时,会被栈释放。而静态变量是存储在堆上的,超出作用域时,静态变量没有被释放,所以还可以访问。
-
添加 __block修饰符
__block存储域类说明符。存储域说明符会指定变量存储的域,如栈auto、堆static、全局extern,寄存器register。
比如刚刚的代码加上 __block说明符:
typedef void (^MyBlock)(void); int main(int argc, const char * argv[]) { @autoreleasepool { int __block age = 10; MyBlock block = ^{ age = 18; }; block(); } return 0; } 复制代码
在命令行中对这个文件进行一下处理:
xcrun -sdk iphoneos clang -arch arm64 -w -rewrite-objc main.m 复制代码
我们看到main函数发生了变化:
-
原来的
age
变量:int age = 10; -
现在的
age
变量:__Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};。
int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10}; MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344)); ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); } return 0; } 复制代码
原来我们知道添加 __block说明符,我们就可以在block里面修改自动变量了。
恭喜你,现在你达到了第二层!__block说明符,其实会把自动变量包含到一个结构体中。
这也就解释了问题1为什么加入__block说明符,token可以正确拿到值。
MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344)); 复制代码
这次block初始化的过程中,把age
这个结构体传入到了block结构体中,现在就变成了指针引用。
struct __Block_byref_age_0 { void *__isa; //isa指针 __Block_byref_age_0 *__forwarding; // 指向自己的指针 int __flags; // 标记 int __size; // 结构体大小 int age; // 成员变量,存储age值 }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_age_0 *age; // 结构体指针引用 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; 复制代码
我们再来看看block中是如何修改age
对应的值:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_age_0 *age = __cself->age; // 通过结构体的self指针拿到age结构体的指针 (age->__forwarding->age) = 18; // 通过age结构体指针修改age值 } 复制代码
看到这里可能不明白__forwarding的作用,我们之后再讲。现在知道是age是指针引用修改成功的就可以了。
2.3 Block存储域
从C代码中我们可以看到Block的是指是Block结构体实例,__block变量实质是栈上__block变量结构体实例。从初始化函数中我们可以看到,impl.isa = &_NSConcreteStackBlock;
,即之前我们使用的是栈Block。
其实,Block有3中类型:
- _NSConcreteGlobalBlock类对象存储在程序的数据区(.data区)。
- _NSConcreteStackBlock类对象存储在栈上。
- _NSConcreteMallocBlock类对象存储在堆上。
void (^blk)(void) = ^{ NSLog(@"Global Block"); }; int main() { blk(); NSLog(@"%@",[blk class]);//打印:__NSGlobalBlock__ } 复制代码
全局Block肯定是存储在全局数据区的,但是在函数栈上创建的Block,如果没有捕获自动变量,Block的结构实例还是 _NSConcreteGlobalBlock,而不是 _NSConcreteStackBlock:
void (^blk0)(void) = ^{ // 没有截获自动变量的Block NSLog(@"Stack Block"); }; blk0(); NSLog(@"%@",[blk0 class]); // 打印:__NSGlobalBlock__ int i = 1; void (^blk1)(void) = ^{ // 截获自动变量i的Block NSLog(@"Capture:%d", i); }; blk1(); NSLog(@"%@",[blk1 class]); // 打印:__NSMallocBlock__ 复制代码
可以看到没有捕获自动变量的Block打印的类是NSGlobalBlock,表示存储在全局数据区。 但为什么捕获自动变量的Block打印的类却是设置在堆上的NSMallocBlock,而非栈上的NSStackBlock?这个问题稍后解释。
设置在栈上的Block,如果超出作用域,Block就会被释放。若 __block变量也配置在栈上,也会有被释放的问题。所以, copy方法调用时,__block变量也被复制到堆上,同时impl.isa = &_NSConcreteMallocBlock;
。复制之后,栈上 __block变量的__forwarding
指针会指向堆上的对象。因 此 __block变量无论被分配在栈上还是堆上都能够正确访问。
编译器如何判断何时需要进行copy操作呢?
在ARC开启时,自动判断进行 copy:
- 手动调用copy。
- 将Block作为函数参数返回值返回时,编译器会自动进行 copy。
- 将Block赋值给 copy修饰的id类或者Block类型成员变量,或者
__strong
修饰的自动变量。 - 方法名含有
usingBlock
的Cocoa
框架方法或GCD
相关API传递Block。
如果不能自动 copy,则需要我们手动调用 copy方法将其复制到堆上。比如向不包括上面提到的方法或函数的参数中传递Block时。
ARC环境下,返回一个对象时会先将该对象复制给一个临时实例指针,然后进行retain
操作,再返回对象指针。runtime/objc-arr.mm
提到,Block的retain
操作objc_retainBlock
函数实际上是Block_copy
函数。在实行retain
操作objc_retainBlock
后,栈上的Block会被复制到堆上,同时返回堆上的地址作为指针赋值给临时变量。
2.4 __block变量存储域
当Block从栈复制到堆上时候,__block变量也被复制到堆上并被Block持有。
- 若此时 __block变量已经在堆上,则被该Block持有。
- 若配置在堆上的Block被释放,则它所持有的 __block变量也会被释放。
__block int val = 0; void (^block)(void) = [^{ ++val; } copy]; ++val; block(); 复制代码
利用 copy操作,Block和 __block变量都从栈上被复制到了堆上。无论是{ ++val; }
还是++val;
都转换成了++(val->__forwarding->val);
。
Block中的变量val
为复制到堆上的 __block变量结构体实例,而Block外的变量val
则为复制前栈上的 __block变量结构体实例,但这个结构体的__forwarding
成员变量指向堆上的 __block变量结构体实例。所以,无论是是在Block内部还是外部使用 __block变量,都可以顺利访问同一个 __block变量。
3. 面试题C代码
下面我们看看面试题的C代码。
@interface Test : NSObject @end @implementation Test - (void)test_notification { NSNotificationCenter *__weak center = [NSNotificationCenter defaultCenter]; id __block token = [center addObserverForName:@"com.demo.perform.once" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { [self doSomething]; [center removeObserver:token]; token = nil; }]; } - (void)doSomething { } @end 复制代码
3.1 重写
在命令行中对这个文件进行一下处理,因为用到了 __weak说明符,需要额外指定一些参数:
xcrun -sdk iphoneos clang -arch arm64 -w -rewrite-objc -fobjc-arc -mios-version-min=8.0.0 -fobjc-runtime=ios-8.0.0 main.m 复制代码
这个会更复杂一些,但我们只看重要的部分:
struct __Block_byref_token_0 { void *__isa; __Block_byref_token_0 *__forwarding; int __flags; int __size; void (*__Block_byref_id_object_copy)(void*, void*); void (*__Block_byref_id_object_dispose)(void*); __strong id token; // id类型的token变量 (strong) }; struct __Test__test_notification_block_impl_0 { struct __block_impl impl; struct __Test__test_notification_block_desc_0* Desc; Test *const __strong self; // 被捕获的self (strong) NSNotificationCenter *__weak center; // center对象 (weak) __Block_byref_token_0 *token; // token结构体的指针 __Test__test_notification_block_impl_0(void *fp, struct __Test__test_notification_block_desc_0 *desc, Test *const __strong _self, NSNotificationCenter *__weak _center, __Block_byref_token_0 *_token, int flags=0) : self(_self), center(_center), token(_token->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; 复制代码
现在我们看到block
结构体 __Test__test_notification_block_impl_0中持有token
,同时之前我们看到token
也是持有block
的,所以造成了循环引用。
这也就回答了问题2。
下面我们看看block
的IMP函数是如何解决循环引用问题的:
static void __Test__test_notification_block_func_0(struct __Test__test_notification_block_impl_0 *__cself, NSNotification * _Nonnull __strong note) { __Block_byref_token_0 *token = __cself->token; // bound by ref Test *const __strong self = __cself->self; // bound by copy NSNotificationCenter *__weak center = __cself->center; // bound by copy ((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("doSomething")); ((void (*)(id, SEL, id _Nonnull __strong))(void *)objc_msgSend)((id)center, sel_registerName("removeObserver:"), (id)(token->__forwarding->token)); (token->__forwarding->token) = __null; } 复制代码
可以看到,token = nil;
被转换为了(token->__forwarding->token) = __null;
,相当于block
对象对token
的持有解除了!如果你觉得看不太明白,我再转换一下:
(__cself->token->__forwarding->token) = __null; // __cself为block结构体指针 复制代码
3.2 Block的类型
细心的同学可能发现:
impl.isa = &_NSConcreteStackBlock; 复制代码
这是一个栈类型的block
呀,声明周期结束不是就该被系统回收释放了么。我们使用了ARC同时我们调用是方法名中含有usingBlock
,会主动触发 copy操作,将其复制到堆上。
4. 总结
Block最常问的就是循环引用、内存泄露问题。
注意要点:
- __weak说明符的使用
- __block说明符的使用
- 谁持有谁
- 如何解除循环引用
另外,需要再强调一下的是:
-
面试题中的block代码如果一次都没有执行也是会内存泄露的!
-
可能有人会说使用
__weak typeof(self) wkSelf = self;
就可以解决self不释放的问题。确实这可以解决self不释放的问题,但是这里 仍然存在内存泄露! 我们还是需要从根上解决这个问题。
这篇关于一道Block面试题的深入挖掘的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值