3. __block ?__weak ?__strong ? 這都是做什么的

1.1 局部變量

局部自動變量,在Block中可被讀取。Block定義時copy變量的值,在Block中作為常量使用,所以即使變量的值在Block外改變,也不影響他在Block中的值,Block此時對局部變量只是做了值傳遞的操作。

1.2 static 修飾的全局變量

因為全局變量或靜態變量在內存中的地址是固定的,Block在讀取該變量值的時候是直接從其所在內存地址讀出,獲取到的是最新值,而不是在定義時copy的常量。

1.3 對OC對象的截獲

NSMutableArray?*array?=?[NSMutableArray?array];

void(^block)()?=?^(){

NSObject?*obj?=?[[NSObject?alloc]?init];

[array?addObject:obj];

};

block();

上述代碼編譯通過,Block截獲的值為NSMutableArray類的對象,用C語言表述,就是用的NSMutableArray類的對象所用的結構體實例的指針,所以向該對象中添加元素操作屬于使用截獲變量的值,因此是沒有問題的。那么對該截獲的變量進行賦值


屏幕快照 2016-06-23 下午2.12.28.png

編譯未通過,提示缺少__block修飾符。

1.4 C語言數組



屏幕快照 2016-06-23 下午2.18.28.png

上圖代碼中, 在Block外部定義一個C語言字符串字面量數組, 在Block內部截獲自動變量的方法并沒有實現對C語言數組的截獲, 此時訪問數組元素text[2]會報錯. 此時使用指針可以解決.

const?char?*text?=?"adsdczv";

void(^block)()?=?^(){

NSLog(@"%c",text[2]);

};

block();

1.5__block 修飾的變量

某些場景下,我們需要在Block內部對外部變量進行修改。這時需要使用__block來修飾該變量實現在Block內部的修改,此時Block是復制其引用地址來實現訪問的。

關于__block 修飾符

從上面講解我們已經知道,Block內部能夠讀取外部局部變量的值。但如果我們需要在Block內部修改變量的值,則需要在Block外部給該變量添加一個__block修飾符。

__block另一個使用場景是,避免某些情況下Block使用中出現的循環引用的問題,此時可以給相應的對象加上一個__block來修飾。

為什么使用__block可以實現在Block內部修改外部變量的值?

這邊我們用一個Block代碼,并使用clang _rewrite_objc命令轉換成C++的代碼來說明__block是怎么實現內部變量的修改。

Block在main中實現

int main(int argc, const char * argv[]) {

@autoreleasepool {

NSInteger val = 10;

void (^block)(void) = ^{

NSLog(@"%ld", val);

};

block();

}

return 0;

}

轉碼后:

struct __main_block_impl_0 {

struct __block_impl impl;

struct __main_block_desc_0* Desc;

int val;

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : 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) {

int val = __cself->val;? // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__val_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_41daf1_mi_0, 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[]) {

/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

int val = 10;

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

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

}

return 0;

}

從展開代碼可以發現,Block被轉成了一個struct __main_block_impl_0類型的結構體實例,并且該結構體成員中包含局部變量val。當執行Block時,通過該實例找到Block執行部分void __main_block_func_0,并把該結構體實例傳入到void __main_block_func_0方法中。

void __main_block_func_0方法中第一個參數聲明如下

struct __main_block_impl_0 *__cself

注意:這里的__cself就類似于OC中的self,而它指向結構體的指針。

此時我們就可以通過__cself->val訪問該局部變量。

那么問題來了,為什么此時不對變量val進行修改?

因為main函數中的局部變量val和函數__main_block_func_0不在同一個作用域中,調用過程中只是進行了值傳遞。

當然,在上面代碼中,我們可以通過指針來實現局部變量的修改。不過這是由于在調用__main_block_func_0時,main函數棧還沒展開完成,變量val還在棧中。

但是在很多情況下,Block是作為參數傳遞以供后續回調執行的。通常在這些情況下,Block被執行時,定義時所在的函數棧已經被展開,局部變量已經不在棧中了,再用指針訪問會產生野指針錯誤。

所以,這類情況下對于auto類型的局部變量,不允許Block進行修改是合理的。

