大部分內容來自
Block:帶有自動變數的匿名函式.是C語言的擴充功能.Block擁有捕獲外部變數的功能.在Block中訪問一個外部的區域性變數,Block會持有它的臨時狀態,自動捕獲變數值,外部區域性變數的變化不會影響它的狀態.
Block實質
在通過clange -rewrite-objec
檢視Block的原始碼可以看到,Block是作為引數進行了傳遞
//Block結構體
struct __main_block_impl_0 {
struct __block_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.Floags = flags;
impl.funcPtr = fp;
Desc = desc;
}
}
複製程式碼
其中isa = &_NSConcreteStackBlock
,將Block指標賦給Block的結構體成員變數isa.為了理解它,首先要理解Objective-c類和物件的實質.其實,Block就是Objective-c物件.
id
這一變數型別用於儲存Objective-c物件.id
為objc_object結構體的指標型別
typedef struct objc_object {
Class isa;
} *id;
複製程式碼
Class
為objc_class結構體的指標型別
typedef struct objc_class *Class
struct objc_class {
Class isa;
};
//這與objc_object結構體相同.然而,objc_object結構體和objc_class結構體歸根結底是在各個物件和類的實現中使用的最基本的結構體
複製程式碼
這裡需要理解一下Objective-c的類與物件的實質,各類的結構體就是基於objc_class結構體的class_t結構體.class_t結構體在objc4執行時庫中的宣告
struct class_t {
struct class_t *isa;
struct class_t *superclass;
Cache cache;
IMP *vtable;
uintptr_t data_NEVER_USE;
};
複製程式碼
class_t結構體例項持有宣告的成員變數,方法名稱,方法的實現(函式指標),屬性以及父類的指標,並被Objective-c執行時庫所使用.
在__main_block_impl_0結構體相當於基於objc_object結構體的objective-c類物件的結構體. 即_NSConcreteStackBlock相當於class_t結構體例項.在將Block作為Objective-c的物件處理時,關於該類的資訊放置於_NSConcreteStackBlock中.
截獲自動變數值
所謂的"截獲自動變數值"意味著在執行Block語法時,Block語法表示式所使用的自動變數值被儲存到Block的結構體例項(即Block自身)中. Block中使用自動變數後,在Block的結構體例項中重寫該自動變數也不會改變原先截獲的自動變數.這樣一來就無法在Block中儲存值了,解決這個問題有兩種方法:
- C語言有一個變數,允許Block改寫值:
靜態變數
靜態變數的指標傳遞給__main_block_impl_0結構體的建構函式並儲存.
- 使用
__block
說明符
C語言有一下儲存域類說明符: * typedef * extern * static :表示作為靜態變數儲存在資料區中 * auto :表示作為自動變數儲存在棧中 * register
__block
說明符類似於這些,用它來指定Block中想變更值的自動變數.
使用__block修飾一個變數,在Block的原始碼中會變成一個結構體例項.
__block int val = 10;
//轉換原始碼
__Block_byref_val_0 val = {
0,
&val,
0,
sizeof(__Block_byref_val_0),
10//該變數初始化為10,且這個值也出現在結構體例項的初始化中,這意味著該結構體持有相當於原自動變數的成員變數.
};
//該結構體宣告
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *forwarding;
int __flags:
int __size;
int val;
}
複製程式碼
在給__block變數賦值時,__Block_byref_val_0結構體例項的成員變數__forwarding持有指向該例項自身的指標.通過成員變數__forwarding訪問成員變數val.通過訪問val的指標,就可以從多個Block中使用同一個__block變數.
Block儲存域
Block的儲存,Block在棧堆上的問題
Block也是objective-c物件,具有這幾個類
類 | 設定物件的儲存域 |
---|---|
_NSConcreteStackBlock | 棧 |
_NSConcreteGlobalBlock | 程式的資料區域(.data區) |
_NSConcreteMallocBlock | 堆 |
__block變數用結構體成員變數__forwarding可以實現無論__block變數配置在棧上還是堆上是都能夠正確地訪問__block變數.
當ARC有效時,大多數情形下編譯器會恰當的進行判斷,自動生成將Block從棧上覆制到堆上的程式碼.
Block的copy總結
Block的類 | 副本源的配置儲存域 | 複製效果 |
---|---|---|
_NSConcreteStackBlock | 棧 | 從棧複製到堆 |
_NSConcreteGlobalBlock | 程式的資料區域(.data區) | 什麼也不做 |
_NSConcreteMallocBlock | 堆 | 引用結束增加 |
Block引起的迴圈引用
我們在設定Block之後,不希望再回撥Block時Block已經被釋放了,所以我們會對block進行copy,copy到堆中.
//這裡定義一個陣列,裡面新增兩個block,因為這兩個block沒有copy到堆上,所以當呼叫它時可能已經被釋放了
- (void)viewDidLoad {
[super viewDidLoad];
//建立陣列
id obj = [self getBlockArray];
//定義
typedef void (^blk_t)(void);
//賦值-獲取到obj陣列中的第一個元素
blk_t blk = (blk_t)[obj objectAtIndex:0];
//呼叫
blk();
}
-(id)getBlockArray {
int val = 10;
return [[NSArray alloc]initWithObjects:^{
NSLog(@"blk0:%d",val);
},^{
NSLog(@"blk0:%d",val);
}, nil];
}
複製程式碼
但在copy到堆上的時候,會retain其引用的外部變數,如果Block中引用了他的宿主物件,那就可能引起迴圈引用的問題. ARC下,有兩種方法可以解決迴圈引用問題:
- 事前避免(
__weak
和__strong
)
在使用block時,__weak修飾他的宿主物件,物件的引用計數不會+1.還需要在Block內部對弱引用物件進行一次強引用,這是因為僅用__weak修飾的物件,如果被釋放,那麼這個物件在Block執行的過程中就會變成nil,這就可能會帶來一些問題.使用__strong修飾物件,直到Block被執行完畢,這個物件都不會被釋放.
- 事後補救
在確定block回撥執行完成後,將其中一方強制置為空 xx==nil
.
補充關於weak
weak修飾屬性,這個問題就好像iOS程式設計師之間打招呼的方式一樣.
weak表示一個由runtime
維護的hash表,key是所指物件的地址,value是weak指標的地址陣列.
weak是弱引用,所引用物件的計數器不會加1,並在引用物件被釋放的時候自動被設定為nil.這是因為:
runtime
會對註冊的類進行佈局,當此物件的引用次數為0時釋放物件,在weak表中以物件地址為鍵搜尋,將搜尋到的weak物件置為nil
這也是weak會置為nil,而assign有可能會野指標的原因,所以在修飾Objective-c物件的時候多用weak,不用assign.
如果有不對的地方還請指正,謝謝