iOS開發(fā)之淺談Block

目錄

  • block概要
  • 自動變量的截獲
  • block的調(diào)用本質(zhì)
  • block的內(nèi)存管理
  • block的循環(huán)引用

1.block概要

在剛接觸iOS的時候帘睦,block真是一個讓人頭疼的東西祷嘶,基本上所有的第三方框架都用了block蔗候,然后在我們閱讀這些大神框架的時候濒生,如果沒有學(xué)好block结耀,不了解block的用法抑党,那基本上無法把這些第三方框架給吃透润绎。

那么什么是block呢撬碟,我從一些書上得到的答案是:帶有自動變量的匿名函數(shù),顧名思義莉撇,匿名函數(shù)就是不帶有名稱的函數(shù)呢蛤。帶有自動變量這個意思我們在后面再具體講。下面我們看看block的語法:

block語法:

 ^ 返回值類型  參數(shù)列表 表達(dá)式

例如

^ int ( int  a){
    return a+1;
}
  • 一般在開發(fā)過程中棍郎,我們可以省略返回值類型
 ^  參數(shù)列表 表達(dá)式

例如

^( int  a){
    return a+1;
}

注意: 當(dāng)我們省略返回值時顾稀,如果表達(dá)式中有return語句,則返回值類型為該返回值的類型坝撑,如果沒有return語句静秆,則返回值類型為void類型粮揉。如果表達(dá)式中有多個return語句,所有的返回值必須是相同類型抚笔,不然會報錯扶认。 報錯信息為:

Return type '類型1 *' must match previous return type '類型2' when block literal has unspecified explicit return type
  • 其次在沒有參數(shù)的時候,我們還可以省略其參數(shù)
    例如
^{
    NSLog(@"省略參數(shù)");
}

Block類型變量

聲明BLock屬性的時候殊橙,需要定義一個屬性類型:

@property (nonatomic,copy) void (^cpBlock)(void);

 _cpBlock =   ^{
      NSLog(@"111");
    };

 _cpBlock();
  • 當(dāng)block作為參數(shù)的時候:
- (void)add:(void (^)(void))block{ 
    block(); 
}

[self add:^{
        NSLog(@"111");
    }];
  • 當(dāng)block作為返回值的時候:
- (void (^)(void))sum{
    
    return  ^{
        NSLog(@"111");
    };
    
}

[self sum]();

但是 我們一般在開發(fā)中并不會直接這么用辐宾,因?yàn)檫@樣寫也太繁瑣了。我們一般使用typedef來解決問題:

typedef void (^Blk)(void);

@property (nonatomic,copy) Blk block;

- (void)add:(Blk)block{
    
    block();
    
}

- (Blk)sum{
    
    return  ^{
        NSLog(@"111");
    };
    
}

2.自動變量的截獲

上面我們講了block是帶有自動變量的匿名函數(shù)膨蛮,我們已經(jīng)解釋了匿名函數(shù)叠纹,那么什么是帶有自動變量呢,我們先來看看下面的代碼:

typedef int (^Blk)(void);
int age = 10;
Blk block = ^{
    NSLog(@"age = %d", age);
};
age = 20;
block();

相信有點(diǎn)開發(fā)經(jīng)驗(yàn)的同學(xué)敞葛,應(yīng)該馬上知道結(jié)果:

結(jié)果是:age = 10

block在使用過程中誉察,會截獲表達(dá)式中所使用的自動變量的值,即保存該自動變量的瞬間值惹谐。因?yàn)閎lock在內(nèi)部保存了這個自動變量的值持偏,所以在執(zhí)行block語法后,即使在修改block中使用的自動變量的值也不會影響block執(zhí)行時自動變量的值氨肌。如果block表達(dá)式中不使用自動變量鸿秆,則不會截獲,因?yàn)榻孬@的自動變量會存儲于block的結(jié)構(gòu)體內(nèi)部, 會導(dǎo)致block體積變大怎囚。特別要注意的是默認(rèn)情況下block只能訪問不能修改局部變量的值卿叽。

截獲變量的類型

  1. 局部變量
對于基本數(shù)據(jù)類型的局部變量截獲其值
對于對象類型的局部變量連同所有權(quán)修飾符一起截獲
  1. 靜態(tài)局部變量
以指針形式截獲局部靜態(tài)變量
  1. 全局變量
