Block原理解析

Block語(yǔ)法簡(jiǎn)介

Block:可以理解為帶有自動(dòng)變量值的匿名函數(shù)。

Blocks提供了類似由C++和Objective-C類生成實(shí)例變量或?qū)ο髞?lái)保持變量值的方法。

Block語(yǔ)法定義
^返回值 類型參數(shù)列表 表達(dá)式

^void (int event) { NSLog(@"。。。");}

Block類型變量定義
int (^blk)(int); 可以認(rèn)為是匿名函數(shù)的地址,但是實(shí)際上它是是被看成對(duì)象來(lái)操作的,有自己的isa指針。

簡(jiǎn)單的Block原理分析

我們來(lái)分析最簡(jiǎn)單的block:我們定了一個(gè)變量名稱為blk的Block變量,在定義部分省略了返回值和類型參數(shù)列表,然后在下面調(diào)用它,打出一串”Block”;


void (^blk)(void) = ^{printf("Block\n");};
blk();

源碼通過(guò)clang,去掉一些類型轉(zhuǎn)換我們可以得到以下代碼


struct _block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
//Block定義的結(jié)構(gòu)體
struct __main_block_impl_0 {
struct _block_impl impl;
struct __main_block_desc_0 *Desc;
__main_block_impl_0(void *fp,struct __main_block_desc_0 *desc, int flags=0)
{
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//我們的Block里面的函數(shù)
static void __main_block_func_0(struct __main_block_impl_0 *_cself)
{
printf("Block\n");
}
//存儲(chǔ)block的其他信息,大小等
struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
} __mainBlock_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
/*以下是我們的代碼部分*/
//賦值部分,
struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0,&__mainBlock_desc_0_DATA);
struct __main_block_impl_0 *blk = &temp;
//調(diào)用部分
(*blk->impl.FuncPtr)(blk);
/*以下是我們的代碼部分*/

看起來(lái)好像很麻煩?居然兩句代碼變出了這么多代碼。慢慢分析起來(lái)其實(shí)也不難理解

C++中,struct 約等于 class,唯一差別是struct中的默認(rèn)成員屬性是public的。class中的默認(rèn)成員屬性是private的。所以struct也可以擁有變量和函數(shù)。

首先系統(tǒng)自動(dòng)給我們生成了三個(gè)結(jié)構(gòu)體。


//block的結(jié)構(gòu)體定義
struct __main_block_impl_0 {
struct _block_impl impl;//Block isa ,函數(shù)地址等定義
struct __main_block_desc_0 *Desc;//Block size等信息定義
};
struct _block_impl {
void *isa;//所屬的類
int Flags;
int Reserved;
void *FuncPtr;//函數(shù)地址
};
struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
} 

生成了兩個(gè)函數(shù)


//Block信息初始化的函數(shù)
__main_block_impl_0(void *fp,struct __main_block_desc_0 *desc, int flags=0)
{
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
//我們的Block里面的函數(shù),_cself就是調(diào)用這個(gè)函數(shù)的調(diào)用者的指針
static void __main_block_func_0(struct __main_block_impl_0 *_cself)
{
printf("Block\n");
}

我們定義Block的代碼如下


void (^blk)(void) = ^{printf("Block\n");};

轉(zhuǎn)化成


/*
初始化
__main_block_func_0:函數(shù)地址,
__mainBlock_desc_0_DATA:block的size信息
*/
struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0,&__mainBlock_desc_0_DATA);
struct __main_block_impl_0 *blk = &temp;

定義了一個(gè)main_block_impl_0的block,初始化函數(shù)為main_block_impl_0,傳入函數(shù)指針和block的大小等信息


//Block信息初始化的函數(shù)
__main_block_impl_0(void *fp,struct __main_block_desc_0 *desc, int flags=0)
{
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}

