【OC語法】block的本質(zhì)

目錄
一、block是什么
二、block的本質(zhì)
三、block的類型


一、block是什么


簡單地說,block跟Swift和Dart里的閉包(匿名函數(shù))差不多,我們都知道在Swift和Dart里函數(shù)是一等公民,所以函數(shù)可以賦值給變量、也可以作為函數(shù)的參數(shù)、也可以作為函數(shù)的返回值,也就是說一個函數(shù)可以傳來傳去。但是在OC里函數(shù)不是一等公民,它不能傳來傳去,不過OC又提供了一種特殊的對象那就是block,它就像一個匿名函數(shù)一樣可以可以賦值給變量、也可以作為函數(shù)的參數(shù)、也可以作為函數(shù)的返回值,但是要注意它本質(zhì)上不是一個函數(shù)而是一個OC對象。

本質(zhì)地說,block其實也是一個OC對象,所以它的內(nèi)存里存儲的是什么和普通OC對象差不多,只不過普通OC的對象用來包裝一些普通的數(shù)據(jù)(如字符串?dāng)?shù)據(jù)、數(shù)組數(shù)據(jù)、字典數(shù)據(jù)等),而block則用來包裝一段代碼以及這段代碼的調(diào)用環(huán)境。所謂包裝一段代碼,是指block內(nèi)部會把block的參數(shù)、返回值、執(zhí)行體封裝成一個函數(shù),并且存儲該函數(shù)的內(nèi)存地址;所謂包裝這段代碼的調(diào)用環(huán)境,是指block內(nèi)部會捕獲變量,并且存儲這些捕獲的變量。(這段話會在下一小節(jié)詳細證明)

  • block的聲明

block作為屬性時,這樣聲明:

// block的類型為:int (^)(int a, int b)
// block的名字為:block
@property (nonatomic, copy) int (^block)(int a, int b);

block作為方法的參數(shù)時,這樣聲明:

// block的類型為:void (^)(NSData *data, NSError *error)
// block的名字為:completionHandler
- (void)fecthDataWithCompletionHandler:(void (^)(NSData *data, NSError *error))completionHandler;

如果項目中使用了大量相同類型的block,那為了使代碼更簡潔,我們可以先typedef一下block的類型,然后再聲明。則上面兩例可以寫成這樣:

typedef int (^Block)(int a, int b);

@property (nonatomic, copy) Block block;
typedef void (^Block)(NSData *data, NSError *error);

- (void)fecthDataWithCompletionHandler:(Block)completionHandler;
  • block的實現(xiàn)

箭頭打頭就代表block的實現(xiàn),如果block沒有返回值,可省略returnType,如果block沒有參數(shù),可省略params。

^returnType(params) {
    
    // block的執(zhí)行體
};

不過通常我們都會把block的實現(xiàn)用一個變量記錄下來,以便將來調(diào)用,就像函數(shù)那樣。

returnType (^blockName)(params) = ^returnType(params) {
    
    // block的執(zhí)行體
};
  • block的調(diào)用

像C語言那樣加小括號就代表block的調(diào)用,如果block沒有返回值,則不接收返回值,如果block沒有參數(shù),可省略params。

returnType v = blockName(params);
  • block代碼的執(zhí)行順序

block代碼的執(zhí)行順序永遠都是:block的聲明 --> block的實現(xiàn) --> block的調(diào)用 --> 最后返回去真正去執(zhí)行block的實現(xiàn)代碼。舉例如下:

#import "ViewController.h"

@interface ViewController ()

// 第一步:block的聲明
@property (nonatomic, copy) int (^block)(int a, int b);

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 第二步:block的實現(xiàn)
    self.block = ^int(int a, int b) {
        
        // 第四步:最后返回去真正去執(zhí)行block的實現(xiàn)代碼
        return a + b;
    };
    
    // 第三步:block的調(diào)用
    int num = self.block(1, 2);
    NSLog(@"%d", num);
}

@end


二、block的本質(zhì)


我們簡單創(chuàng)建一個block,并調(diào)用它。

// 創(chuàng)建一個block
void (^block)(void) = ^{
    
    NSLog(@"11");
};

// 調(diào)用block
block();

接著用clang編譯器把這段OC代碼轉(zhuǎn)換成C/C++代碼,來窺探一下block的本質(zhì)。(偽代碼)