不截獲
  1. 靜態(tài)全局變量
不截獲

__block修飾符

默認(rèn)情況下,block只能訪問不能修改局部變量的值恳守,而且在表達(dá)式后再修改block語法外聲明的自動變量考婴,無法影響block內(nèi)部的自動變量。那么如果我們想在block語法的表達(dá)式將值賦給block語法外聲明的自動變量井誉,則需要在該自動變量上附加__block修飾符蕉扮。

typedef void (^Blk)(void);
__block  int age = 10;
Blk block = ^{
    NSLog(@"age = %d", age);
    age = 5;
   NSLog(@"age = %d", age);
};
age = 20;
block();

結(jié)果是:
age = 20
age = 5

那么這是為什么呢,我們可以在命令行輸入代碼 clang -rewrite-objc 需要編譯的OC文件.m,看一下block底層是怎么實(shí)現(xiàn)的颗圣。
__block修飾的時候

__block  int age = 10;
        Blk block = ^{
           age喳钟;
        };
        age = 20;
        block();

轉(zhuǎn)化成cpp后是

Blk block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
struct __main_block_impl_0 {
  struct __block_impl impl;//封裝了函數(shù)實(shí)現(xiàn)的結(jié)構(gòu)體
  struct __main_block_desc_0* Desc;// 里面有內(nèi)存管理函數(shù),Block_size表示block的大小
  __Block_byref_age_0 *age; // by ref

  // 這個結(jié)構(gòu)體的初始化函數(shù) , 入?yún)?: fp,函數(shù)實(shí)現(xiàn)的函數(shù)指針, __main_block_desc_0,占用大小的描述,age傳入的是一個地址
    // 返回一個__main_block_impl_0類型的結(jié)構(gòu)體,賦值給了block
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;//棧block
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }

無__block修飾的時候

 int age = 10;
        Blk block = ^{
           age;
        };
        age = 20;
        block();

轉(zhuǎn)化成cpp后是

Blk block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

struct __main_block_impl_0 {
  struct __block_impl impl;//封裝了函數(shù)實(shí)現(xiàn)的結(jié)構(gòu)體
  struct __main_block_desc_0* Desc;// 里面有內(nèi)存管理函數(shù),Block_size表示block的大小
  int age;// 捕獲到的普通局部變量

  // 這個結(jié)構(gòu)體的初始化函數(shù) , 入?yún)?: fp,函數(shù)實(shí)現(xiàn)的函數(shù)指針, __main_block_desc_0,占用大小的描述
    // 返回一個__main_block_impl_0類型的結(jié)構(gòu)體,賦值給了block
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }

相比之下在岂,__block修飾后奔则,把變量變成來__Block_byref_age_0結(jié)構(gòu)體對象,而沒有__block修飾時蔽午,捕獲的是變量的瞬間值易茬,所以結(jié)果顯而易見。

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

這個結(jié)構(gòu)體有一個__forwarding指針,這個指針指向這個結(jié)構(gòu)體抽莱,通過這個__forwarding可以訪問結(jié)構(gòu)體的成員變量age

下面我們來看下下面的代碼:

{
    NSMutableArray *array =[NSMutableArray array];
    void (^Block)(void) = ^{
      [array addObject:@123];  
    };
    Block()
}

這段代碼需要給array加__block嗎

不需要范抓,因?yàn)檫@個只是使用,并沒有對array賦值

注意 靜態(tài)局部變量食铐、全局變量匕垫、靜態(tài)全局變量不需要添加__block

3.block的調(diào)用本質(zhì),

block的調(diào)用其實(shí)就是函數(shù)的調(diào)用,我們通過源碼去看

(1)block定義的時候虐呻,把表達(dá)式傳給__main_block_func_0

 Blk block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

看一下__main_block_impl_0的結(jié)構(gòu)體實(shí)現(xiàn):

struct __main_block_impl_0 {
  struct __block_impl impl;//封裝了函數(shù)實(shí)現(xiàn)的結(jié)構(gòu)體
  struct __main_block_desc_0* Desc;/ 里面有內(nèi)存管理函數(shù),Block_size表示block的大小
  __Block_byref_age_0 *age; // by ref
//構(gòu)造函數(shù)
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
//把__main_block_func_0函數(shù)賦值給__block_impl結(jié)構(gòu)體的FuncPtr
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

再看下這個函數(shù)實(shí)現(xiàn)結(jié)構(gòu)體__block_impl的代碼:

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

(2)然后我們再看__main_block_func_0的函數(shù)定義

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

