Objective-C高級編程之block篇

block是C語言的一項重要的特性钦购。在很多其他計算機語言中都有類似的概念,比如lamda表達式褂萧,閉包等押桃。那么block是什么?簡而言之导犹,block是帶有自動變量的匿名函數(shù)唱凯。本文將以這句話展開,對block進行深入解析谎痢。

block語法

block可以認為是匿名的C函數(shù)磕昼,它的語法格式是這樣的:

^ int (int i){
  return 0;
}

block變量

當(dāng)不需要返回值或函數(shù)參數(shù)時,也可以將這兩部分都省略掉节猿。我們可以將block代碼塊賦值給block類型的變量票从,用法是這樣的:

void (^blk)(int) = ^ void (int i) {
  printf("I am block.");
}

blk即為block類型的變量漫雕,它與普通的C語言變量是一樣的,可以用作自動變量峰鄙、函數(shù)參數(shù)浸间、靜態(tài)局部變量、靜態(tài)全局變量吟榴,全局變量魁蒜。可以看到吩翻,block類型的變量定義的方式跟其他變量很是不同兜看,直觀上不易閱讀,所以我們可以使用typedef來給block類型起一個別名仿野,例如這樣:

typedef void (^block_t)(int);
//使用block_t類型定義block變量
block_t blk;

這樣block類型的變量定義就和普通的C語言變量定義看起來一致了铣减,代碼也變得更加直觀。

block截獲的自動變量

理解了block是匿名函數(shù)脚作,再來看看帶有自動變量是怎么回事葫哗。事實上,這里指的是在block內(nèi)部可以截獲外部的自動變量值球涛,示例如下:

typedef void (^block_t)();
int main () 
{
    int a = 0;
    int b = 1;
    block_t blk = ^ () {
        printf("block: %d", a);
    }
    a = 2;
    blk();
    return 0;
}

在這里劣针,block截獲了自動變量a的值,即便之后a值改變亿扁,并不會影響block中截獲的值捺典,換句話說,上述代碼輸出的結(jié)果是0从祝,而不是2襟己。當(dāng)我們想在block中修改自動變量的值時,會報編譯錯誤牍陌,因為默認情況下擎浴,block內(nèi)是不能修改自動變量的值的。如果我們想要修改毒涧,需要在自動變量前加__block修飾符贮预。為什么加了此修飾符就可以實現(xiàn)修改自動變量呢,這個我們后面詳述契讲。我們先來看個例子來深入理解“不能修改自動變量值”這句話仿吞。

typedef void (^block_t)();
int main () 
{
    NSMutableArray *array = [[NSMutableArray alloc] init];
    block_t blk = ^ () {
        id object = [[NSObject alloc] init];
        [array addObject:object]; //正確 
        array = otherArray; //錯誤
    }
    blk();
    return 0;
}

雖然block內(nèi)不允許修改自動變量的值,這里截獲的是類的對象捡偏,用C語言來描述的話唤冈,這里截獲的是類對象對應(yīng)的結(jié)構(gòu)體實例的指針,我們不能修改指針银伟,但使用指針是沒有問題的你虹。需要指出的是凉当,block并未實現(xiàn)截獲C語言字符數(shù)組,所以我們無法在block內(nèi)使用外部定義的C語言字符數(shù)組售葡,不過我們可以用字符指針char*類型代替。

block實現(xiàn)原理

接下來我們通過分析源碼這種激動人心的方式來理解block的實現(xiàn)原理忠藤。我們可以利用clang這個工具將block代碼轉(zhuǎn)換成C++源碼來分析挟伙。我們在main.m文件中實現(xiàn)一段簡單的block代碼:

int main () {
    void (^blk)() = ^() {
        int a = 0;
    };
    blk();
}

接下來,我們使用clang -rewrite-objc main.m命令轉(zhuǎn)換為C++源碼模孩,由于源碼太長尖阔,我們將關(guān)鍵部分摘出來:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

  int a = 0;
 }

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 () {
 void (*blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
 ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}

通過對比,很容易發(fā)現(xiàn)我們的block代碼塊實際上轉(zhuǎn)換為了普通C語言函數(shù)__main_block_func_0榨咐,函數(shù)命名上表示該block是main函數(shù)中第一個block介却。函數(shù)的參數(shù)是一個指向__main_block_impl_0結(jié)構(gòu)體的指針,事實上這個結(jié)構(gòu)體就是block對應(yīng)的結(jié)構(gòu)體實例块茁。這個結(jié)構(gòu)體中只有兩個成員變量齿坷,其類型分別是__block_impl和__main_block_desc_0,__block_impl結(jié)構(gòu)體中記錄了block的一些信息数焊,例如函數(shù)指針等永淌,__main_block_desc_0結(jié)構(gòu)體中表示了block的size。另外還有一個構(gòu)造函數(shù)佩耳,對__main_block_impl_0的成員變量做初始化遂蛀。

最后我們來看下main函數(shù)。我們?nèi)サ衾锩娴念愋娃D(zhuǎn)換部分干厚,簡化如下:

