《Objective-C高級編程》溫故知新之"Blocks"

本文

前言

很久前看了《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)體實例指針”,如下圖所示苦始。


Objective-C 類與對象的實質(zhì)

各類的結(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)換過后的類)沟启,存儲的位置也不同忆家。如下圖:


Block 的類

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];   在這里修改一下就好了
        }

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末到腥,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蔚袍,更是在濱河造成了極大的恐慌左电,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件页响,死亡現(xiàn)場離奇詭異篓足,居然都是意外死亡,警方通過查閱死者的電腦和手機闰蚕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進(jìn)店門栈拖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人没陡,你說我怎么就攤上這事涩哟∷魃停” “怎么了?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵贴彼,是天一觀的道長潜腻。 經(jīng)常有香客問我,道長器仗,這世上最難降的妖魔是什么融涣? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮精钮,結(jié)果婚禮上威鹿,老公的妹妹穿的比我還像新娘。我一直安慰自己轨香,他們只是感情好忽你,可當(dāng)我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著臂容,像睡著了一般科雳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上脓杉,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天糟秘,我揣著相機與錄音,去河邊找鬼丽已。 笑死蚌堵,一個胖子當(dāng)著我的面吹牛买决,可吹牛的內(nèi)容都是我干的沛婴。 我是一名探鬼主播,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼督赤,長吁一口氣:“原來是場噩夢啊……” “哼嘁灯!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起躲舌,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤丑婿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后没卸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體羹奉,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年约计,在試婚紗的時候發(fā)現(xiàn)自己被綠了诀拭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡煤蚌,死狀恐怖耕挨,靈堂內(nèi)的尸體忽然破棺而出细卧,到底是詐尸還是另有隱情,我是刑警寧澤筒占,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布贪庙,位于F島的核電站,受9級特大地震影響翰苫,放射性物質(zhì)發(fā)生泄漏止邮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一革骨、第九天 我趴在偏房一處隱蔽的房頂上張望农尖。 院中可真熱鬧,春花似錦良哲、人聲如沸盛卡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽滑沧。三九已至,卻和暖如春巍实,著一層夾襖步出監(jiān)牢的瞬間滓技,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工棚潦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留令漂,地道東北人。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓丸边,卻偏偏與公主長得像叠必,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子妹窖,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,947評論 2 355

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