__block是如何實現變量修改的

此時使用更新后的代碼

添加__block修飾符后

int main(int argc, const char * argv[]) {

@autoreleasepool {

__block NSInteger val = 0;

void (^block)(void) = ^{

val = 1;

};

block();

NSLog(@"val = %ld", val);

}

return 0;

}

使用_rewrite_objc展開

struct?__Block_byref_val_0?{

void?*__isa;

__Block_byref_val_0?*__forwarding;

int?__flags;

int?__size;

NSInteger?val;

};

struct?__main_block_impl_0?{

struct?__block_impl?impl;

struct?__main_block_desc_0*?Desc;

__Block_byref_val_0?*val;?//?by?ref

__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;

}

};

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;

}

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*/);}

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(int?argc,?const?char?*?argv[])?{

/*?@autoreleasepool?*/?{?__AtAutoreleasePool?__autoreleasepool;

__attribute__((__blocks__(byref)))?__Block_byref_val_0?val?=?{(void*)0,(__Block_byref_val_0?*)&val,?0,?sizeof(__Block_byref_val_0),?0};

void(*block)(void)?=?((void?(*)())&__main_block_impl_0((void?*)__main_block_func_0,?&__main_block_desc_0_DATA,?(__Block_byref_val_0?*)&val,?570425344));

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

NSLog((NSString?*)&__NSConstantStringImpl__var_folders_2h_70k4gzp53qn7ytk0cdjr9kk80000gn_T_main_7eb9e7_mi_0,(val.__forwarding->val));

}

return?0;

這次轉碼后似乎比剛才多了些東西,仔細看下,

一個是__Block_byref_val_0的結構體以及兩個方法static void __main_block_copy_0和static void __main_block_dispose_0; 后面的兩個方法先暫且不關注(后面會涉及)。

其實結構體__Block_byref_val_0產生的實例就是我們使用__block修飾過的變量。

struct __Block_byref_val_0 {

void *__isa;

__Block_byref_val_0 *__forwarding;

int __flags;

int __size;

NSInteger val;

};

從該結構體聲明可以看出,這個結構體中包含了該實例本身的引用 __forwarding。

我們從上述被轉化的代碼中可以看出 Block 本身也一樣被轉換成了__main_block_impl_0結構體實例,該實例持有__Block_byref_val_0結構體實例的指針。

我們再看一下Block實現和調用部分代碼被轉化后的結果:

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;

}

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

不難發現從__cself找到__Block_byref_val_0結構體實例,然后通過該實例的__forwarding訪問成員變量val。成員變量val是該實例自身持有的變量,指向的是原來的局部變量。

詳情參見下圖:


__block.jpg

至此,已經展示了__block變量在Block中查找和修改的過程,那么:

當Block作為回調執行時,局部變量val已經出棧了,這個時候代碼為什么還能正常工作呢?

我們為什么通過成員變量__forwarding而不是直接去訪問結構體中我們需要修改的變量呢?

我們在上述轉換過的代碼中可以發現__main_block_impl_0結構體構造函數中,isa指針指向的是_NSConcreteStackBlock; 而Block還有另外兩個與之相似的類:

_NSConcreteGlobalBlock //全局的靜態block 不會訪問任何外部變量

_NSConcreteMallocBlock //保存在堆區的,引用計數為0時會被銷毀。

_NSConcreteStackBlock //保存在棧區,出棧后被銷毀

上述示例代碼中,Block是被設為_NSConcreteStackBlock,在棧上生成。當我們把Block作為全局變量使用時,對應生成的Block將被設為_NSConcreteGlobalBlock

void?(^block)(void)?=?^{NSLog(@"This?is?a?Global?Block");};

int?main(int?argc,?const?char?*?argv[])?{

@autoreleasepool?{

block();

}

return?0;

}

該代碼轉碼c++后,Block結構體的isa指針初始化時如下:

impl.isa = &_NSConcreteGlobalBlock;

那_NSConcreteMallocBlock何時被使用

分配在全局變量上的Block,在變量作用域外也可以通過指針安全的訪問。

