面試遇到block的次日-類型和__block

經過上一篇文章對block本質的分析,咱們能夠了解到,block的本質就是一個OC對象,擁有一個isa指針,那麼block就確定有本身的類型。上一篇文章中經過C++代碼咱們也看到了,isa指向一個&_NSConcreteStackBlock,本文就繼續分析block的類型,繼承鏈等知識點。bash

繼承關係

block有三種類型,能夠經過isa或者調用class方法獲取,最終都繼承自NSObject. 下面咱們經過class獲取到block的類型函數

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
       
        void(^myBlock)(void) = ^{
            
            NSLog(@"this is a block ");
        };
        
        NSLog(@"%@",[myBlock class]);
        NSLog(@"%@",[[myBlock class] superclass]);
        NSLog(@"%@",[[[myBlock class] superclass] superclass]);
        NSLog(@"%@",[[[[myBlock class] superclass] superclass] superclass]);
        
    }
    return 0;
}

複製代碼

結果以下,也更加印證了block是一個OC對象post

block[12750:370967] __NSGlobalBlock__
block[12750:370967] __NSGlobalBlock
block[12750:370967] NSBlock
block[12750:370967] NSObject
複製代碼

block類型

上面咱們分析出了block都繼承自NSBlock,最終繼承自NSObject,下面繼續看看block的三種類型學習

  • NSGlobalBlock
  • NSMallocBlock
  • NSStackBlock 仍是經過class的方式獲取block的類型
void (^block1)(void) = ^{
            NSLog(@"this is a block");
        };
        
        int age = 10;
        void (^block2)(void) = ^{
            NSLog(@"this is a block %d",age);
        };
        
        NSLog(@"%@ --- %@ --- %@",[block1 class],[block2 class],[^{
            NSLog(@"this is a block %d",age);
        } class]);
複製代碼

輸出: __NSGlobalBlock__ --- __NSMallocBlock__ --- __NSStackBlock__
可是咱們仍是須要編輯成C++代碼再看一下isa指向的類型,就不貼代碼了,直接說結果,都是_NSConcreteStackBlock;測試

這裏要說一下一切以運行時的結果類型爲準,由於clang這個命令編譯的結果可能跟咱們最初寫的代碼有些出入,只能做爲學習的參考。ui

內存分佈

一圖勝前言 this

  • 編寫的代碼都放在程序區域spa

  • 全局變量通常放在數據區域3d

  • 程序區域和數據區域 都是編譯器自動處理的區域指針

  • 堆:動態分配內存 好比[NSObject alloc] malloc(20) 須要開發者申請內存頁須要本身管理內存

  • 棧:局部變量 參數 系統自動分配內存,自動釋放內存

  • NSGlobalBlock就放在數據區域,NSMallocBlock放在堆區 ,NSStackBlock放在棧區

那麼咱們開發中寫的不少block,咱們怎麼知道他是什麼類型的block呢?

下面總結幾點判斷的規則:

  1. NSGlobalBlock:沒有訪問auto變量就是NSGlobalBlock,對NSGlobalBlock進行copy操做依然是NSGlobalBlock
  2. NSStackBlock:訪問了auto變量就是NSStackBlock(須要在MRC環境測試,由於ARC環境編譯器在打印的時候幫咱們作了copy操做)
  3. NSMallocBlock:NSStackBlock作了copy操做就是NSMallocBlock

爲何上面要強調一下copy操做呢,由於在棧上的block調用copy以後會從棧賦值到堆上,在堆上的block再copy,則會引用計數加1,而在數據區的NSGlobalBlock調用block則不會有任何操做。

block的copy

上面內存分部中說過NSStackBlock作了copy操做就是NSMallocBlock,在ARC下,系統會根據狀況自動把棧上的block copy到堆上,那是什麼狀況系統會作這件事呢?下面舉例說一說

  1. block做爲函數的返回值
typedef void (^MyBlock)(void);
MyBlock returnBlock() {
    int age = 20;
    //自動調用copy
    return ^{
        NSLog(@"----- %d",age);
    };
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyBlock block = returnBlock();
        block();
        NSLog(@"%@",[block class]);//NSMallocBlock
    }
    return 0;
}
複製代碼

