一道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會被銷燬。雖然不會有循環引用問題,可是可能致使沒法移除這個觀察者的問題。

若是以爲本文對你有所幫助,給我點個贊吧~

相關文章
相關標籤/搜索