Block的引用迴圈問題 (ARC & non-ARC),block引用迴圈arc
Block實現原理
首先探究下Block的實現原理,由於Objective-C是C語言的超集,既然OC中的NSObject對象其實是由C語言的struct+isa指標實現的,那麼Block的內部實現估計也一樣,以下三篇Blog對Block的實現機製做了詳細研究:
- A look inside blocks: Episode 1
- A look inside blocks: Episode 2
- A look inside blocks: Episode 3
雖然實現細節看著頭痛,不過發現Block果然是和OC中的NSObject類似,也是用struct實現出來的東西。這個是LLVM項目compiler-rt分析的block頭文Block_private.h標頭檔中關於Block的struct聲明:
123456789101112131415 |
struct Block_descriptor { unsigned long int reserved; unsigned long int size; void (*copy)(void *dst, void *src); void (*dispose)(void *);};struct Block_layout { void *isa; int flags; int reserved; void (*invoke)(void *, ...); struct Block_descriptor *descriptor; /* Imported variables. */};
|
我們發現Block_layout中也有一個isa指標,像極了NSobject內部實現struct中的isa指標。這裡的isa可能指向三種類型之一的Block:
- _NSConcreteGlobalBlock:全域類型Block,在編譯器就已經確定,直接放在程式碼片段__TEXT上。直接在NSLog中列印的類型為__NSGlobalBlock__。
- _NSConcreteStackBlock:位於棧上分配的Block,即__NSStackBlock__。
- _NSConcreteMallocBlock:位於堆上分配的Block,即__NSMallocBlock__。
為什麼會有這麼多種類呢?首先來看全域類型Block,看例子:
123456789101112 |
void addBlock(NSMutableArray *array) { [array addObject:^{ printf("global block\n"); }];} void example() { NSMutableArray *array = [NSMutableArray array]; addBlock(array); void (^block)() = [array objectAtIndex:0]; block();}
|
為什麼addBlock中添加到array中的Block屬於全域Block呢?因為它不需要運行時(Runtime)任何的狀態來改變行為,不需要放在堆上或者棧上,直接編譯後在程式碼片段中即可,就像個c函數一樣。這種類型的Block在ARC和non-ARC情況下沒有差別。
這個Block訪問了範圍外的變數d,在實現上就是這個block會多一個成員變數對應這個d,在賦值block時會將方法exmpale中的d變數值複製到成員變數中,從而實現訪問。
1234567 |
void example() { int d = 5; void (^block)() = ^() { printf("%d\n", d); }; block();}
|
如果要修改d呢?:
123456789 |
void example() { int d = 5; void (^block)() = ^() { d++; printf("%d\n", d); }; block(); printf("%d\n", d);}
|
由於局部變數d和這個block的實現不在同一範圍,僅僅在調用過程中用到了值傳遞,所以不能直接修改,而需要加一個標識符__block int d = 5;
,那麼block就可以實現對這個局部變數的修改了。如果是這種block標識的變數,在Block實現中不再是簡單的一個成員變數,而是對應一個新的結構體表示這個block變數。block的本質是引入了一個新的Block_byref{$var_name}{$index}結構體,被block關鍵字修飾的變數就被放到這個結構體中。另外,block結構體通過引入Block_byref{$var_name}{$index}指標類型的成員,得以間接訪問到Block的外部變數。這樣對Block外的變數訪問從值傳遞轉變為引用,從而有了修改內容的能力。
正常我們使用Block是在棧上產生的,離開了棧範圍便釋放了,如果copy一個Block,那麼會將這個Block copy到堆上分配,這樣就不再受棧的限制,可以隨意使用啦。例如:
1234567891011121314 |
typedef void (^TestBlock)(); TestBlock getBlock() { char e = 'E'; void (^returnedBlock)() = ^{ printf("%c\n", e); }; return returnedBlock;} void example() { TestBlock block = getBlock(); block();}
|
函數getBlock中聲明並賦值的returnedBlock,一開始是在棧上分配的,屬於NSStackBlock,如果是non-ARC情況下return這個NSStackBlock,那麼其實已經被銷毀了,在函數中example()使用時就會crash。如果是ARC情況下,getBlock返回的block會自動copy到堆上,那麼block的類型就是NSMallocBlock,可以在example()中繼續使用。要在Non-ARC情況下正常運行,那麼就應該修改為:
1234567 |
TestBlock getBlock() { char e = 'E'; void (^returnedBlock)() = ^{ printf("%c\n", e); }; return [[returnedBlock copy] autorelease];}
|
Block中的循環參考問題
扯了這麼多,回到Block的循環參考問題,由於我們很多行為會導致Block的copy,而當Block被copy時,會對block中用到的對象產生強引用(ARC下)或者引用計數加一(non-ARC下)。
如果遇到這種情況:
123456789 |
@property(nonatomic, readwrite, copy) completionBlock completionBlock;//========================================self.completionBlock = ^ { if (self.success) { self.success(self.responseData); } }};
|
對象有一個Block屬性,然而這個Block屬性中又引用了對象的其他成員變數,那麼就會對這個變數本身產生強應用,那麼變數本身和他自己的Block屬性就形成了循環參考。在ARC下需要修改成這樣:
123456789 |
@property(nonatomic, readwrite, copy) completionBlock completionBlock;//========================================__weak typeof(self) weakSelf = self;self.completionBlock = ^ { if (weakSelf.success) { weakSelf.success(weakSelf.responseData); }};
|
也就是產生一個對自身對象的弱引用,如果是倒黴催的項目還需要支援iOS4.3,就用__unsafe_unretained替代__weak。如果是non-ARC環境下就將__weak替換為__block即可。non-ARC情況下,__block變數的含義是在Block中引入一個新的結構體成員變數指向這個__block變數,那麼__block typeof(self) weakSelf = self;
就表示Block別再對self對象retain啦,這就打破了循環參考。