根據咱們上面分析的結果,調用了auto變量,是一個NSStackBlock,可是最後打印的結果是一個NSMallocBlock,說明編譯器幫咱們作了一次copy操做,固然在ARC下咱們也不須要關心release,在做用域結束,編譯器也幫咱們作了release操做。

  1. block賦值給__strong指針
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 20;
        MyBlock block = ^{
            NSLog(@"----- %d",age);
        };
        block();
        NSLog(@"%@",[^{
            NSLog(@"----- %d",age);
        } class]);//NSStackBlock
        NSLog(@"%@",[block class]); //NSMallocBlock
    }
    return 0;
}
複製代碼
  1. API中方法名含有usingBlock的方法
NSArray *array = [NSArray array];
        [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            
        }];
複製代碼
  1. GCD中的block
static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            <#code to be executed once#>
        });
複製代碼

對象類型的auto變量

總結一下:經過表面看本質,棧區的block是不會對外面的變量執行retain操做的(MRC),或者說不會對外面變量發生強引用(ARC),只有堆區的block纔有可能持有變量。

__block修飾符

說到__block這個修飾符,你們開發中必定是用過不少次了,我們先從爲何要用開始提及,再說__block加了這個修飾詞以後到底發生了什麼。

爲何要用__block呢?最多見的一個使用就是在block裏面想修改block外部的局部變量,前面咱們也已經分析過了,auto變量是經過值傳遞,被記錄在block的結構體中的,並且是跨函數訪問,咱們如何能在block執行的函數中,修改另一個函數中的auto變量呢,固然是改不了的。而後咱們又知道了只要用__block修飾這個想改的auto變量,咱們就能夠在block中修改他的值了,那麼這裏就能夠想想__block這個修飾詞究竟是作了什麼呢?也許他就是把以前的值傳遞改爲了地址傳遞呢?

修改auto變量

想要修改auto變量,咱們先說一種不是使用__block的方法

  1. static修飾 前面咱們也分析過static修飾的變量被block捕獲是地址傳遞,只要是地址傳遞咱們能拿到地址,就能夠根據地址修改對應內存中的內容,能夠確定,static修飾,是能夠修改auto變量的
  2. 全局變量 就更不用說了,誰均可以改,都不用經過block去捕獲
  3. __block 除了上面兩種方法(可能會有內存問題,咱們通常不用),__block纔是咱們最優的一種解決方法

__block原理

仍是經過clang編譯成C++代碼區分析__block的原理,先寫一段測試代碼

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int age = 20;
        MyBlock block = ^{
            age = 33;
            NSLog(@"----- %d",age);
        };
        block();
       
        
    }
    return 0;
}
複製代碼

__block int age = 20;最終被編譯成了

__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 20};
複製代碼

其實這裏能夠理解爲,添加了__block修飾以後的age,被編譯成了一個__Block_byref_age_0對象

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};
複製代碼

其中__forwarding被賦值了&age,也就是__Block_byref_age_0對象自己的地址,age的值(20)也被存儲在了這個對象中。而後就是block中修改age的值,並打印age,使用age->__forwarding->age取到age的值

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_age_0 *age = __cself->age; // bound by ref

            (age->__forwarding->age) = 33;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_86_ljphgvys0hzg8hfxdtv5q5gm0000gn_T_main_76267f_mi_0,(age->__forwarding->age));
        }
複製代碼

到這裏咱們應該已經清楚了,爲何__block修飾的變量在block中能夠被修改,其實說白了,仍是變相的實現了地址的傳遞,只有拿到地址才能夠改變相應地址指向的內存區域中的數據。

下面再說一個block中使用array的小問題:

在block中調用 addObject編譯不會報錯,可是給 array賦值成 nil就報錯,這是爲何呢?

先說賦值nil報錯,根據上面的分析,由於咱們並無用__block修飾array,block中不能修改auto對象,因此報錯了。

再說addObject爲何不會報錯,由於addObject其實並非去改變array的值,只是去操做array,而block只是不能修改auto變量,並非不能操做變量,因此不會報錯。

相關文章
相關標籤/搜索