首先編譯器會把block轉(zhuǎn)換成它的本質(zhì)——即一個C++的__block_impl_0結(jié)構(gòu)體,該結(jié)構(gòu)體內(nèi)部有兩個成員變量,第一個成員變量內(nèi)部有一個isa指針指向block所屬的類,這也證明我們上面所說的“block其實也是一個OC對象”,還有一個FuncPtr指針指向該block對應(yīng)的函數(shù);第二個成員變量內(nèi)部則存儲著該block的一些描述信息,如該block的實際大小、copy函數(shù)、dispose函數(shù)等。此外,該結(jié)構(gòu)體內(nèi)部還有一個block構(gòu)造函數(shù),它用來創(chuàng)建并初始化一個block——即一個__block_impl_0類型的結(jié)構(gòu)體。當(dāng)然我們也不能忘了,block結(jié)構(gòu)體內(nèi)部還可以有更多的成員變量,它們就是block捕獲并存儲的變量,也就是我們上面所說的“包裝代碼的調(diào)用環(huán)境”。

// block的本質(zhì),是一個C++結(jié)構(gòu)體
struct __block_impl_0 {
    // 第一個成員變量
    struct __block_impl impl;
    // 第二個成員變量(這個不用太關(guān)心)
    struct __block_desc_0* Desc;
    
    /**
     * block的構(gòu)造函數(shù)
     *
     * @param fp block對應(yīng)函數(shù)的內(nèi)存地址
     * @param desc block描述信息結(jié)構(gòu)體的內(nèi)存地址
     *
     * @return 返回一個當(dāng)前類型的結(jié)構(gòu)體————即返回一個__block_impl_0類型的結(jié)構(gòu)體
     */
    __block_impl_0(void *fp, struct __block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock; // block所屬的類
        impl.Flags = flags;
        impl.FuncPtr = fp; // 把block對應(yīng)函數(shù)的內(nèi)存地址存儲在block內(nèi)部
        Desc = desc; // 把block描述信息結(jié)構(gòu)體的內(nèi)存地址存儲在block內(nèi)部
    }
};

// block實現(xiàn)信息結(jié)構(gòu)體
struct __block_impl {
    void *isa; // 指向block所屬的類
    int Flags;
    int Reserved;
    void *FuncPtr; // 指向block對應(yīng)的函數(shù)
};

// block描述信息結(jié)構(gòu)體
struct __block_desc_0 {
  size_t reserved; // 預(yù)留
  size_t Block_size; // block的實際大小
};

然后編譯器會把block的參數(shù)、返回值、執(zhí)行體封裝成一個__block_func_0函數(shù),也就是我們上面所說的“包裝一段代碼”,將來創(chuàng)建block的時候,block內(nèi)部的FuncPtr指針就會指向這個函數(shù),并把block的描述信息封裝進一個__block_desc_0結(jié)構(gòu)體(這個不用太關(guān)心)。

// block對應(yīng)的函數(shù)
void __block_func_0(struct __block_impl_0 *__cself) {
    
    NSLog("11");
}

// block對應(yīng)的描述信息
struct __block_desc_0 {
  size_t reserved;
  size_t Block_size; // block的實際大小
} __block_desc_0_DATA = {
    0,
    sizeof(struct __block_impl_0) // 計算block的實際大小
};

知道了這些,上面那幾行OC代碼其實就對應(yīng)著下面這幾行C/C++代碼,創(chuàng)建一個block的本質(zhì)就是調(diào)用block的構(gòu)造方法創(chuàng)建一個結(jié)構(gòu)體,構(gòu)造方法會接收block對應(yīng)的函數(shù)的地址和block對應(yīng)的描述信息的地址作為參數(shù),接收后會存儲在它內(nèi)部,然后把這個結(jié)構(gòu)體的內(nèi)存地址賦值給*block這里的block指針變量;而調(diào)用block的本質(zhì)就是找到block內(nèi)部FuncPtr指針指向的函數(shù)來調(diào)用。

// 創(chuàng)建一個block
void (*block)(void) = &__block_impl_0(
                                      __block_func_0,// 把函數(shù)的地址傳進去
                                      &__block_desc_0_DATA // 把結(jié)構(gòu)體的地址傳進去
                                      );

// 調(diào)用block
block->impl.FuncPtr(block);


三、block的類型


1、全局block、棧block、堆block

(注意:這一小節(jié)的示例代碼都是在MRC下的)