            age;
        }

(3) 然后再看下block調(diào)用的代碼

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

先是把block強(qiáng)轉(zhuǎn)成__block_impl類型象泵,然后取出FuncPtr屬性,而這個屬性就是__main_block_impl_0結(jié)構(gòu)體里面構(gòu)造函數(shù)的fp斟叼,也就是(1)里面的__main_block_func_0函數(shù)偶惠。

所以,block的調(diào)用其實(shí)就是__main_block_func_0函數(shù)的調(diào)用朗涩,也就是表達(dá)式的調(diào)用忽孽。

4.block的內(nèi)存管理

block的存儲域

block是一個對象,根據(jù)block對象的存儲域馋缅,可以分為以下幾種:

1._NSConcreteStackBlock    存儲在棧上 
2._NSConcreteGlobalBlock   存儲在程序的數(shù)據(jù)區(qū)域(.data區(qū))
3._NSConcreteMallocBlock   存儲在堆上

根據(jù)結(jié)構(gòu)體__block_impl里面的的isa指針扒腕,可以判斷block的存儲域

Block的Copy操作

1.對于棧上的block绢淀,copy后會在堆上產(chǎn)生Block
2.對于已初始化數(shù)據(jù)取的全局block萤悴,copy后什么也不做
3.對于堆上的block,copy后會增加引用計(jì)數(shù)

那么什么時候block會被copy呢皆的?

1.調(diào)用Block的copy實(shí)例方法時
2.Block作為函數(shù)返回值返回時
3.將Block賦值給附有__strong修飾符id類型的類或Block類型成員變量時
4.在方法名中含有usingBlock的Cocoa框架方法或GCD的Api中傳遞Block時

__block變量的存儲域

上面我們講了block的復(fù)制覆履,那么對__block變量又是怎么處理的呢?

__block變量的配置存儲域 Block從棧復(fù)制到堆時的影響
從棧復(fù)制到堆并被Block持有
被Block持有

(1)若只有1個block中使用__block變量费薄,則當(dāng)該Block從棧復(fù)制到堆時硝全,使用的所有__block變量也必定配置在棧上,在復(fù)制block的時候楞抡,也會把__block變量從棧復(fù)制到堆伟众。此時block持有__block變量。如果block已經(jīng)在堆上召廷,再進(jìn)行copy凳厢,那么堆block所使用的__block變量沒有任何影響。

(2) 若多個block中使用__block變量時竞慢,因?yàn)樽钕葘⑺械腷lock配置在棧上先紫,所以 __block變量也會配置在棧上。在任何一個block從棧復(fù)制到堆時筹煮,__block變量也會一并從棧復(fù)制到堆并被該block持有遮精。當(dāng)剩下的block從棧復(fù)制到堆時,被復(fù)制的block持有__block變量败潦,并增加__block變量的引用計(jì)數(shù)本冲。

注意 當(dāng)__block變量從棧復(fù)制到堆時准脂,會同時把棧上的__block變量的__forwarding指針指向堆上的__block變量,通過這一操作檬洞,無論在Block表達(dá)意狠、Block表達(dá)式外使用__block變量,不管__block變量配置在棧上還是堆上疮胖,都可以順利地訪問同一個__block變量环戈。

總結(jié):__forwarding的意義:不論在任何內(nèi)存位置,都可以順利的訪問同一個__block變量澎灸。

5.block的循環(huán)引用

在開發(fā)過程中院塞,block的循環(huán)引用應(yīng)該是我們經(jīng)常碰到也讓人頭疼的問題。比如我們看下面的代碼塊:

_array = [NSMutableArray arrayWithObject:@"block"];
_cpBlock = ^NSString *(NSString *str){
          return [NSString stringWithFormat:@"hello_%@",_array[0]];
}; 

_cokong(@"hello");

這個代碼塊有什么問題的性昭,相信有開發(fā)經(jīng)驗(yàn)的同學(xué)應(yīng)該能立馬看出來拦止,這段代碼有block循環(huán)引用的問題。