但分配在棧上的Block,如果它所屬的變量作用域結束,該Block就被廢棄。同樣,__block變量也分配在棧上,當超過該變量的作用域時,該__block變量也會被廢棄。

此時,就需要使用_NSConcreteMallocBlock,OC中提供了將Block和__block變量從棧上復制到堆上的方法,將分配到棧上的Block復制到堆上,這樣當棧上的Block超過它原本作用域時,堆上的Block還可以繼續存在。

復制到堆上的Block,它的結構體成員變量isa將變為:

impl.isa = &_NSConcreteMallocBlock;

而_block變量中結構體成員__forwarding就在此時保證了從棧上復制到堆上能夠正確訪問__block變量。在這種情況下,只要棧上的_block變量的成員變量__forwarding指向堆上的實例,我們就能夠正確訪問。

我們一般可以使用copy方法手動將 Block 或者 __block變量從棧復制到堆上。比如我們把Block做為類的屬性訪問時,我們一般把該屬性設為copy。有些情況下我們可以不用手動復制,比如Cocoa框架中使用含有usingBlock方法名的方法時,或者GCD的API中傳遞Block時。

當一個Block從棧復制到堆中,與之相關的__block變量也會被復制到堆中。此時堆中的Block持有相應堆上的__block變量,當堆上的__block變量沒有持有者,才會被釋放。

而在棧上的__block變量被復制到堆上之后,會將成員變量__forwarding的值替換為堆上的__block變量的地址。這個時候我們可以通過以下代碼訪問:

val.__forwarding->val

如下圖:



__block變量和循環引用問題

__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*/); }

當Block從棧復制到堆時,會調用_Block_object_assign函數持有該變量(相當于retain);

當堆上的Block被廢棄時,會調用_Block_object_dispose函數釋放該變量(相當于release)。

由上文描述可知,我們可以使用下述代碼解除Block循環引用的問題:

__block?id?tmp?=?self;

void(^block)(void)?=?^{

tmp?=?nil;

};

block();

通過執行block方法,nil被賦值到_block變量tmp中。這個時候_block變量對 self 的強引用失效,從而避免循環引用的問題。

總結:

通過__block變量可以控制對象的生命周期,在不能使用__weak修飾符的環境中,我們可以避免使用__unsafe_unretained修飾符。

在執行Block時可動態地決定是否將nil或者其它對象賦值給__block變量。

但是這種方法有一個明顯的缺點就是,我們必須去執行Block才能夠解除循環引用問題,否則就會出現問題。

4. 比較__weak 和 __strong

這邊用AFN中的一段代碼

__weak?__typeof(self)weakSelf?=?self;

AFNetworkReachabilityStatusBlock?callback?=?^(AFNetworkReachabilityStatus?status)?{

__strong?__typeof(weakSelf)strongSelf?=?weakSelf;

strongSelf.networkReachabilityStatus?=?status;

if?(strongSelf.networkReachabilityStatusBlock)?{

strongSelf.networkReachabilityStatusBlock(status);

}

};

1. __weak

我們在使用Block時,有時候會用到self,而Block內部對self默認都是強引用。在ARC下,編譯器將Block從棧區拷貝到堆區,Block會強引用和持有self,而self 也會強引用和持有Block,于是就造成了循環引用。

此時就需要使用__weak,在修飾變量時,修飾符修飾變量self,讓 block 不強引用self,從而破除循環。

__weak?typeof(self)?weakSelf?=?self;

self.passValueBlock?=?^(NSString?*string){

dispatch_async(dispatch_get_main_queue(),?^{

weakSelf.pointView.startLabel.text?=?string;

});

};

弱引用不會影響對象釋放,當一個對象被釋放是,所有指向它的弱引用會被置空,也避免出現野指針。

2. __strong

上面提到,__weak很好的解決retain Cycle,但還是會存在一些隱患。不知道self什么時候被釋放,為了保證在Block內部不會被釋放,所以使用__strong修飾。

看下一段測試代碼

在ViewController添加屬性

@property (nonatomic, strong) ViewController *vc;

viewDidLoad中

ViewController?*vc?=?[[ViewController?alloc]?init];

self.vc?=?vc;

