iOS Block用法和實現原理

Block概要

Block:帶有自動變量匿名函數

匿名函數:沒有函數名的函數,一對{}包裹的內容是匿名函數的作用域。

自動變量:棧上聲明的一個變量不是靜態變量和全局變量,是不可以在這個棧內聲明的匿名函數中使用的,但在Block中卻可以。

雖然使用Block不用聲明類,但是Block提供了類似Objective-C的類一樣可以通過成員變量來保存作用域外變量值的方法,那些在Block的一對{}里使用到但卻是在{}作用域以外聲明的變量,就是Block截獲的自動變量。

Block常規概念

Block語法

Block表達式語法:

^ 返回值類型 (參數列表) {表達式}

例如:

^int(intcount) {returncount +1;? ? };

其中,可省略部分有:

返回類型,例:

^ (intcount) {returncount +1;? ? };

參數列表為空,則可省略,例:

^ {NSLog(@"No Parameter");? ? };

即最簡模式語法為:

^ {表達式}

Block類型變量

聲明Block類型變量語法:

返回值類型 (^變量名)(參數列表) = Block表達式

例如,如下聲明了一個變量名為blk的Block:

int(^blk)(int) = ^(intcount) {returncount +1;? ? };

當Block類型變量作為函數的參數時,寫作:

- (void)func:(int(^)(int))blk {NSLog(@"Param:%@", blk);}

借助typedef可簡寫:

typedefint(^blk_k)(int);- (void)func:(blk_k)blk {NSLog(@"Param:%@", blk);}

Block類型變量作返回值時,寫作:

- (int(^)(int))funcR {return^(intcount) {returncount ++;? ? };}

借助typedef簡寫:

typedefint(^blk_k)(int);- (blk_k)funcR {return^(intcount) {returncount ++;? ? };}

截獲自動變量值

Block表達式可截獲所使用的自動變量的值。

截獲:保存自動變量的瞬間值

因為是“瞬間值”,所以聲明Block之后,即便在Block外修改自動變量的值,也不會對Block內截獲的自動變量值產生影響。

例如:

inti =10;void(^blk)(void) = ^{NSLog(@"In block, i = %d", i);? ? };? ? i =20;//Block外修改變量i,也不影響Block內的自動變量blk();//i修改為20后才執行,打印: In block, i = 10NSLog(@"i = %d", i);//打印:i = 20

__block說明符號

自動變量截獲的值為Block聲明時刻的瞬間值,保存后就不能改寫該值,如需對自動變量進行重新賦值,需要在變量聲明前附加__block說明符,這時該變量稱為__block變量。

例如:

__blockinti =10;//i為__block變量,可在block中重新賦值void(^blk)(void) = ^{NSLog(@"In block, i = %d", i);? ? };? ? i =20;? ? blk();//打印: In block, i = 20NSLog(@"i = %d", i);//打印:i = 20

自動變量值為一個對象情況

當自動變量為一個類的對象,且沒有使用__block修飾時,雖然不可以在Block內對該變量進行重新賦值,但可以修改該對象的屬性。

如果該對象是個Mutable的對象,例如NSMutableArray,則還可以在Block內對NSMutableArray進行元素的增刪:

NSMutableArray*array = [[NSMutableArrayalloc] initWithObjects:@"1",@"2",nil];NSLog(@"Array Count:%ld", array.count);//打印Array Count:2void(^blk)(void) = ^{? ? ? ? [array removeObjectAtIndex:0];//Ok//array = [NSNSMutableArray new];//沒有__block修飾,編譯失敗!};? ? blk();NSLog(@"Array Count:%ld", array.count);//打印Array Count:1

Block實現原理

使用Clang

Block實際上是作為極普通的C語言源碼來處理的:含有Block語法的源碼首先被轉換成C語言編譯器能處理的源碼,再作為普通的C源代碼進行編譯

使用LLVM編譯器的clang命令可將含有Block的Objective-C代碼轉換成C++的源代碼,以探查其具體實現方式:

clang -rewrite-objc 源碼文件名

注:如果使用該命令報錯:’UIKit/UIKit.h’ file not found,可參考《Objective-C編譯成C++代碼報錯》解決。

Block結構

使用Block的時候,編譯器對Block語法進行了怎樣的轉換?

intmain(){intcount =10;void(^ blk)() = ^(){? ? ? ? NSLog(@"In Block:%d", count);? ? };? ? blk();}

如上所示的最簡單的Block使用代碼,經clang轉換后,可得到以下幾個部分(有代碼刪減和注釋添加):