struct __main_block_impl_0 tmp = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &tmp;

(*blk->impl.FuncPtr)(blk);

對比我們的block代碼李滴,前兩行表示了將block實現(xiàn)賦值給block類型的變量,從第三行可以看出蛮瞄,blk的調(diào)用變成了函數(shù)指針的調(diào)用所坯,同時函數(shù)指針中傳入了block變量作為參數(shù)。現(xiàn)在我們已經(jīng)明白裕坊,block其實是用C語言函數(shù)和結(jié)構(gòu)體來實現(xiàn)的包竹。

block截獲自動變量的原理

那么block是怎么截獲自動變量的呢?我們修改下block代碼籍凝,再用clang看下源碼實現(xiàn):

int main () {
    int a = 0;
    void (^blk)() = ^() {
        int b = a + 1;
    };
    blk();
}

轉(zhuǎn)換后我們來看下源碼相比之前有何變化:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

  int b = a + 1;
 }

可以發(fā)現(xiàn)周瞎,在block對應(yīng)的結(jié)構(gòu)體__main_block_impl_0中多了一個int型成員a,這個成員正是保存了截獲的自動變量a的值饵蒂,然后在__main_block_func_0函數(shù)中声诸,通過指向block自身的指針訪問成員a來獲取自動變量a的值。這下便清楚了退盯,block是將自動變量的值保存在block結(jié)構(gòu)體實例中彼乌,使用時再通過指向block的指針訪問泻肯,這也就解釋了為什么block內(nèi)無法修改外部自動變量值的問題。

__block修改自動變量值的原理

我們再來通過源碼分析:

int main () {
    int __block a = 0;
    void (^blk)() = ^() {
        a = 2;
    };
    blk();
}

轉(zhuǎn)換后發(fā)現(xiàn)源碼驟然增多慰照,我們摘出關(guān)鍵代碼來分析:

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref

        (a->__forwarding->a) = 2;
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

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};
int main () {
    __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 0};
    void (*blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}

我們來看引入__block后源碼發(fā)生的變化灶挟。我們依舊從__main_block_func_0這個函數(shù)入手,可以發(fā)現(xiàn)原本的自動變量竟然轉(zhuǎn)化成了一個結(jié)構(gòu)體毒租!再去看main函數(shù)稚铣,第一行就是使用自動變量a初始化了一個結(jié)構(gòu)體__Block_byref_a_0,在這個結(jié)構(gòu)體中墅垮,有一個成員變量a惕医,它保存了外部自動變量a的值,__forwarding是指向結(jié)構(gòu)體自身的指針算色。而在__main_block_impl_0結(jié)構(gòu)體中則是有一個指向__Block_byref_a_0結(jié)構(gòu)體的指針抬伺。這里就清楚了,__block變量轉(zhuǎn)換成結(jié)構(gòu)體時其地址也通過指針的方式被保存了下來灾梦,通過指針當(dāng)然可以實現(xiàn)修改自動變量值的目的(當(dāng)然前提是該自動變量沒有被廢棄峡钓,因為一旦作用域結(jié)束,棧上的變量就會被廢棄斥废,那么指針就成為野指針了椒楣。但事實上,block往往能跨其作用域而存在牡肉,并修改自動變量的值捧灰。這是為什么?我們后面詳細來說)统锤。

block修改靜態(tài)變量值的原理

值得一提的是毛俏,在block內(nèi),除了可以修改__block變量的值外饲窿,還可以修改全局變量煌寇,靜態(tài)全局變量,靜態(tài)局部變量的值逾雄。我們重點看一下靜態(tài)局部變量在源碼中是怎么處理的:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *a = __cself->a; // bound by copy

        (*a) = 2;
    }