__weak?ViewController?*?weakVC?=?self.vc;

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,?0),?^{

NSInteger?count?=?0;

while?(count?<?4)?{

count++;

NSLog(@"%@",weakVC);

sleep(1);

}

});

dispatch_after(dispatch_time(DISPATCH_TIME_NOW,?(int64_t)(2.0?*?NSEC_PER_SEC)),?dispatch_get_main_queue(),?^{

self.vc?=?nil;

});

實現dealloc方法

-?(void)dealloc?{

NSLog(@"%@",[self?class]);

}

看輸出結果:

2016-06-20?15:12:27.797?__strongTest[14823:1753981]?

2016-06-20?15:12:28.802?__strongTest[14823:1753981]?

2016-06-20?15:12:29.797?__strongTest[14823:1753934]?ViewController

2016-06-20?15:12:29.804?__strongTest[14823:1753981]?(null)

2016-06-20?15:12:30.808?__strongTest[14823:1753981]?(null)

可以看出:Block內部的對self.vc是弱引用。當2s后,self.vc在外部被釋放,則Block內部對self.vc的持有也失效。

現在在Block內部對self.vc進行強引用,Block內部代碼調整為:

__strong?ViewController?*strongVC?=?weakVC;

NSInteger?count?=?0;

while?(count?<?4)

count++;

NSLog(@"%@",strongVC);

sleep(1);

}

再看輸出結果:

2016-06-20?15:22:38.423?__strongTest[14839:1762881]?

2016-06-20?15:22:39.424?__strongTest[14839:1762881]?

2016-06-20?15:22:40.429?__strongTest[14839:1762881]?

2016-06-20?15:22:41.430?__strongTest[14839:1762881]?

2016-06-20?15:22:42.431?__strongTest[14839:1762835]?ViewController

Block內部對對象采用strong修飾后,既使原持有對象在block外部已經被釋放,但Block內部扔能持有,于是執行完Block后,該對象才被dealloc。

總結:weakSelf是為了Block不持有self,避免循環引用,而再聲明一個strongSelf是因為一旦進入Block執行,就不允許self在這個執行過程中釋放。Block執行完后這個strongSelf會自動釋放,沒有循環引用問題。

最后,使用Block時的注意事項

1.Block內部不能直接修改局部變量

Block內部可以訪問外部的變量, 默認是將其拷貝到其數據結構中來實現訪問的, 屬性是只讀的. Block內部不能修改外面的局部變量.

如果要修改需要對要修改的局部變量用__block修飾, 這樣局部變量就可以在Block內部修改了,Block是復制其引用地址來實現訪問的

2.當Block里面的出現self,造成的循環引用

循環引用就是當self 擁有一個Block的時候,在Block中又調用self的方法。形成了你中有我,我中有你,造成誰都無法將誰釋放。從而發生內存泄漏。

解決方法:

__weak typeof (self) weakSelf = self;

定義一個weakSelf變量并加上__weak修飾符,在Block代碼塊中,所有需要self的地方都用weakSelf來替代。這樣就不會增加引用計數,所以Block持有self對象也就不會造成循環引用,從而避免內存泄漏。

參考

Objective-C中的Block

Objective-C中Block的存儲域

__block &__weak & __strong

Objective-C 高級編程: iOS和OS X多線程和內存管理

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容

  • Blocks Blocks Blocks 是帶有局部變量的匿名函數 截取自動變量值 int main(){ ...
    南京小伙閱讀 952評論 1 3
  • 前言 Blocks是C語言的擴充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了這...
    小人不才閱讀 3,778評論 0 23
  • Block基礎回顧 1.什么是Block? 帶有局部變量的匿名函數(名字不重要,知道怎么用就行),差不多就與C語言...
    Bugfix閱讀 6,786評論 5 61
  • 摘要block是2010年WWDC蘋果為Objective-C提供的一個新特性,它為我們開發提供了便利,比如GCD...
    西門吹雪123閱讀 932評論 0 4
  • 2.1 Blcoks概要 2.1.1 什么是Blocks Blocks是C語言的擴充功能——“帶有自動變量(即局部...
    SkyMing一C閱讀 2,366評論 6 18