因?yàn)槲覀儺?dāng)前block是用copy修飾的糜颠,而array是用strong修飾的,而在截獲變量中汹族,我們知道,關(guān)于block中對象類型的局部變量會連同其屬性關(guān)鍵字一起截獲其兴,所以在block中有一個strong指針指向self顶瞒,所以造成了自循環(huán)引用。

那么我們怎么去解決呢元旬?
(1)我們可以使用__weak 來解決循環(huán)引用榴徐,如下代碼塊

_array = [NSMutableArray arrayWithObject:@"block"];
  __weak NSArray *weakArray = _ayyay;
_cpBlock = ^NSString *(NSString *str){
          return [NSString stringWithFormat:@"hello_%@",weakArray[0]];
}; 

_cokong(@"hello");

這樣,因?yàn)閷ο蠼孬@的時候會連同其屬性關(guān)鍵字一起截獲匀归,這邊block指向的是一個__weak修飾的self坑资,所以不會造成循環(huán)引用。

(2)同理穆端,我們可以使用__unsafe_unretained來解決循環(huán)引用袱贮。如下代碼塊

_array = [NSMutableArray arrayWithObject:@"block"];
 _unsafe_unretained NSArray *unsafe_unretainedArray = _ayyay;
_cpBlock = ^NSString *(NSString *str){
          return [NSString stringWithFormat:@"hello_%@",unsafe_unretainedArray[0]];
}; 

_cokong(@"hello");

因?yàn)槌钟衏pBlock的self一定存在,所以使用__unsafe_unretainedArray修飾符体啰,不必?fù)?dān)心懸垂指針攒巍。

(3)我們還可以通過__block來解決循環(huán)引用狡赐。如下代碼塊:


 _block id blockSelf = self;
_cpBlock = ^int(NSString *str){
          return blockSelf.a;
}; 

_cokong(@"hello");

其實(shí)上面的代碼在MRC下一點(diǎn)問題都沒有常柄,但是在ARC下存在問題。

用__block修飾self后,self持有cpBlock,cpBlock持有self相种,這樣會造成循環(huán)引用寝并。所以我們要在block內(nèi)部做一個斷環(huán)的操作衬潦,才能解決循環(huán)引用。如下代碼塊:


 _block id blockSelf = self;
_cpBlock = ^int(NSString *str){
           int a = blockSelf.a;
          blockSelf = nil;//斷環(huán)
          return blockSelf.a;
}; 

_cokong(@"hello");

但是其實(shí)漂羊,這樣做還是有點(diǎn)問題,因?yàn)槿绻覀冎皇嵌x了block买喧,而一直不去調(diào)用block,那么就永遠(yuǎn)不能斷環(huán)算柳,循環(huán)引用就一直存在。

總的來說: __block相比較于上面兩種方法囱淋,優(yōu)點(diǎn)在于,__block變量可以控制對象的持有時間税手。缺點(diǎn)在于為了避免循環(huán)引用艺挪,必須執(zhí)行block。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市国瓮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌孵睬,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡郭怪,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來颖医,“玉大人僚祷,你說我怎么就攤上這事俺榆∽岸撸” “怎么了罐脊?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵萍桌,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮躺孝,結(jié)果婚禮上享扔,老公的妹妹穿的比我還像新娘。我一直安慰自己于个,他們只是感情好氛魁,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著厅篓,像睡著了一般秀存。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上羽氮,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天或链,我揣著相機(jī)與錄音,去河邊找鬼档押。 笑死澳盐,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的令宿。 我是一名探鬼主播洞就,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼掀淘!你這毒婦竟也來了旬蟋?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤革娄,失蹤者是張志新(化名)和其女友劉穎倾贰,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拦惋,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡匆浙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了厕妖。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片首尼。...
    茶點(diǎn)故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖言秸,靈堂內(nèi)的尸體忽然破棺而出软能,到底是詐尸還是另有隱情,我是刑警寧澤举畸,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布查排,位于F島的核電站,受9級特大地震影響抄沮,放射性物質(zhì)發(fā)生泄漏跋核。R本人自食惡果不足惜岖瑰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望砂代。 院中可真熱鬧蹋订,春花似錦、人聲如沸刻伊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽娃圆。三九已至玫锋,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間讼呢,已是汗流浹背撩鹿。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留悦屏,地道東北人节沦。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像础爬,于是被迫代替她去往敵國和親甫贯。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評論 2 353

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