我們看到這個(gè)block的class類型為_(kāi)NSConcreteStackBlock,這個(gè)下面會(huì)詳細(xì)解釋。函數(shù)地址為FuncPtr。所以iOS里的Block是被當(dāng)成一個(gè)類來(lái)看待的,有自己的存儲(chǔ)空間??梢岳斫鉃?strong>帶有自動(dòng)變量值的匿名函數(shù)

我們調(diào)用block的代碼如下


//調(diào)用部分,
blk();

轉(zhuǎn)化成


//調(diào)用部分
(*blk->impl.FuncPtr)(blk);

拿到上面定義的Block變量blk,找到函數(shù)地址,調(diào)用函數(shù),并把調(diào)用者也就是blk傳遞進(jìn)去。

Block會(huì)截獲自動(dòng)變量


int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{printf(fmt,val);};
val = 2;
fmt = "these values were changed. val = %d\n";
blk();

輸出為輸出

val = 10

而不是

these values were changed. val = 2

說(shuō)明自動(dòng)變量截獲只能保存執(zhí)行block語(yǔ)法瞬間的值

但我們知道加上__block,是可以在Block內(nèi)部對(duì)變量進(jìn)行修改的。詳細(xì)講__block(__block storage-class-specifier)為存儲(chǔ)類型說(shuō)明符,

c語(yǔ)言有以下說(shuō)明符:

  • tydedef
  • extern
  • static:表示靜態(tài)變量存儲(chǔ)在數(shù)據(jù)區(qū)
  • auto:表示自動(dòng)變量存儲(chǔ)在棧
  • register:應(yīng)將其保存在CPU的寄存器中(而不是棧或堆)

__block類似于后三種,表示將變量值設(shè)置到哪個(gè)存儲(chǔ)區(qū)
如果我們加上__block


__block int val = 10;
void (^blk)(void) = ^{val=1;};

進(jìn)行編譯后,并剔除和以上通過(guò)clang一樣的部分,我們看到以下不同


struct __Block_byref_val_0 {
void *_isa;
__Block_byref_val_0 *_forwarding;
int __flags;
int __size;
int __val;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val;
}   

我們發(fā)現(xiàn)val變量居然變成了結(jié)構(gòu)體實(shí)例__Block_byref_val_0,既在棧上生成了__Block_byref_val_0結(jié)構(gòu)體實(shí)例,且初始化為10

^{val=1;}賦值過(guò)程變成什么樣子了呢


static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val;
(val->__forwarding->val) = 1;
}

找到Block下面的val變量,拿出val變量的__forwarding指向的val變量,拿出val變量下的val值賦值。如果Block此時(shí)在棧區(qū),那么__forwarding指向val變量自己。

image

copy到堆后:__forwarding指向堆區(qū)的val變量

image

Block的類型

  • _NSConcreteStackBlock: 存儲(chǔ)在棧區(qū),需要截取變量
  • _NSConcreteGlobalBlock: 1.存儲(chǔ)在程序數(shù)據(jù)區(qū)域,2.不需要截取變量
  • _NSConcreteMallocBlock: 存儲(chǔ)在堆區(qū)

根據(jù)之前的分析,我們看到block的isa指針為_(kāi)NSConcreteStackBlock,里面有個(gè)Stack,可以猜到,這個(gè)為存儲(chǔ)在棧區(qū)的Block。

我們?cè)谟浭鋈肿兞康牡胤绞褂肂lock語(yǔ)法時(shí)候,生成的Block為_(kāi)NSConcreteGlobalBlock,因?yàn)樵谑褂萌肿兞康牡胤讲荒苁褂米詣?dòng)變量,所以不存在對(duì)自動(dòng)變量的截獲。


void (^blk)(void) = ^{printf("global Block");};
int main(){
}

那如何使得棧上的Block到堆上呢?

ARC 條件下編譯器會(huì)適當(dāng)判斷,自動(dòng)生成將block從棧上復(fù)制到堆上的代碼。


