Block原理解析

Block語法簡介

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

Blocks提供了類似由C++和Objective-C類生成實(shí)例變量或?qū)ο髞肀3肿兞恐档姆椒ā?/p>

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

^void (int event) { NSLog(@"肉盹。看峻。隘弊。");}

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

簡單的Block原理分析

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


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

源碼通過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");
}
//存儲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);
/*以下是我們的代碼部分*/

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

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

首先系統(tǒng)自動給我們生成了三個結(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;
} 

生成了兩個函數(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)用這個函數(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;

定義了一個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;
}

我們看到這個block的class類型為_NSConcreteStackBlock,這個下面會詳細(xì)解釋扫俺。函數(shù)地址為FuncPtr苍苞。所以iOS里的Block是被當(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會截獲自動變量


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

說明自動變量截獲只能保存執(zhí)行block語法瞬間的值

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

c語言有以下說明符:

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

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


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

進(jìn)行編譯后,并剔除和以上通過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;}賦值過程變成什么樣子了呢


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: 存儲在棧區(qū),需要截取變量
  • _NSConcreteGlobalBlock: 1.存儲在程序數(shù)據(jù)區(qū)域恩闻,2.不需要截取變量
  • _NSConcreteMallocBlock: 存儲在堆區(qū)

根據(jù)之前的分析,我們看到block的isa指針為_NSConcreteStackBlock剧董,里面有個Stack幢尚,可以猜到,這個為存儲在棧區(qū)的Block翅楼。

我們在記述全局變量的地方使用Block語法時(shí)候尉剩,生成的Block為_NSConcreteGlobalBlock,因?yàn)樵谑褂萌肿兞康牡胤讲荒苁褂米詣幼兞浚圆淮嬖趯ψ詣幼兞康慕孬@毅臊。


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

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

ARC 條件下編譯器會適當(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被廢棄。但編譯器自動會加上copy

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

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

例:
在用 如NSArray 的 enumerateObjectsUsingBlock 的實(shí)例方法和 dispatch_async函數(shù)前沟启,不用手動copy忆家。

在用如NSArray的 initWithObjects 前,需要手動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];
}
//會發(fā)生崩潰芽卿。因?yàn)镹SArray 的initWithObjects因?yàn)橄到y(tǒng)不確定加入的是不是block,不會自動執(zhí)行copy操作胳搞,如果我們也不執(zhí)行卸例,在作用域外調(diào)用就會發(fā)生崩潰。

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

綜上:我們要想把棧上的Block復(fù)制到堆上芽腾,只有執(zhí)行copy方法旦装,有些情況下,系統(tǒng)會自動幫我們執(zhí)行摊滔,但也有些情況我們需要手動執(zhí)行copy阴绢。

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

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

注: __weak, __strong 用來修飾變量呻袭,此外還有 __unsafe_unretained, __autoreleasing 都是用來修飾變量的。
__strong 是缺省的關(guān)鍵詞腺兴。
__weak 聲明了一個可以自動 nil 化的弱引用左电。
__unsafe_unretained 聲明一個弱應(yīng)用,但是不會自動nil化页响,也就是說篓足,如果所指向的內(nèi)存區(qū)域被釋放了,這個指針就是一個野指針了闰蚕。
__autoreleasing 用來修飾一個函數(shù)的參數(shù)栈拖,這個參數(shù)會在函數(shù)返回的時(shí)候被自動釋放。

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

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

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


//多次調(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持有砾赔,所以沒有廢棄。
*/
{
/*
配置在堆上的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持有弹沽,所以沒有廢棄檀夹。
*/
/*下面重復(fù)*/

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

一個含有__block變量的block被copy

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

我們看看以下代碼,一個在棧上的Block

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

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

說明

無論在Block語法中策橘,Block語法外使用__block變量炸渡,還是__block變量配置在棧上或者堆上,都可以順利訪問同一個__block

說到Block不得不談循環(huán)引用問題丽已,但是比較簡單蚌堵,網(wǎng)上一大堆,這里也不分析了沛婴。

小結(jié)

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市丑婿,隨后出現(xiàn)的幾起案子性雄,更是在濱河造成了極大的恐慌,老刑警劉巖羹奉,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毅贮,死亡現(xiàn)場離奇詭異,居然都是意外死亡尘奏,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進(jìn)店門病蛉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來炫加,“玉大人瑰煎,你說我怎么就攤上這事∷仔ⅲ” “怎么了酒甸?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長赋铝。 經(jīng)常有香客問我插勤,道長,這世上最難降的妖魔是什么革骨? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任农尖,我火速辦了婚禮,結(jié)果婚禮上良哲,老公的妹妹穿的比我還像新娘盛卡。我一直安慰自己,他們只是感情好筑凫,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布滑沧。 她就那樣靜靜地躺著,像睡著了一般巍实。 火紅的嫁衣襯著肌膚如雪滓技。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天棚潦,我揣著相機(jī)與錄音令漂,去河邊找鬼。 笑死瓦盛,一個胖子當(dāng)著我的面吹牛洗显,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播原环,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼挠唆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了嘱吗?” 一聲冷哼從身側(cè)響起玄组,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎谒麦,沒想到半個月后俄讹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡绕德,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年患膛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片耻蛇。...
    茶點(diǎn)故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡踪蹬,死狀恐怖胞此,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情跃捣,我是刑警寧澤漱牵,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站疚漆,受9級特大地震影響酣胀,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜娶聘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一闻镶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧趴荸,春花似錦儒溉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至酝豪,卻和暖如春涛碑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背孵淘。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工蒲障, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瘫证。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓揉阎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親背捌。 傳聞我的和親對象是個殘疾皇子毙籽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內(nèi)容

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