前言
很久前看了《Objective-C高級編程 iOS與OS X多線程和內(nèi)存管理》這本書理卑,但當(dāng)時看起來晦澀難懂。最近利用下班時間重讀了一遍期吓,覺得還是得記錄一下呐赡。畢竟往后階段對相同的東西會有更深刻的理解。溫故知新焚虱!
系列文章:
1购裙、《Objective-C高級編程》溫故知新之"自動引用計數(shù)"
2、《Objective-C高級編程》溫故知新之"Blocks"
Blocks概要
Blocks 是C語言的擴充功能鹃栽,即帶有自動變量(局部變量)的匿名函數(shù)躏率。
在計算機科學(xué)中,此概念也稱為閉包(Closure)、lambda計算等薇芝。Swift中稱作閉包
其他程序語言中 Block 的名稱
淺顯理解 Block
1蓬抄、Block 語法
完整形式的 Block 語法與一般的C語言函數(shù)定義相比,僅有兩點不同夯到。
(1)沒有函數(shù)名倡鲸。
(2)帶有“^”。
上面第一點也是匿名函數(shù)的由來黄娘。
Block 語法如下:
注意:Block 語法可以省略好幾個項目。
1克滴、返回值類型
省略返回值類型,如果表達(dá)式有 return 語句就使用該返回值的類型逼争,如果表達(dá)式中沒有 return 語句就使用 void 類型。 表達(dá)式中含有多個 return 語句時劝赔,所有 return 的返回值類型必須相同誓焦。相當(dāng)于
省略返回值類型
2、如果沒有參數(shù)着帽,參數(shù)列表也可以省略杂伟。
省略參數(shù)列表
2、截獲自動變量值
了解了匿名函數(shù)仍翰,接下來得了解“帶有自動變量值得匿名函數(shù)”中的“帶有自動變量值”的含義赫粥。
int dmy = 256;
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
在調(diào)用blk
前予借,改動了val,大家可能認(rèn)為打印 val = 2,但實際上打印 val = 10;
該源碼中越平,Block 語法表達(dá)式使用的是它之前聲明的自動變量值為10的 val ,Block 表達(dá)式截獲了這個值灵迫,并且保存下來秦叛,所以在執(zhí)行 Block 語法后,即使在后面改寫 Block 中使用的自動變量的值也不會影響 Block 執(zhí)行時自動變量的值瀑粥。
3挣跋、 __block 說明符
如果我們嘗試修改截獲的自動變量值,會怎么樣狞换,結(jié)果是會報錯避咆。
編譯的時報“Variable is not assignable (missing __block type specifier)”
所以若要在 Block 語法的表達(dá)式中將值賦給在 Block 語法外聲明的自動變量,需要在該自動變量上附加 __block 說明符
__block int val = 0;
void (^blk)(void) = ^{
val = 1;
};
blk();
printf("val = %d\n", val);
打有拊搿:
val = 1
注意:使用截獲的值不會有問題牌借,只有賦值才回報錯。
深入理解 Block
1割按、Block 的實現(xiàn)
通過上篇系列文章講到的 clang -rewrite-objc
將下列 Block 語句 轉(zhuǎn)換為可讀源碼
void (^blk)(void) = ^{(printf("Block\n"));};
blk();
return 0;
我們先挑出部分可讀源碼:
顯然膨报,上面代碼被轉(zhuǎn)換成了下列源碼
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");
}
該函數(shù)的參數(shù) __cself 相當(dāng)于 C++ 實例方法中指向?qū)嵗陨淼淖兞縯his, 或是 Objective-C 實例方法中指向?qū)ο笞陨淼淖兞?self ,即參數(shù) __cself 為指向 Block值的變量。
繼續(xù)看源碼中的參數(shù) struct __main_block_impl_0 *__cself
的聲明
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;
}
};
由于轉(zhuǎn)換后的源碼中现柠,也一并寫入了其構(gòu)造函數(shù)院领,所以看起來稍顯復(fù)雜,如果除開該構(gòu)造函數(shù)够吩,__main_block_impl_0
結(jié)構(gòu)體會變得非常簡單比然。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
};
第一個成員變量是 impl,看下 __block_impl impl
的聲明
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
我們從其名稱可以聯(lián)想到某些標(biāo)志周循、今后版本升級所需的區(qū)域以及函數(shù)指針强法。這些會在后面詳細(xì)說明。第二個成員是 Desc 指針湾笛,以下為其__main_block_desc_0
結(jié)構(gòu)體的聲明饮怯。
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
如同其名稱所示,其結(jié)構(gòu)為今后版本升級所需的區(qū)域和 Block 的大小嚎研。
我們再回頭看下初始化含有這些結(jié)構(gòu)體的 __main_block_impl_0
結(jié)構(gòu)體的構(gòu)造函數(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;
}
進(jìn)行講解前,我們先看看構(gòu)造函數(shù)的調(diào)用
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
簡化后為
__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
第一個參數(shù)是由 Block 語法轉(zhuǎn)換的 C 語言函數(shù)指針临扮。第二個是參數(shù)是作為靜態(tài)全局變量初始化的 __main_block_desc_0
結(jié)構(gòu)體實例指針论矾。問我怎么知道,看下面源碼案擞隆:
static struct __main_block_func_0 __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
由此可知贪壳,該源碼使用 Block,即__main_block_impl_0
結(jié)構(gòu)體實例的大小蚜退,進(jìn)行初始化寥袭。
下面看看棧上的 __main_block_impl_0
結(jié)構(gòu)體實例是如何根據(jù)這些參數(shù)進(jìn)行初始化的。如果展開結(jié)構(gòu)體__main_block_impl_0
中的__block_impl
結(jié)構(gòu)體关霸,可記述為如下形式:
struct __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
struct __main_block_desc_0 *desc;
}
該結(jié)構(gòu)體構(gòu)造函數(shù)會像下面這樣進(jìn)行初始化传黄。
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
}
從而,__main_block_impl_0
構(gòu)造函數(shù)已經(jīng)足夠清晰队寇,除了_NSConcreteStackBlock
仍未講解膘掰,先看blk()
的源碼
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
去掉轉(zhuǎn)換部分為
(((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
這就是簡單的使用函數(shù)指針調(diào)用函數(shù)。
那么一直沒說的_NSConcreteStackBlock
是什么呢佳遣?
首先识埋,將 Block 指針賦給 Block 的結(jié)構(gòu)體成員變量 isa .為了理解他,首先要理解 Objective-C 類和對象的實質(zhì)零渐。其實窒舟,所謂 Block 就是 Objective-C 對象。
id
這一變量類型用于存儲 Objective-C 對象诵盼。在 Objective-C 源代碼中惠豺,雖然可以像使用 void *類型那樣隨意使用 id, 但此 id 類型也能在 C 語言中聲明银还。在 runtime.h 聲明如下:
typedef struct objc_object {
Class isa;
} *id;
id 為 objc_object 結(jié)構(gòu)體的指針類型。 然后再看下 Class
typedef struct objc_class *Class;
Class 為objc_class 結(jié)構(gòu)體 的指針類型洁墙。objc_class 結(jié)構(gòu)體聲明如下:
struct objc_class {
Class isa;
};
這與 objc_object 結(jié)構(gòu)體相同蛹疯,所以
@interface MyObject : NSObject
{
int val0;
int val1;
}
上面代碼基于 object_object 結(jié)構(gòu)體,該類的對象的結(jié)構(gòu)體如下:
struct MyObject {
Class isa;
int val0;
int val1;
};
可以看出热监,MyObject 類的實例變量 val0 和 val1 被聲明為對象的結(jié)構(gòu)體成員捺弦。 意味著,生成的各個對象孝扛,也就是“由該類生成的對象的各個結(jié)構(gòu)體實例列吼,通過成員變量 isa 保持該類的結(jié)構(gòu)體實例指針”,如下圖所示苦始。
各類的結(jié)構(gòu)體就是基于 objc_class 結(jié)構(gòu)體的 class_t 結(jié)構(gòu)體寞钥。class_t 結(jié)構(gòu)體在 obj4 運行時庫聲明如下:
struct class_t {
struct class_t *isa;
struct class_t *superclass;
Cache cache;
IMP *vtable;
uintptr_t data_NEVER_USE;
};
回到之前的 Block 結(jié)構(gòu)體,得知 _NSConcreteStackBlock
盈简,相當(dāng)于 class_t 結(jié)構(gòu)體實例。在將 Block 作為 Objective-C 的對象處理時太示,關(guān)于該類的信息放置于 _NSConcreteStackBlock
柠贤,信息包含成員變量、方法名稱类缤、方法的實現(xiàn)(即函數(shù)指針)臼勉、屬性以及父類的指針。
到此餐弱,我們已經(jīng)了解 Block 的實質(zhì)宴霸,知道 Block 即為 Objective-C 的對象了。
2膏蚓、截獲自動變量值
C代碼
int dmy = 256;
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();
源代碼
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;
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;
}
};
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
z printf(fmt, val);
}
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)};
int main(int argc, const char * argv[]) {
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 values were changed. val = %d\n";
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
我們挑出部分代碼
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *fmt;
int val;
};
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
z printf(fmt, val);
}
從clang 編譯后自帶的注釋得知是值復(fù)制,不是指針復(fù)制瓢谢,不會受外界影響。再加上形參 __cself 是 block 自身驮瞧,也就是說氓扛, val 值是執(zhí)行block 內(nèi)部語句時的值,也就是 10论笔,這就是所謂的截獲自動變量值原理采郎。
3、__block 說明符
上面說到狂魔,添加__block 說明符則可以在 Block 內(nèi)部修改截獲的值蒜埋,那原理是怎樣的呢
C++代碼
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
發(fā)現(xiàn)添加了__block 說明符,它竟然變成了結(jié)構(gòu)體實例最楷,結(jié)構(gòu)體最后一個成員變量 val 相當(dāng)于原自動變量整份。
Block 會被復(fù)制到堆上待错,即存儲在堆上,所以外部變量也被復(fù)制到了堆上皂林,我們對加了
__block
關(guān)鍵字的外部變量進(jìn)行操作朗鸠,實際上是對已經(jīng)被copy到了堆區(qū)的變量進(jìn)行操作,而不是原來棧上的變量础倍。如果不加__block
烛占,變量在 Block 內(nèi)部只讀
4、Block 存儲域
Block 語法根據(jù)不同類型變量(下圖的類是轉(zhuǎn)換過后的類)沟启,存儲的位置也不同忆家。如下圖:
1、_NSConcreteStackBlock
截獲自動變量德迹,上面講得 Block 類都是 _NSConcreteStackBlock 類芽卿,存儲在棧上。
2胳搞、_NSConcreteGlobalBlock
記述全局變量的地方使用 Block 語法時卸例,生成的就是該類。存儲在數(shù)據(jù)區(qū)域肌毅。
例如下列代碼:
void (^blk)(void) = ^{ //此時將 Block 用結(jié)構(gòu)體實例設(shè)置在程序的數(shù)據(jù)區(qū)域筷转。
printf("Global Block\n");
};
int main(int argc, const char * argv[]) { ... }
3、_NSConcreteMallocBlock
在某種情況下悬而,Block 會被復(fù)制到堆上呜舒,即存儲在堆上。
例如下列代碼:
/**對于引用了外部變量的Block笨奠,如果沒有對他進(jìn)行copy袭蝗,*/
/**他的作用域只會在聲明他的函數(shù)棧內(nèi)(類型是__NSStackBlock__),*/
/**如果想在非ARC下直接返回此類Block般婆,Xcode會提示編譯錯誤的 */
typedef int(^MyBlock)();
MyBlock func()
{
//ARC
int i = 1;
return ^{ return i; };
}
/** ----------------------- */
typedef int(^MyBlock)();
MyBlock func()
{
//非ARC
int i = 1;
return [^{ return i; } copy]; 在這里修改一下就好了
}