//比如
typedef int (^blk_t)(int);
blk_t func(int rate) {
return ^(int count){return rate * count;};
}

該代碼返回設(shè)置在棧上的Block函數(shù)。但函數(shù)作用域結(jié)束,棧上的Block被廢棄。但編譯器自動(dòng)會(huì)加上copy

什么情況下編譯器不能進(jìn)行判斷要不要加copy,而需要手動(dòng)執(zhí)行copy?

  1. 向方法或者函數(shù)參數(shù)中傳遞block;
  2. 如果在函數(shù)或者方法中已經(jīng)copy了傳遞過(guò)來(lái)的參數(shù)(Cocoa框架的方法且方法名中含有usingBlock,GCD的API)

例:
在用 如NSArray 的 enumerateObjectsUsingBlock 的實(shí)例方法和 dispatch_async函數(shù)前,不用手動(dòng)copy。

在用如NSArray的 initWithObjects 前,需要手動(dòng)copy。


typedef void (^blk_t)(void);
NSArray *blocks = [self getBlockArray];
blk_t blk = (blk_t)[blocks objectAtIndex:0];
blk();
- (NSArray *)getBlockArray {
int val = 0;
return [NSArray arrayWithObjects: ^{NSLog(@"blk0:%d",val);},
^{NSLog(@"blk0:%d",val);}, nil];
}
//會(huì)發(fā)生崩潰。因?yàn)镹SArray 的initWithObjects因?yàn)橄到y(tǒng)不確定加入的是不是block,不會(huì)自動(dòng)執(zhí)行copy操作,如果我們也不執(zhí)行,在作用域外調(diào)用就會(huì)發(fā)生崩潰。

也許你會(huì)想,那么任何時(shí)候都用copy就好啦。但是從棧上的block copy到堆上很耗CPU。所以最好自己判斷需不需要把Blockcopy到棧上

綜上:我們要想把棧上的Block復(fù)制到堆上,只有執(zhí)行copy方法,有些情況下,系統(tǒng)會(huì)自動(dòng)幫我們執(zhí)行,但也有些情況我們需要手動(dòng)執(zhí)行copy。

棧上的Block被復(fù)制到堆的情況

  • 手動(dòng)調(diào)用Block的copy實(shí)例方法
  • Block作為函數(shù)返回值返回
  • 將block賦值給附有__strong修飾符id類型的類或Block類型的成員變量。
  • 如果在函數(shù)或者方法中已經(jīng)copy了傳遞過(guò)來(lái)的參數(shù)(Cocoa框架的方法且方法名中含有usingBlock,GCD的API)

注: __weak, __strong 用來(lái)修飾變量,此外還有 __unsafe_unretained, __autoreleasing 都是用來(lái)修飾變量的。
__strong 是缺省的關(guān)鍵詞。
__weak 聲明了一個(gè)可以自動(dòng) nil 化的弱引用。
__unsafe_unretained 聲明一個(gè)弱應(yīng)用,但是不會(huì)自動(dòng)nil化,也就是說(shuō),如果所指向的內(nèi)存區(qū)域被釋放了,這個(gè)指針就是一個(gè)野指針了。
__autoreleasing 用來(lái)修飾一個(gè)函數(shù)的參數(shù),這個(gè)參數(shù)會(huì)在函數(shù)返回的時(shí)候被自動(dòng)釋放。

各種類型的Block調(diào)用copy后

Block類型 存儲(chǔ)區(qū)域 賦值效果
_NSConcreteStackBlock 棧-》堆
_NSConcreteGlobalBlock 程序數(shù)據(jù)區(qū)域 什么也不做
_NSConcreteMallocBlock 引用計(jì)數(shù)+1

所以不管任何時(shí)候copy方法復(fù)制都不會(huì)出錯(cuò)。但是多次調(diào)用copy會(huì)不會(huì)引起內(nèi)存釋放問(wèn)題呢?