可以看出阀溶,源碼里通過指向靜態(tài)局部變量的指針進行變量訪問和賦值。那為什么自動變量不采用這種方式來改值呢鸦泳?道理很簡單银锻,靜態(tài)局部變量存在于整個應(yīng)用程序生命周期中,不用擔(dān)心會出現(xiàn)野指針的問題做鹰,而自動變量在函數(shù)作用域結(jié)束時就被廢棄了击纬,指針也就無法訪問了。

現(xiàn)在還剩下的問題

到目前為止钾麸,我們還有兩個問題沒有詳細提及:

  • block為何可以超出其作用域而存在更振?
  • 加入__block修飾符后源碼中新增的copy與dispose方法是什么作用炕桨?

block超出作用域存在的原因

觀察block結(jié)構(gòu)體實例的實現(xiàn),可以發(fā)現(xiàn)在__block_impl結(jié)構(gòu)體中有一ISA指針肯腕。我們知道每一個OC對象的底層實現(xiàn)是一個包含了isa成員變量的結(jié)構(gòu)體献宫,而block結(jié)構(gòu)體正好符合了這一點。事實上实撒,block也是一個OC的對象遵蚜,其isa指向了block所屬的class,從源碼看奈惑,該class是_NSConcreteStackBlock,顧名思義睡汹,該class表示存在于棧上的block肴甸,事實上,block根據(jù)其存儲域可分為三類:

  • _NSConcreteGlobalBlock囚巴,存在于靜態(tài)數(shù)據(jù)區(qū)的block
  • _NSConcreteStackBlock原在,存在于棧上的block
  • _NSConcreteMallocBlock,存在于堆上的block

我們前面看到的block都是_NSConcreteStackBlock類型的彤叉,那么什么時候block會在數(shù)據(jù)區(qū)存儲呢庶柿?當(dāng)block不去截獲自動變量,其結(jié)構(gòu)體實例內(nèi)容不必在運行時才能確定秽浇,此時就會以單例形式存在數(shù)據(jù)區(qū)浮庐。而_NSConcreteMallocBlock正是block可以超出其作用域而存在的原因。舉個例子柬焕,block作為函數(shù)返回值返回時审残,棧上的block會因作用域結(jié)束而被釋放,故此時運行時會將block從棧上拷貝到堆上一份斑举,在這個過程中搅轿,堆上的block的ISA指針會被賦值_NSConcreteMallocBlock。這樣富玷,棧上的block廢棄后璧坟,我們依舊可以正確使用block。堆上的block的持有與釋放是服從引用計數(shù)的管理方式赎懦,和普通OC對象無異雀鹃。

在ARC下,多數(shù)情況編譯器可以自動判斷在合適的時候?qū)lock進行拷貝铲敛,例如像上面舉得例子褐澎。但是當(dāng)block作為方法的參數(shù)傳遞時,編譯器不會自動拷貝block伐蒋,我們需要自己判斷是否要對block對象調(diào)用copy方法工三,將棧上的block拷貝到堆上迁酸。例如以下情形拷貝就是必要的:

{
     void(^blk1)() = ^() {NSLog(@"block 1");};
     void(^blk2)() = ^() {NSLog(@"block 2");};
     NSArray *array = [NSArray arrayWithObjects:[blk1 copy], [blk2 copy], nil];
     return array;
}

不過如果在方法內(nèi)已經(jīng)對block進行了適當(dāng)?shù)膹?fù)制,那么傳遞的時候我們就不必在拷貝了俭正,比如cocoa框架中含有usingBlock的方法以及GCD中的方法奸鬓。
如果對已經(jīng)在堆上的block執(zhí)行copy方法,按照引用計數(shù)的管理方式掸读,block的引用計數(shù)會增加串远,而如果是靜態(tài)數(shù)據(jù)區(qū)的block,那么調(diào)用copy方法不會做任何事情儿惫,所以必要的時候執(zhí)行copy操作總是安全的澡罚。

__block變量的情況是類似的,當(dāng)棧上的block執(zhí)行copy操作時肾请,其所持有的棧上的__block變量也會隨之拷貝到堆上留搔,并被堆上的block所持有。從源碼可以知道铛铁,__block變量對應(yīng)的結(jié)構(gòu)體中有一指向自身結(jié)構(gòu)體的指針__forwarding隔显,當(dāng)__block變量拷貝到堆上時,堆上的__block變量的__forwarding指針指向自己饵逐,而棧上的__block變量的__forwarding指向堆上的__block變量結(jié)構(gòu)體括眠,故無論什么情況下,程序都可以正確找到__block變量對應(yīng)的結(jié)構(gòu)體倍权。因此當(dāng)棧上的block和__block變量被廢棄時掷豺,程序依舊可以使用堆上的block和__block變量,這就是block和__block變量可以超出作用域存在的原因薄声。