block有三種類型:全局block(__NSGlobalBlock__)、棧block(__NSStackBlock__)、堆block(__NSMallocBlock__),它們都繼承自NSBlock,并最終繼承自NSObject。那什么是全局block?什么是棧block?什么又是堆block?全局block是指存儲在全局區(qū)的block,棧block是指存儲在棧區(qū)的block,堆block是指存儲在堆區(qū)的block,所以說看一個block是什么類型,不是看它在代碼的什么位置定義的,而是看它存儲在哪塊內(nèi)存分區(qū)中——即系統(tǒng)把它存儲在哪塊內(nèi)存分區(qū)中了。這在代碼中有什么體現(xiàn)呢?也就是說我們?nèi)绾瓮ㄟ^代碼一眼就能知道這個block是什么類型的呢?

  • 全局block

沒有訪問外界普通局部變量的block就是全局block,系統(tǒng)會把這樣的block放在全局區(qū)。

// 普通全局變量
//int age = 25;
// 靜態(tài)全局變量
//static int age = 25;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 靜態(tài)局部變量
        static int age = 25;
        
        void (^block)(void) = ^{
            // 訪問外界的變量
            NSLog(@"%d", age);
        };
        
        NSLog(@"%@", [block class]); // __NSGlobalBlock__
        NSLog(@"%@", [[block class] superclass]); // __NSGlobalBlock
        NSLog(@"%@", [[[block class] superclass] superclass]); // NSBlock
        NSLog(@"%@", [[[[block class] superclass] superclass] superclass]); // NSObject
    }
    return 0;
}
  • 棧block

訪問了外界普通局部變量的block就是棧block,系統(tǒng)會把這樣的block放在棧區(qū),可見棧block和全局block是完全對立的。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 普通局部變量
        int age = 25;
        
        void (^block)(void) = ^{
            // 訪問外界的變量
            NSLog(@"%d", age);
        };
        
        NSLog(@"%@", [block class]); // __NSStackBlock__
        NSLog(@"%@", [[block class] superclass]); // __NSStackBlock
        NSLog(@"%@", [[[block class] superclass] superclass]); // NSBlock
        NSLog(@"%@", [[[[block class] superclass] superclass] superclass]); // NSObject
    }
    return 0;
}
  • 堆block

對棧block執(zhí)行一下copy操作,copy方法返回的就是一個堆block,所以說堆block就是把棧block copy了一份到堆區(qū)。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 普通局部變量
        int age = 25;
        
        void (^block)(void) = [^{
            // 訪問外界的變量
            NSLog(@"%d", age);
        } copy]; // 需適時的[block release]一下
        
        NSLog(@"%@", [block class]); // __NSMallocBlock__
        NSLog(@"%@", [[block class] superclass]); // __NSMallocBlock
        NSLog(@"%@", [[[block class] superclass] superclass]); // NSBlock
        NSLog(@"%@", [[[[block class] superclass] superclass] superclass]); // NSObject
    }
    return 0;
}

你會發(fā)現(xiàn)在平常的開發(fā)中,我們用到的總是堆block,而不是棧block。這是因為在ARC下我們使用block的時候,系統(tǒng)很多情況下都會自動幫我們復(fù)制一份棧block到堆區(qū),而MRC下則需要我們手動調(diào)用copy方法讓系統(tǒng)復(fù)制一份棧block到堆區(qū)。那為什么非要復(fù)制一份棧block到堆區(qū)?棧block有什么問題嗎?

void (^block)(void);
void test() {
    
    // 普通局部變量
    int age = 25;
    
    block = ^{
        // 訪問外界的變量
        NSLog(@"%d", age); // -272632440,不是25
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        test();
        block();
    }
    return 0;
}

試看上面這段代碼,我們定義了一個block,并且它的實現(xiàn)部分訪問了外界的普通局部變量,所以它是一個棧block。而我們知道棧內(nèi)存是由系統(tǒng)自己管理的,在出了相應(yīng)的作用域后棧內(nèi)存就會自動釋放,可以供別人使用了,你的數(shù)據(jù)有可能就被別人替換掉。那么test函數(shù)執(zhí)行完后,block超出作用域,已經(jīng)被系統(tǒng)釋放掉了,此時雖然說我們還能通過block這個全局變量去訪問那塊內(nèi)存,但那塊內(nèi)存里存的很有可能已經(jīng)是別人的數(shù)據(jù)了,所以block()這個調(diào)用本身其實已經(jīng)沒有意義了,可以根據(jù)block的執(zhí)行順序去分析一下這段代碼。

總結(jié)一下:為什么要把棧block到copy到堆區(qū)?

block剛被創(chuàng)建出來時,若不是全局block就是棧block,而棧內(nèi)存又是系統(tǒng)自動管理的,一旦超出變量的作用域,變量對應(yīng)的內(nèi)存就會被釋放,所以如果不把棧block復(fù)制到堆區(qū),就很有可能我們在調(diào)用棧block的時候它已經(jīng)被銷毀了,也就是說block內(nèi)存里的數(shù)據(jù)已經(jīng)都是垃圾數(shù)據(jù)了,即便能調(diào)用成功那也是毫無意義地調(diào)用,會導(dǎo)致數(shù)據(jù)錯亂。

