前言:git
Blocks表現爲「帶有自動變量(局部變量)的匿名函數」。Blocks的本質是Objective-C的對象。本文主要內容來自《Objective-C高級編程 iOS與OSX多線程和內存管理》學習與探索,從Blocks的表現形式出發,經過Objective-C轉換成的C++源碼探索Blocks的本質。咱們主要探討了如下幾個結論:(1)Block的實質是棧上Block的結構體實例;__block變量的實質是棧上__block變量的結構體實例。(2)Block「截獲自動變量值」是Block把所使用的自動變量值被保存到Block的結構體實例(即Block自身)中了。(3)Block變量和__block變量超出其做用域而存在的理由是編譯器幫咱們把棧上的變量複製到了堆上(有些狀況須要咱們本身複製)。除此以外,咱們還探討了Block的循環引用等一些使用狀況。github
正文:編程
1、什麼是Blocks?數組
Blocks是C語言的擴充功能。能夠用一句話來表示Blocks的擴充功能:帶有自動變量(局部變量)的匿名函數。Block的使用其實至關於代理,通常是跨越兩個類來使用的。好比做爲property屬性或者做爲方法的參數,這樣就能跨越兩個類了.數據結構
下面的2.1講解的"Block語法"和2.2講解的"Block類型變量"能夠說明"Blocks是匿名函數"這一特性。而"帶有自動變量"在Blocks中的表現爲"截獲自動變量值",下面的2.3節將會講解這個特性。多線程
2、Blocks模式框架
2.1 Block語法jsp
Block語法使用,有以下幾種形式:async
//返回值類型+參數列表+表達式 ^int(int count){return count+1;}
//參數列表+表達式 ^(int count){return count+1;}
//表達式 ^{printf("Blocks\n");}
注意上面的舉例,省略了返回值類型的block,不必定它的返回值爲void。ide
2.2 Block類型變量
跟函數指針類型變量很相像,聲明Block類型變量舉例以下:
int(^blk)(int) = ^(int count){return count+1;};
Block能夠做爲函數的參數傳遞,也能夠做爲函數的返回值。爲了使用方便,咱們將blk用typedef聲明以下:
typedef int (^blk_t)(int);
Block類型變量可徹底像一般的C語言變量同樣使用,所以也可使用指向Block類型變量的指針,即Block的指針類型變量。
typedef int (^blk_t)(int); blk_t blk = ^(int count){return count+1;}; blk_t *blkptr = &blk; (*blkptr)(10);
2.3 截獲自動變量值
舉一個例子:
int main(int argc, char * argv[]) { int val = 10; void(^blk)(void)=^{printf("%d",val)}; val = 2; blk(); // 輸出10,而不是2. }
val爲10的值被保存(即被截獲),從而在執行塊時使用。這就是自動變量的截獲。
2.4 __block關鍵字的使用
若想在Block語法的表達式中將值賦給在Block語法外聲明的自動變量,須要在該自動變量上附加__block說明符,不然會產生編譯錯誤的,後面會講爲何。
__block int val = 10; void(^blk)(void)=^{val=1;}; blk();
若是截獲的是Objective-C對象,那麼向其賦值也會產生編譯錯誤,必須附加__block說明符:
__block id array = [[NSMutablearray alloc] init]; void(^blk)(void)=^{array = [[NSMutablearray alloc] init];}; //若是array沒有指定__block說明符,此處就會編譯報錯.
可是,對截獲的Objective-C對象調用變動該對象的方法是ok的:
id array = [[NSMutablearray alloc] init]; void(^blk)(void)=^{ id obj = [[NSObject alloc] init]; [array addObject: obj];//array沒有指定__block說明符,沒有問題. };
另外,如今的Blocks中,截獲自動變量的方法並無實現對C語言數組的截獲,好比「const char text[] = "hello"; 」,固然,你能夠把它聲明成指針的形式來解決這個問題「const char *text = "hello"; 」。
3、Blocks的實現
3.1 Block的實質(C++源碼分析)
Blocks的實質是Objective-C的對象。
咱們能夠用clang(LLVM編譯器)來將Objective-C的代碼轉換成能夠理解的C++的源代碼,來探索這個問題。例如,我寫一個Objective-C的代碼文件命名爲block_obj.m,而後我就能夠經過以下clang命令將其轉換爲block_obj.cpp文件:
clang -rewrite-objc block_obj.m
寫一個很簡單的Objective-C的Blocks的代碼:
#include <stdio.h> int main(){ void(^blk)(void) = ^{printf("Block\n");}; // 聲明並定義Blocks blk(); // 執行Blocks函數 return 0; }
轉換後的block_obj.cpp文件中,咱們看一下其C++實現至關長,這裏咱們摘幾個跟咱們上面這3句Objective-C的代碼息息相關的轉換來看,恕我不把源碼整個貼出來了,只撿須要的來逐步講解一下。上面的Objective-C的代碼直觀的轉換爲下面的C++代碼:
int main(){ void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); //聲明並定義Blocks ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); //執行Blocks函數 return 0; }
第二句的執行Blocks函數看起來比較複雜,實際上是簡單的使用函數指針調用函數,它能夠簡化爲下面的句子:
(*blk->impl.FuncPtr)(blk);
blk對應的結構體是下面的C++代碼:
//blk對應的結構體 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; } };
對blk結構體中各個成員變量進一步查看其源碼以下:
//blk結構體中構造函數中參數fp指針指向的函數 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { // __cself至關於C++中的this指針,這裏指向Blocks的變量 printf("Block\n"); } //blk結構體中成員變量impl的數據結構,即Block的結構體 struct __block_impl { void *isa; int Flags; int Reserved; // 版本升級所需的區域 void *FuncPtr; // 函數指針 }; //blk結構體中成員變量Desc的數據結構 static struct __main_block_desc_0 { size_t reserved; // 版本升級所需的區域 size_t Block_size; // Block的大小 } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
Blocks函數對應的變換後源碼是上面的__main_block_func_0函數,從變換後的源碼來看,經過Blocks使用匿名函數實際上被做爲簡單的C語言函數來處理的。由Block語法轉換的__main_block_func_0函數的指針被賦值成員變量FuncPtr中。__main_block_func_0函數的的參數_cself指向Block值。在調用該函數的源代碼中能夠看出Block正是做爲參數進行了傳遞。
到此,咱們總算摸清了Block的實質。但還不夠,咱們要重點理解將Block指針賦給Block的結構體的成員變量isa這句話,才能最終理解爲何Block就是Objective-C對象:
isa = &_NSConcreteStackBlock;
首先要理解Objective-C類和對象的實質。從最基本的objc_class結構體談起,「id」這一變量類型用於存儲Objective-C對象,它的聲明以下:
typedef struct objc_object { Class *isa; } *id; // objc_class就是Class typedef struct objc_class *Class;
struct objc_class { Class isa; };
objc_object結構體和objc_class結構體歸根結底是在各個對象和類的實現中使用的最基本的結構體。在Objective-C中,各種的結構體就是基於objc_class結構體的class_t結構體:
struct class_t { struct class_t *isa; struct class_t *superclass; Cache cache; IMP *vtable; uintptr_t data_NEVER_USE; };
"Objective-C中由類生成對象"意味着,像該結構體這樣"生成由該類生成的對象的結構體的實例"。該實例持有聲明的成員變量、方法的名稱、方法的實現(即函數指針)、屬性以及父類的指針,並被Objective-C運行時庫所使用。這就是Objective-C的類與對象的實質。
再看上面的Block結構體:
struct __main_block_impl_0 { void *isa; int Flags; int Reserved; void *FuncPtr; struct __main_block_desc_0* Desc; };
此__main_block_impl_0結構體至關於objc_object結構體的Objective-C類對象的結構體,對isa的初始化「isa = &_NSConcreteStackBlock;」, _NSConcreteStackBlock至關於class_t結構體實例,在將Block做爲Objective-C的對象處理時,關於該類的信息放置於_NSConcreteStackBlock中。
至此,就理解了Block的實質,知道Block即爲Objective-C的對象了。
補充isa的知識:
isa是一個Class類型的指針,每一個實例對象有個isa指針,它指向對象的類,而Class裏也有個isa指針,指向metaClass(元類),元類保存了類方法的列表。元類也有isa指針,它的isa指針最終指向根元類(root metaClass),根元類的isa指針指向自己,這樣造成了一個封閉的內循環。
(1)每個對象本質上都是一個類的實例。其中類定義了成員變量和成員方法的列表。對象經過對象的isa指針指向類。
(2)每個類本質上都是一個對象,類實際上是元類(metaClass)的實例。元類定義了類方法的列表。類經過類的isa指針指向元類。
(3)全部的元類最終繼承一個根元類,根元類isa指針指向自己,造成一個封閉的內循環。
3.2 截獲自動變量(C++源碼分析)
Block如何截獲自動變量的,咱們仍是從C++源碼進行分析。先寫一個簡單的截獲自動變量的Block程序,以下:
#include <stdio.h> int main(){ int dmy = 256; int val = 10; const char *fmt = "val = %d\n"; void(^blk)(void) = ^{printf(fmt,val);}; val = 2; fmt = "These value were changed. val = %d\n"; blk(); return 0; }
直觀的轉換爲下面的C++代碼:
int main(){ int dmy = 256; int val = 10; const char *fmt = "val = %d\n"; void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val)); val = 2; fmt = "These value were changed. val = %d\n"; ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); return 0; }
和上面3.2節介紹的基本相同,咱們直接看blk對應的結構體的C++代碼:
//blk對應的結構體 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; const char *fmt; int val; // 構造函數 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
其中,fmt和val是blk中要用到的變量,自動變量被做爲成員變量追加到__main_block_impl_0中了,Blocks的自動變量截獲只針對Block中使用的自動變量。在構造函數中,咱們能夠看到自動變量值被截獲。對blk結構體中各個成員變量進一步查看其源碼以下:
// blk結構體中構造函數中參數fp指針指向的函數 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { const char *fmt = __cself->fmt; // bound by copy int val = __cself->val; // bound by copy printf(fmt,val);
} //blk結構體中成員變量impl的數據結構,即Block的結構體 struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; //blk結構體中成員變量Desc的數據結構 static struct __main_block_desc_0 { size_t reserved; size_t Block_size; } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
總的來講,所謂「截獲自動變量值」意味着在執行Block語法時,Block語法表達式所使用的自動變量值被保存到Block的結構體實例(即Block自身)中。
3.3 __block說明符(C++源碼分析)
Block僅截獲自動變量的值,Block中使用自動變量後,在Block的結構體中重寫該自動變量也不會改變原先截獲的自動變量。要改變截獲的自動變量值,咱們要談一談「__block說明符」了。先寫一個簡單的例子:
int main(){ __block int val = 10; void(^blk)(void) = ^{val = 1;}; }
變換後以下:
int main(){ __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10}; void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344)); }
咱們看到,一個簡單的__block val變量,變成告終構體實例,把上面的__block val變量整理一下,就是下面的結構體:
__Block_byref_val_0 val = { (void*)0, (__Block_byref_val_0 *)&val, // 指向實例自身 0, sizeof(__Block_byref_val_0), 10 };
main中的blk對應的結構體和__block val變量對應的結構體以下所示:
//blk對應的結構體 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_val_0 *val; // by ref ,block變量 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; // __block val變量對應的結構體 struct __Block_byref_val_0 { void *__isa; __Block_byref_val_0 *__forwarding; // 持有指向該實例自身的指針 int __flags; int __size; int val; // 成員變量val是至關於原自動變量的成員變量 };
blk結構體中構造函數中參數fp指針指向的函數,即一個簡單的val=1,變成以下的代碼:
// blk結構體中構造函數中參數fp指針指向的函數 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_val_0 *val = __cself->val; // bound by ref (val->__forwarding->val) = 1; }
認真理解上面這句代碼,val是屬於__Block_byref_val_0結構體的,__Block_byref_val_0結構體的成員變量__forwarding持有指向該實例自身的指針,最後經過__forwarding訪問成員變量val(成員變量val是該實例自身持有的變量,它至關於原自動變量)。
(1)__block變量即__Block_byref_val_0結構體實例;
(2)訪問__block變量,即(val->__forwarding->val)=1;
(以下圖所示:__Block_byref_val_0結構體中訪問__block變量)
圖1 __forwarding的指向
此處看起來這個__forwarding有點多餘,它的存在的意義咱們下一節討論。
另外,上面兩句代碼還生成了一些copy相關的代碼以下,雖然咱們沒有進行過block的copy操做,但仍是生成了copy的代碼,這都是編譯器幫助咱們作的,下一節也會講解爲何會須要copy block,以及編譯器在哪些狀況下會copy block。
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { _Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/); } static void __main_block_dispose_0(struct __main_block_impl_0*src) { _Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/); } //blk結構體中成員變量Desc的數據結構 static struct __main_block_desc_0 { size_t reserved; size_t Block_size; void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); void (*dispose)(struct __main_block_impl_0*); } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
咱們看到,編譯器幫咱們在__main_block_desc_0中增長了成員變量copy和dispose,以及做爲指針賦值給成員變量__main_block_copy_0函數和__main_block_dispose_0函數。
3.4 Block存儲域
本節主要說明:(1)Block超出變量做用域可存在的理由;(2)__block變量的結構體成員變量__forwarding存在的理由。
首先,經過上面的分析,咱們知道:Block的實質是:棧上Block的結構體實例;__block變量的實質是:棧上__block變量的結構體實例。咱們以前看到Block的類爲_NSConcreteStackBlock。實際上Block的類有如下三種形式,它們的內存分配對應以下:
圖2 三種Block類對應的內存分配
有兩種狀況,Block是_NSConcreteGlobalBlock類的,即配置在程序的數據區域:(1)Block自己定義在全局區(2)Block不截獲自動變量(Block不管定義在哪裏,有可能轉換後的源碼是_NSConcreteStackBlock類,但實際上是_NSConcreteGlobalBlock類)。除此以外的Block語法生成的Block爲_NSConcreteStackBlock類對象,且設置在棧上。
而對於棧上的Block,有一個問題,就是若是其所屬的變量做用域結束,該Block就會廢棄,因爲__block變量也配置在棧上,一樣的,若是其所屬的變量做用域結束,則__block變量也會被廢棄。Block提供了將Block和__block變量從棧上覆制到堆上來解決這個問題,各類Block的複製效果以下:
圖3 三種Block類複製的效果
而__block變量用結構體成員變量__forwarding能夠實現不管_block變量配置在棧上仍是堆上時都可以正確地訪問__block變量。__forwarding的終極解釋:__block 變量從棧複製到堆上時,會將成員變量__forwarding的值替換爲複製目標堆上的__block變量用結構體實例的地址。在棧上和複製到堆上的__forwarding的指向能夠用下圖表示:
圖4 __forwarding複製前和複製後的指向
這時,咱們再看一眼剛開始講的Block截獲自動變量的問題,就比較清楚爲何blk中的val自動變量不會跟隨棧上的val在變更了:
當ARC有效時,大多數情形下編譯器會恰當的進行判斷,自動生成將Block從棧上覆制到堆上的代碼。編譯器有時候會自動幫咱們複製,有時候不會幫咱們複製,若是咱們不手動調用copy方法的話,就會帶來一些問題。那麼,什麼狀況下須要手動copy Block,什麼狀況不須要?
不須要咱們手動複製Block的狀況:
(1)Cocoa框架的方法且方法名中含有usingBlock等時。好比在使用NSArray類的enumerateObjectsUsingBlock實例方法,不用手動複製Block。
(2)GCD的API。好比在使用dispatch_async函數時,不用手動複製Block。
須要手動複製Block的狀況:向方法或函數的參數中傳遞Block時。好比,在NSArray類的initWithObjects實例方法上傳遞Block時,須要手動複製,舉例以下:
-(id)getBlockArray { int val = 10; return [[NSArray alloc] initWithObjects: [^{NSLog(@"blk0:%d",val);} copy], //若是不進行copy的話,取出來用時就會發生異常 [^{NSLog(@"blk1:%d",val);} copy],nil]; }
除了以上講到的須要手動複製Block的狀況和不須要手動複製Block的狀況,咱們從以前的三種Block複製效果能夠知道,不管Block是在棧上、堆上,仍是在程序的數據區域,用copy方法複製都不會引發任何問題。在不肯定時調用copy方法便可。好比,咱們常聲明blk類型的屬性以下:
typedef void (^blk_t)(void); @property (nonatomic, copy) blk_t blk;
3.5 __block變量存儲域
上節對Block複製的狀況進行了說明,那麼複製時__block變量的狀況是怎麼樣的呢?
圖5 Block中使用__block變量的複製狀況
圖5是一個Block使用__block變量時的複製狀況,其實多個Block變量使用一個__block的狀況也相似。其思考方式與Objective-C的引用計數式內存管理徹底相同。使用__block變量的Block持有__block變量。若是Block被廢棄,它所持有的__block變量也就被釋放。
3.6 截獲對象
參考3.3節的代碼:
int main(){ __block int val = 10; void(^blk)(void) = ^{val = 1;}; }
咱們看看轉換後的C++源碼中的copy和dispose函數:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { _Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/); } static void __main_block_dispose_0(struct __main_block_impl_0*src) { _Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/); } //blk結構體中成員變量Desc的數據結構 static struct __main_block_desc_0 { size_t reserved; size_t Block_size; void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); void (*dispose)(struct __main_block_impl_0*); } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
__main_block_copy_0函數會將val變量複製給Block用結構體的成員變量val中並持有該對象。__main_block_dispose_0函數調用至關於release實例方法的函數,釋放賦值在對象類型的結構體成員變量中的對象。copy函數和dispose函數調用時機以下:
那麼,何時棧上的Block會複製到堆上呢?有如下4種狀況:
(1)調用Block的copy實例方法時;
(2)Block做爲函數返回值返回時;
(3)將Block賦值給類的附有__strong修飾符的id類型或Block類型成員變量時;
(4)向方法名中含有usingBlock的Cocoa框架方法或GCD的API中傳遞Block時。
對於__block變量和__block對象,總結以下:copy函數持有截獲的對象、變量,dispose函數釋放截獲的對象、變量。
3.7 Block循環引用
首先看一段代碼:
@weakify(self); [[MTANetworkManager instance] addResMiddleware:^(id object, MTAMiddlewareNextBlock next) { @strongify(self); [self refreshTokenWithTask:task]; }];
若是不寫上面的@weakify,@strongify關鍵字,會存在下面左邊的循環引用,使用了關鍵字以後,咱們打破了這個循環引用,以下所示:
上面的代碼中,self擁有block,block中又使用了self,所以須要使用@weakify(self)和@strongify(self)來避免循環引用。原理:
After @strongify
is called, self
will have a different pointer address inside the block than it will outside the block. That's because @strongify
declares a new local variable called self
each time. (This is why it suppresses the -Wshadow
warning, which will 「warn whenever a local variable shadows another local variable.」) It's worth reading and understanding the implementation of these functions. So even though the names are the same, treat them as separate strong
references. However, remember that after your first use of @strongify
, self
will refer to local, stack variables.
結論:
經過上面的分析,咱們主要探討了如下結論:
(1)Block的實質是棧上Block的結構體實例;__block變量的實質是棧上__block變量的結構體實例。
(2)Block「截獲自動變量值」是Block把所使用的自動變量值被保存到Block的結構體實例(即Block自身)中了。
(3)Block變量和__block變量超出其做用域而存在的理由是編譯器幫咱們把棧上的變量複製到了堆上(有些狀況須要咱們本身複製)。
(4)__block變量存在的理由是:__forwarding能夠實現不管_block變量配置在棧上仍是堆上時都可以正確地訪問__block變量。