copy與dispose方法的作用

OC中的對象類型和id類型也可以被block所截獲萌业,這種情況下,在block對應(yīng)的結(jié)構(gòu)體實例中會有一個OC對象類型的成員變量奸柬,換句話說生年,OC對象成為了結(jié)構(gòu)體的成員變量,在內(nèi)存管理篇我們說過廓奕,OC對象類型不能作為結(jié)構(gòu)體的成員變量抱婉,因為編譯器無法正確判斷結(jié)構(gòu)體的持有和廢棄的生命周期,無法很好地管理OC對象的內(nèi)存桌粉,但是運行時可以準確把握block從椪艏ǎ拷貝到堆以及在堆上被廢棄的時機,因此這里OC對象也可以放入block的結(jié)構(gòu)體中铃肯。當(dāng)block從椈家冢拷貝到堆時,會調(diào)用copy方法,當(dāng)堆上的block不再被持有步藕,要被廢棄時惦界,調(diào)用dispose方法。
如果OC對象被指定為__block變量咙冗,那么和之前類似沾歪,會對應(yīng)生成一個__block變量的結(jié)構(gòu)體,結(jié)構(gòu)體中會保存OC對象雾消,同時還會有該對象自己的copy和dispose方法灾搏,當(dāng)block被拷貝到堆時,賦值給__block變量的OC對象會通過此copy方法被持有立润,而當(dāng)堆上的block要被廢棄時狂窑,__block變量持有的對象也應(yīng)該釋放,這時會通過此dispose方法進行釋放桑腮,這和block本身的copy和dispose邏輯是一樣的蕾域。

PS:關(guān)于block引起循環(huán)引用的問題,在內(nèi)存管理篇已經(jīng)提及到旦,這里不再詳述了。

更新@20170624

關(guān)于目前ARC下蘋果對于block的處理方式巨缘,事實上添忘,被__strong修飾的block在棧上生成后運行時會自動將其拷貝到堆上,并非前文所說只是在必要的時候才將block從椚羲拷貝到堆搁骑,即便是block僅在某個函數(shù)內(nèi)使用,不超出函數(shù)作用域又固。而被__weak修飾的block則在棧上生成后依舊會保持在棧上仲器。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市仰冠,隨后出現(xiàn)的幾起案子乏冀,更是在濱河造成了極大的恐慌,老刑警劉巖洋只,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辆沦,死亡現(xiàn)場離奇詭異,居然都是意外死亡识虚,警方通過查閱死者的電腦和手機肢扯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來担锤,“玉大人蔚晨,你說我怎么就攤上這事「匮” “怎么了铭腕?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵银择,是天一觀的道長。 經(jīng)常有香客問我谨履,道長欢摄,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任笋粟,我火速辦了婚禮怀挠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘害捕。我一直安慰自己绿淋,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布尝盼。 她就那樣靜靜地躺著吞滞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪盾沫。 梳的紋絲不亂的頭發(fā)上裁赠,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音赴精,去河邊找鬼佩捞。 笑死,一個胖子當(dāng)著我的面吹牛蕾哟,可吹牛的內(nèi)容都是我干的一忱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼谭确,長吁一口氣:“原來是場噩夢啊……” “哼帘营!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起逐哈,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤芬迄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后昂秃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體薯鼠,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年械蹋,在試婚紗的時候發(fā)現(xiàn)自己被綠了出皇。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡哗戈,死狀恐怖郊艘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤纱注,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布畏浆,位于F島的核電站,受9級特大地震影響狞贱,放射性物質(zhì)發(fā)生泄漏刻获。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一瞎嬉、第九天 我趴在偏房一處隱蔽的房頂上張望蝎毡。 院中可真熱鬧,春花似錦氧枣、人聲如沸沐兵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽晦款。三九已至坛吁,卻和暖如春弹灭,著一層夾襖步出監(jiān)牢的瞬間剂习,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工逊移, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留预吆,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓螟左,卻偏偏與公主長得像,于是被迫代替她去往敵國和親觅够。 傳聞我的和親對象是個殘疾皇子胶背,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

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