額外的考慮,拿來玩兒

上面我們知道了對棧block執(zhí)行copy操作是在堆區(qū)復(fù)制出了一個新的block,那對全局block和堆block執(zhí)行copy操作呢?

  • 全局block執(zhí)行copy操作
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 靜態(tài)局部變量
        static int age = 25;
        
        void (^block)(void) = ^{
            // 訪問外界的變量
            NSLog(@"%d", age);
        };
        
        NSLog(@"%p", block); // 0x100001060
        NSLog(@"%p", [block copy]); // 0x100001060
    }
    return 0;
}
  • 堆block執(zhí)行copy操作
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 普通局部變量
        int age = 25;
        
        void (^block)(void) = [^{
            // 訪問外界的變量
            NSLog(@"%d", age);
        } copy];
        
        NSLog(@"%p", block); // 0x102005650
        NSLog(@"%p", [block copy]); // 0x102005650
    }
    return 0;
}

可見:

block類型 執(zhí)行copy操作后的效果
全局block 什么也不做,不會產(chǎn)生新的block,舊block的引用計數(shù)也不會加1,因為內(nèi)存是放在全局區(qū)的,生命周期跟App同步,你用就行了,不必增加引用計數(shù)
棧block 復(fù)制一份棧block到堆區(qū)
堆block 僅僅是block的引用計數(shù)加1,不會產(chǎn)生新的block

2、ARC下系統(tǒng)會在某些情況下自動copy一份棧block到堆區(qū)

(注意:這一小節(jié)的示例代碼都是在ARC下的)

  • block賦值給一個強指針時(即__strong修飾的指針),系統(tǒng)會自動把該棧block復(fù)制到堆區(qū),可以理解為Swift和Dart里函數(shù)復(fù)制給變量時
typedef void(^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 25;
        
        Block block = ^{

            NSLog(@"%d", age);
        };
        
        // 等價于
//      __strong Block block = ^{
//
//            NSLog(@"%d", age);
//        };

        NSLog(@"%@", [block class]); // __NSMallocBlock__,是一個堆block
    }
    return 0;
}

上面代碼中block變量是個強指針,等號右邊的block本來是個棧block,但是在賦值給強指針時系統(tǒng)會自動把該棧block復(fù)制到堆區(qū),所以就返回了一個堆block。當(dāng)然如果我們把block變量變成弱指針,那block自然就還是棧block,系統(tǒng)不會自動復(fù)制了。

typedef void(^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 25;
                 
        __weak Block block = ^{

            NSLog(@"%d", age);
        };

        NSLog(@"%@", [block class]);// __NSStackBlock__,是一個棧block
    }
    return 0;
}
  • block作為函數(shù)的參數(shù)時,系統(tǒng)會自動把該棧block復(fù)制到堆區(qū),可以理解為Swift和Dart里函數(shù)作為函數(shù)的參數(shù)時
typedef void(^Block)(void);
void test(Block block) {
    
    NSLog(@"%@", [block class]); // __NSMallocBlock__,是一個堆block
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 25;
        
        Block block = ^{
            
            NSLog(@"%d", age);
        };
        
        test(block);
    }
    return 0;
}
  • block作為函數(shù)的返回值時,系統(tǒng)會自動把該棧block復(fù)制到堆區(qū),可以理解為Swift和Dart里函數(shù)作為函數(shù)的返回值時
typedef void(^Block)(void);
Block test() {
    int age = 25;
    
    Block block = ^{
        
        NSLog(@"%d", age);
    };
    
    return block;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSLog(@"%@", [test() class]);// __NSMallocBlock__,是一個堆block
    }
    return 0;
}

上面代碼中test函數(shù)返回的block本來是個棧block,但是在作為函數(shù)的返回值系統(tǒng)會自動把該棧block復(fù)制到堆區(qū),所以調(diào)用test函數(shù)時就得到了一個堆block。

  • GCD方法里的block,系統(tǒng)都會自動復(fù)制到堆區(qū)
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    
});

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    
    
});

// 等等......
  • Foundation框架usingBlock方法里的block,系統(tǒng)都會自動復(fù)制到堆區(qū)
[[NSArray alloc] enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    
    
}];

[[[NSDictionary alloc] init] enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
    
    
}];

// 等等......
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容