一道Block面试题的深刻挖掘

0. 序言

最近看到了一道Block的面试题,还蛮有意思的,来给你们分享一下。ios

本文从一道Block面试题出发,层层深刻到达Block原理的讲解,把面试题吃得透透的。面试

题外话:objective-c

不少人以为Block的定义很怪异,很难记住。但其实和C语言的函数指针的定义对比一下,你很容易就能够记住。架构

// Block
returnType (^blockName)(parameterTypes)

// 函数指针
returnType (*c_func)(parameterTypes)
复制代码

例如输入和返回参数都是字符串:框架

(char *) (*c_func)(const char *);
(NSString *) (^block)(NSString *);
复制代码

好了,下面正式开始~iphone

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 {
    
}
复制代码

答案是存在flex

1.1.1 分析
  • block中,咱们使用到的外部变量有selfcentercenter使用了__weak说明符确定没问题。ui

  • center持有tokentoken持有blockblock持有self,也就是说token不释放,self确定无法释放。spa

  • 咱们注意到[center removeObserver:token];这步会把tokencenter中移除掉。按理说,centerself是否是就能够被释放了呢?

咱们来看看编译器怎么说:

编译器告诉咱们,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的。

那么,为何还会泄露呢?

由于,虽然centertoken的持有已经没有了,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被正确释放了。这一步,解除了tokenblock之间的循环引用,因此正确释放了。

有人可能会说:

使用__weak typeof(self) wkSelf = self;就能够解决self不释放的问题。

确实这能够解决self不释放的问题,可是这里仍然存在内存泄露!

2. Block的原理

虽然面试题解决了,可是还有几个问题没有弄清楚:

  • 为何没有__block说明符token未被初始化,而有这个说明符以后就没问题了呢?
  • tokenblock为何会造成循环引用呢?

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修饰的自动变量。
  • 方法名含有usingBlockCocoa框架方法或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变量存储域

__forwarding

当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。

下面咱们看看blockIMP函数是如何解决循环引用问题的:

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不释放的问题,可是这里 仍然存在内存泄露! 咱们仍是须要从根上解决这个问题。

补充:

上面讲的时候集中在说tokenblock的循环引用,ViewController的问题我简单带过了,可能同窗们看的时候没有注意到。

我在这里专门拎出来讲一下:

tokenblock循环引用,同时block持有self(ViewController),致使ViewController也无法释放。

若是但愿优先释放ViewController(无论block是否执行),最好给ViewController加上__weak说明符。

此外,破除tokenblock的循环引用,实际有两种方法:

  • 手动设置token = nil;
  • token也使用__weak说明符id __block __weak token

注意:

如下说法不够严谨,也可能存在问题:

最简单粗暴的解决办法:你们都__weak

NSNotificationCenter *__weak wkCenter = [NSNotificationCenter >defaultCenter];
__weak typeof(self) wkSelf = self;
id __block __weak wkToken = [wkCenter addObserverForName:UIApplicationDidEnterBackgroundNotification
                                      object:nil
                                       queue:[NSOperationQueue mainQueue]
                                  usingBlock:^(NSNotification * _Nonnull note) {
   [wkSelf doSomething];
   [wkCenter removeObserver:wkToken];
}];
复制代码

这个问题具体要看NSNotificationCenter具体是怎么实现的。token使用__weak说明符,可是若是NSNotificationCenter没有持有token,在函数做用域结束时,token会被销毁。虽然不会有循环引用问题,可是可能致使没法移除这个观察者的问题。

若是以为本文对你有所帮助,给我点个赞吧~

相关文章
相关标签/搜索