staticvoid__main_block_func_0(struct__main_block_impl_0 *__cself) {intcount = __cself->count;// bound by copyNSLog((NSString*)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_d2f8d2_mi_0,? ? count);}

這是一個函數的實現,對應Block中{}內的內容,這些內容被當做了C語言函數來處理,函數參數中的__cself相當于Objective-C中的self。

struct__main_block_impl_0{struct__block_implimpl;struct__main_block_desc_0*Desc;//描述Block大小、版本等信息intcount;//構造函數函數__main_block_impl_0(void*fp,? ? ? ? ? struct __main_block_desc_0 *desc,int_count,intflags=0) : count(_count) {? ? impl.isa = &_NSConcreteStackBlock;//在函數棧上聲明,則為_NSConcreteStackBlockimpl.Flags = flags;? ? impl.FuncPtr = fp;? ? Desc = desc;? }};

__main_block_impl_0即為main()函數棧上的Block結構體,其中的__block_impl結構體聲明如下:

struct__block_impl{void*isa;//指明對象的ClassintFlags;intReserved;void*FuncPtr;};

__block_impl結構體,即為Block的結構體,可理解為Block的類結構

再看下main()函數翻譯的內容:

intmain(){intcount =10;void(* blk)() = ((void(*)())&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, count));? ? ? ? ((void(*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);}

去除掉復雜的類型轉化,可簡寫為:

intmain(){intcount =10;? ? sturct __main_block_impl_0 *blk = &__main_block_impl_0(__main_block_func_0,//函數指針&__main_block_desc_0_DATA));//Block大小、版本等信息(*blk->FuncPtr)(blk);//調用FuncPtr指向的函數,并將blk自己作為參數傳入}

由此,可以看出,Block也是Objective-C中的對象

Block有三種類(即__block_impl的isa指針指向的值,isa說明參考《Objective-C isa 指針 與 runtime 機制》),根據Block對象創建時所處數據區不同而進行區別:

_NSConcreteStackBlock:在棧上創建的Block對象

_NSConcreteMallocBlock:在堆上創建的Block對象

_NSConcreteGlobalBlock:全局數據區的Block對象

如何截獲自動變量

上部分介紹了Block的結構,和作為匿名函數的調用機制,那自動變量截獲是發生在什么時候呢?

觀察上節代碼中__main_block_impl_0結構體(main棧上Block的結構體)的構造函數可以看到,棧上的變量count以參數的形式傳入到了這個構造函數中,此處即為變量的自動截獲

因此可以這樣理解:__block_impl結構體已經可以代表Block類了,但在棧上又聲明了__main_block_impl_0結構體,對__block_impl進行封裝后才來表示棧上的Block類,就是為了獲取Block中使用到的棧上聲明的變量(棧上沒在Block中使用的變量不會被捕獲),變量被保存在Block的結構體實例中。

所以在blk()執行之前,棧上簡單數據類型的count無論發生什么變化,都不會影響到Block以參數形式傳入而捕獲的值。但這個變量是指向對象的指針時,是可以修改這個對象的屬性的,只是不能為變量重新賦值。

Block的存儲域

上文已提到,根據Block創建的位置不同,Block有三種類型,創建的Block對象分別會存儲到棧、堆、全局數據區域。

void(^blk)(void) = ^{NSLog(@"Global Block");};intmain() {? ? blk();NSLog(@"%@",[blkclass]);//打印:__NSGlobalBlock__}

像上面代碼塊中的全局blk自然是存儲在全局數據區,但注意在函數棧上創建的blk,如果沒有截獲自動變量,Block的結構實例還是會被設置在程序的全局數據區,而非棧上

intmain() {void(^blk)(void) = ^{//沒有截獲自動變量的BlockNSLog(@"Stack Block");? ? };? ? blk();NSLog(@"%@",[blkclass]);//打印:__NSGlobalBlock__inti =1;void(^captureBlk)(void) = ^{//截獲自動變量i的BlockNSLog(@"Capture:%d", i);? ? };? ? captureBlk();NSLog(@"%@",[captureBlkclass]);//打印:__NSMallocBlock__}

可以看到截獲了自動變量的Block打印的類是NSGlobalBlock,表示存儲在全局數據區。

但為什么捕獲自動變量的Block打印的類卻是設置在堆上的NSMallocBlock,而非棧上的NSStackBlock?這個問題稍后解釋。

Block復制

配置在棧上的Block,如果其所屬的棧作用域結束,該Block就會被廢棄,對于超出Block作用域仍需使用Block的情況,Block提供了將Block從棧上復制到堆上的方法來解決這種問題,即便Block棧作用域已結束,但被拷貝到堆上的Block還可以繼續存在。

復制到堆上的Block,將_NSConcreteMallocBlock類對象寫入Block結構體實例的成員變量isa:

? ? impl.isa = &_NSConcreteMallocBlock;

在ARC有效時,大多數情況下編譯器會進行判斷,自動生成將Block從棧上復制到堆上的代碼,以下幾種情況棧上的Block會自動復制到堆上

調用Block的copy方法

將Block作為函數返回值時

將Block賦值給__strong修改的變量時

向Cocoa框架含有usingBlock的方法或者GCD的API傳遞Block參數時

其它時候向方法的參數中傳遞Block時,需要手動調用copy方法復制Block。

上一節的棧上截獲了自動變量i的Block之所以在棧上創建,卻是NSMallocBlock類,就是因為這個Block對象賦值給了_strong修飾的變量captureBlk(_strong是ARC下對象的默認修飾符)。

因為上面四條規則,在ARC下其實很少見到_NSConcreteStackBlock類的Block,大多數情況編譯器都保證了Block是在堆上創建的,如下代碼所示,僅最后一行代碼直接使用一個不賦值給變量的Block,它的類才是NSStackBlock

intcount =0;? ? blk_t blk = ^(){NSLog(@"In Stack:%d", count);? ? };NSLog(@"blk's Class:%@", [blkclass]);//打印:blk's Class:__NSMallocBlock__NSLog(@"Global Block:%@", [^{NSLog(@"Global Block");}class]);//打印:Global Block:__NSGlobalBlock__NSLog(@"Copy Block:%@", [[^{NSLog(@"Copy Block:%d",count);}copy]class]);//打印:Copy Block:__NSMallocBlock__NSLog(@"Stack Block:%@", [^{NSLog(@"Stack Block:%d",count);}class]);//打印:Stack Block:__NSStackBlock__

關于ARC下和MRC下Block自動copy的區別,查看《Block 小測驗》里幾道題目就能區分了。

另外,原書存在ARC和MRC混合講解、區分不明的情況,比如書中幾個使用到棧上對象導致Crash的例子是MRC條件下才會發生的,但書中沒做特殊說明。

使用__block發生了什么

Block捕獲的自動變量添加__block說明符,就可在Block內讀和寫該變量,也可以在原來的棧上讀寫該變量。

自動變量的截獲保證了棧上的自動變量被銷毀后,Block內仍可使用該變量。

__block保證了棧上和Block內(通常在堆上)可以訪問和修改“同一個變量”,__block是如何實現這一功能的?

__block發揮作用的原理:將棧上用__block修飾的自動變量封裝成一個結構體,讓其在堆上創建,以方便從棧上或堆上訪問和修改同一份數據。

驗證過程

現在對剛才的代碼段,加上__block說明符,并在block內外讀寫變量count。

intmain() {? ? __blockintcount =10;void(^ blk)() = ^(){? ? ? ? count =20;NSLog(@"In Block:%d", count);//打印:In Block:20};? ? count ++;NSLog(@"Out Block:%d", count);//打印:Out Block:11blk();}

將上面的代碼段clang,發現Block的結構體__main_block_impl_0結構如下所示:

struct__main_block_impl_0{struct__block_implimpl;struct__main_block_desc_0*Desc;__Block_byref_count_0 *count;// by ref__main_block_impl_0(void*fp, struct __main_block_desc_0 *desc, __Block_byref_count_0 *_count,intflags=0) : count(_count->__forwarding) {? ? impl.isa = &_NSConcreteStackBlock;? ? impl.Flags = flags;? ? impl.FuncPtr = fp;? ? Desc = desc;? }};

最大的變化就是count變量不再是int類型了,count變成了一個指向__Block_byref_count_0結構體的指針,__Block_byref_count_0結構如下:

struct__Block_byref_count_0{void*__isa;__Block_byref_count_0 *__forwarding;int__flags;int__size;intcount;};

它保存了int count變量,還有一個指向__Block_byref_count_0實例的指針__forwarding,通過下面兩段代碼__forwarding指針的用法可以知道,該指針其實指向的是對象自身:

//Block的執行函數staticvoid __main_block_func_0(struct __main_block_impl_0 *__cself) {? __Block_byref_count_0 *count = __cself->count;// bound by ref(count->__forwarding->count) =20;//對應count = 20;NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_fafeeb_mi_0,? ? ? ? (count->__forwarding->count));? ? }

//main函數intmain() {? ? __attribute__((__blocks__(byref))) __Block_byref_count_0 count = {(void*)0,? ? (__Block_byref_count_0 *)&count,0,sizeof(__Block_byref_count_0),10};void(* blk)() = ((void(*)())&__main_block_impl_0((void*)__main_block_func_0,? ? &__main_block_desc_0_DATA,? ? (__Block_byref_count_0 *)&count,570425344));? ? ? ? (count.__forwarding->count) ++;//對應count ++;NSLog((NSString*)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_fafeeb_mi_1,? ? (count.__forwarding->count));? ? ? ? ((void(*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);}

為什么要通過__forwarding指針完成對count變量的讀寫修改?

為了保證無論是在棧上還是在堆上,都能通過都__forwarding指針找到在堆上創建的count這個__main_block_func_0結構體,以完成對count->count(第一個count是__main_block_func_0對象,第二個count是int類型變量)的訪問和修改。

示意圖如下:

Block的循環引用

Block的循環引用原理和解決方法大家都比較熟悉,此處將結合上文的介紹,介紹一種不常用的解決Block循環引用的方法和一種借助Block參數解決該問題的方法。

Block循環引用原因:一個對象A有Block類型的屬性,從而持有這個Block,如果Block的代碼塊中使用到這個對象A,或者僅僅是用用到A對象的屬性,會使Block也持有A對象,導致兩者互相持有,不能在作用域結束后正常釋放。

解決原理:對象A照常持有Block,但Block不能強引用持有對象A以打破循環。

解決方法

方法一:對Block內要使用的對象A使用_*_weak*進行修飾,Block對對象A弱引用打破循環。

有三種常用形式:

使用__weak ClassName

__block XXViewController* weakSelf =self;self.blk = ^{NSLog(@"In Block : %@",weakSelf);? ? };

使用__weak typeof(self)

__weaktypeof(self) weakSelf =self;self.blk = ^{NSLog(@"In Block : %@",weakSelf);? ? };

Reactive Cocoa中的@weakify和@strongify

@weakify(self);self.blk = ^{? ? ? ? @strongify(self);? ? ? ? NSLog(@"In Block : %@",self);? ? };

其原理參考《@weakify, @strongify》,自己簡便實現參考《@weak - @strong 宏的實現》

方法二:對Block內要使用的對象A使用__block進行修飾,并在代碼塊內,使用完__block變量后將其設為nil,并且該block必須至少執行一次。

__block XXController *blkSelf =self;self.blk = ^{NSLog(@"In Block : %@",blkSelf);? ? };

注意上述代碼仍存在內存泄露,因為:

XXController對象持有Block對象blk

blk對象持有__block變量blkSelf

__block變量blkSelf持有XXController對象

__block XXController *blkSelf =self;self.blk = ^{NSLog(@"In Block : %@",blkSelf);? ? ? ? blkSelf =nil;//不能省略};self.blk();//該block必須執行一次,否則還是內存泄露

在block代碼塊內,使用完使用完__block變量后將其設為nil,并且該block必須至少執行一次后,不存在內存泄露,因為此時:

XXController對象持有Block對象blk

blk對象持有__block變量blkSelf(類型為編譯器創建的結構體)

__block變量blkSelf在執行blk()之后被設置為nil(__block變量結構體的__forwarding指針指向了nil),不再持有XXController對象,打破循環

第二種使用__block打破循環的方法,優點是:

可通過__block變量動態控制持有XXController對象的時間,運行時決定是否將nil或其他變量賦值給__block變量

不能使用__weak的系統中,使用__unsafe_unretained來替代__weak打破循環可能有野指針問題,使用__block則可避免該問題

缺點也明顯:

必須手動保證__block變量最后設置為nil

block必須執行一次,否則__block不為nil循環應用仍存在

因此,還是避免使用第二種不常用方式,直接使用__weak打破Block循環引用。

方法三:將在Block內要使用到的對象(一般為self對象),以Block參數的形式傳入,Block就不會捕獲該對象,而將其作為參數使用,其生命周期系統的棧自動管理,不造成內存泄露。

即原來使用__weak的寫法:

__weaktypeof(self) weakSelf =self;self.blk = ^{? ? ? ? __strongtypeof(self) strongSelf = weakSelf;NSLog(@"Use Property:%@", strongSelf.name);//……};self.blk();

改為Block傳參寫法后:

self.blk = ^(UIViewController*vc) {NSLog(@"Use Property:%@", vc.name);? ? };self.blk(self);

優點:

簡化了兩行代碼,更優雅

更明確的API設計:告訴API使用者,該方法的Block直接使用傳進來的參數對象,不會造成循環引用,不用調用者再使用weak避免循環

該種用法的詳細思路,和clang后的數據結構,可參考《Heap-Stack Dance》

鏈接:http://www.lxweimin.com/p/d28a5633b963

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容