//多次調(diào)用copy
blk = [[[[blk copy] copy] copy] copy];
//代碼解釋
{
/*
將配置在棧上的Block賦值給blk變量。
*/
blk_t temp = [blk copy];
/*
將配置在堆上的block賦值給tmp變量,temp強(qiáng)持有Block
*/
blk = temp;
/*
將變量tmp的Block賦值為變量blk,blk強(qiáng)持有Block
此時(shí)block的持有者為變量temp和blk;
*/
}
/*
由于變量作用域結(jié)束,所以變量temp被廢棄,其強(qiáng)引用失效并釋放所持有的Block
由于Block的此時(shí)還被blk持有,所以沒(méi)有廢棄。
*/
{
/*
配置在堆上的Block被賦值給blk;同時(shí)變量blk持有強(qiáng)制引用的Block
*/
blk_t temp = [blk copy];
/*
將配置在堆上的block賦值給tmp變量,temp強(qiáng)持有Block
*/
blk = temp;
/*
將變量tmp的Block賦值為變量blk,blk強(qiáng)持有Block
此時(shí)block的持有者為變量temp和blk;
*/
}
/*
由于變量作用域結(jié)束,所以變量temp被廢棄,其強(qiáng)引用失效并釋放所持有的Block
由于Block的此時(shí)還被blk持有,所以沒(méi)有廢棄。
*/
/*下面重復(fù)*/

答案是 :多次調(diào)用copy完全不會(huì)有任何問(wèn)題

一個(gè)含有__block變量的block被copy

__block變量的配置存儲(chǔ)域 Block從棧賦值到堆時(shí)候的影響
從棧賦值到堆并被Block持有
被Block持有

我們看看以下代碼,一個(gè)在棧上的Block

__block int val = 0;
void (^blk)(void) = ^{val = 1; printf("val = %d\n",val);};
blk();
printf("val = %d\n",val);

同樣的如果Block在堆上兩個(gè)輸出也一樣:

說(shuō)明

無(wú)論在Block語(yǔ)法中,Block語(yǔ)法外使用__block變量,還是__block變量配置在棧上或者堆上,都可以順利訪問(wèn)同一個(gè)__block

說(shuō)到Block不得不談循環(huán)引用問(wèn)題,但是比較簡(jiǎn)單,網(wǎng)上一大堆,這里也不分析了。

小結(jié)

本文探索了Block的底層實(shí)現(xiàn)機(jī)制,我們發(fā)現(xiàn)Block在iOS中是作為對(duì)象來(lái)管理的?,F(xiàn)在再看看這句話
Block:可以理解為帶有自動(dòng)變量值的匿名函數(shù)。是不是形容的很貼切。

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

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

  • Blocks Blocks Blocks 是帶有局部變量的匿名函數(shù) 截取自動(dòng)變量值 int main(){ ...
    南京小伙閱讀 1,078評(píng)論 1 3
  • Block基礎(chǔ)回顧 1.什么是Block? 帶有局部變量的匿名函數(shù)(名字不重要,知道怎么用就行),差不多就與C語(yǔ)言...
    Bugfix閱讀 6,909評(píng)論 5 61
  • Block實(shí)際上是Objective-C對(duì)閉包的實(shí)現(xiàn)。 關(guān)于閉包的概念: In programming langu...
    chushen61閱讀 397評(píng)論 0 0
  • Block是什么? Block實(shí)際上是Objective-C對(duì)閉包的實(shí)現(xiàn)。 關(guān)于閉包的概念:In programm...
    Gekkko閱讀 1,660評(píng)論 0 12
  • 原創(chuàng)文章轉(zhuǎn)載請(qǐng)注明出處,謝謝 這段時(shí)間重新回顧了一下Block的知識(shí),由于只要講原理方面的知識(shí),所以關(guān)于Block...
    北辰明閱讀 3,637評(píng)論 2 9

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