Block真的難,筆者靜下心來讀《Objective-C 高級編程 iOS與OS X多線程和內存管理》,讀的時候順便記錄下來本身的心得,方便之後再翻回,也但願能帶給你們一些幫助。html
本文將以一個菜dog的角度,從 Block 不截獲變量、截獲變量不修改、截獲並修改變量 、 截獲對象 四個層次 淺淺探究Block的實現。編程
Block的語法就不回顧了,很差記Block語法能夠翻這篇How Do I Declare A Block in Objective-C?。數組
轉成C++ 的源代碼學習,筆者加了適當的註釋方便理解。bash
int main()
{
void (^blk)(void) = ^{printf("Block\n");};
blk();
retrun 0;
}
複製代碼
將轉爲多線程
// block中通用的成員變量 結構體
// 文章後面的代碼再也不給出,但都有用到
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
// 表明Block 的結構體
struct __main_block_impl_0 {
struct __block_impl impl;// block通用的成員變量
struct __main_block_desc_0* Desc;// block 的大小
// 構造函數
__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;
}
};
// 本來的代碼塊 轉到一個C函數
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
printf("Block\n");
}
// 計算block大小的結構體
// 聲明的同時,初始化一個變量__main_block_desc_0_DATA
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
int main()
{
// 聲明定義block
// 用到了構造函數方法
void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
/*
至關於如下
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。
*/
// 調用block
// 第一個參數爲 blk_>FuncPtr,即C函數
// 第二個參數爲 blk自己
((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
/*
至關於如下
普通的C函數調用
(*blk->impl.FuncPtr)(blk);
*/
return 0;
}
複製代碼
即把本來的代碼塊,轉到一個C函數中。而且建立一個 表明Block 的結構體,最後一個構造函數,Block對象把函數和成員綁定起來。框架
和以上區別在於,Block結構體中的成員變量多了截獲的自動變量,而且構造函數參數也是。less
int main()
{
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{printf(fmt, val);};
val = 2;
fmt = "These values were changed.val = %d\n";
blk();
return 0;
}
複製代碼
將轉爲函數
// 跟上面同樣
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
// 表明Block 的結構體
struct __main_block_impl_0 {
struct __block_impl impl;// block通用的成員變量
struct __main_block_desc_0* Desc;// block 的大小
// 截獲的自動變量
// 結構體中有名字同樣的成員變量
const char *fmt;
int val;
// 構造函數
// 參數多了截獲的自動變量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags = 0) : fmt(_fmt), val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// 本來的代碼塊 轉到一個C函數
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
const char *fmt = __cself->fmt;
int val = __cself->val;
printf(fmt, val);
}
// 計算block大小的結構體
// 聲明的同時,初始化一個變量__main_block_desc_0_DATA
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
int main()
{
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (*blk)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, fmt, val);
/*
結構體初始化以下:
impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
fmt = "val = %d\n";
val = 10;
*/
return 0;
}
複製代碼
根據以上,咱們知道截獲變量後,實質上是Block結構體中有一個成員變量存了起來。調用Block時,是訪問取結構體成員變量,而不是外面的局部變量。學習
Block不容許修改外部變量的值。Apple這樣設計,應該是考慮到了block的特殊性,block也屬於「函數」的範疇,變量進入block,實際就是已經改變了做用域。在幾個做用域之間進行切換時,若是不加上這樣的限制,變量的可維護性將大大下降。又好比我想在block內聲明瞭一個與外部同名的變量,此時是容許呢仍是不容許呢?只有加上了這樣的限制,這樣的情景才能實現。因而棧區變成了紅燈區,堆區變成了綠燈區。ui
iOS Block不能修改外部變量的值,指的是棧中指針的內存地址。下面舉幾個例子理解。
int val = 0;
void (^blk)(void) = ^{
val = 1;
};
複製代碼
如下沒問題
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
id obj = [[NSObject alloc] init];
[array addObject:obj];
};
複製代碼
如下編譯報錯
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
array = [[NSMutableArray alloc] init];
};
複製代碼
如下編譯錯誤
const char text[] = "hello";
void (^blk)(void) = ^{
printf("%c\n", text[2]);
};
複製代碼
需改爲指針
const char *text = "hello";
void (^blk)(void) = ^{
printf("%c\n", text[2]);
};
複製代碼
那麼Block 要怎麼修改變量呢?
C 中有一個變量,容許Block改寫值。
例子
int global_val = 1;// 全局變量
static int static_global_val = 2;// 靜態全局變量
int main()
{
static int static_val = 3;// 靜態變量
void (^blk)(void) = ^{
global_val *= 1;
static_global_val *= 2;
static_val *= 3;
}
return 0;
}
複製代碼
轉換後
int global_val = 1;
static int static_global_val = 2;
// 表明Block 的結構體
struct __main_block_impl_0 {
struct __block_impl impl;// block通用的成員變量
struct __main_block_desc_0* Desc;
// 成員變量只多了靜態變量,緣由在後面分析
int *static_val;
// 構造函數
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_staitc_val, int flags=0) : static_val(_static_val) {
impl.isa = &_NSConcreteStackblock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// 本來的代碼塊 轉到一個C函數
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static__val = __cself->static_val;
global_val *= 1;
static_global_val *= 2;
(*static_val) *=3;
}
// 計算block大小的結構體
// 聲明的同時,初始化一個變量__main_block_desc_0_DATA
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
int main()
{
static int static_val = 3;
blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &static_val);
return 0;
}
複製代碼
爲何成員變量只多了靜態變量呢?
這就要先了解 iOS 內存區域。 iOS-MRC與ARC區別以及五大內存區
全局區又分爲 BSS段 和 數據段(data)。
BSS段:BSS段(bss segment)一般是指用來存放程序中未初始化的或者初始值爲0的全局變量的一塊內存區域。BSS是英文Block Started by Symbol的簡稱。BSS段屬於靜態內存分配。
數據段:數據段(data segment)一般是指用來存放程序中已初始化的全局變量的一塊內存區域。數據段屬於靜態內存分配。
但不一樣的是C++中,不區分有沒有初始化,都放到一塊去。
再回到剛剛的代碼上,爲何block結構體中成員變量只多了靜態變量呢?
int global_val = 1;// 全局變量
static int static_global_val = 2;// 靜態全局變量
static int static_val = 3;// 靜態變量
複製代碼
關於它們的區別——全局變量/靜態全局變量/局部變量/靜態局部變量的異同點
靜態局部變量雖然程序運行時一直存在,但只對定義本身的函數體始終可見。
編譯後,調用block實質上是在 一個新定義的函數 中訪問靜態局部變量,不能直接訪問,因此須要保存其指針。而全局變量能夠訪問到,因此沒有加到成員變量中。
int main()
{
__block int val = 10;
void (^blk)(void) = ^{val = 1;};
return 0;
}
複製代碼
轉換後
// 變量將會變成的結構體
// 即val不是int類型,變成此結構體實例
struct __Block_byref_val_0 {
void *__isa;// __block變量轉化後所屬的類對象
__Block_byref_val_0 *__forwarding;//指向__block變量自身的指針,後面解釋
int __flags;// 版本號
int __size;// 結構體大小
int val;// 本來的int數值
};
// 表明Block 的結構體
struct __main_block_impl_0 {
struct __block_impl impl;// block通用的成員變量
struct __main_block_desc_0* Desc;// block 的大小
__Block_byref_val_0 *val;// val轉成成員變量,類型爲結構體
// 構造函數
__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;
}
};
// 本來的代碼塊 轉到一個C函數
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
__Block_byref_val_0 *val = __cself->val;
// 這裏經過__forwarding賦值?後面解釋
(val->__forwarding->val) = 1;
}
// 當Block從棧複製到堆時
// 經過此函數把截獲的__block變量移到堆或者引用數+1
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src)
{
_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}
// 當Block從堆被廢棄時
// 經過此函數把截獲的__block變量引用數-1
// 至關於對象的delloc方法
static void __main_block_dispose_0(struct __main_block_impl_0*src)
{
_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}
// 計算block大小的結構體
// 該結構體有兩個函數
// copy 和 dispose
// 聲明的同時,初始化一個變量__main_block_desc_0_DATA
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long 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()
{
/*
val變成了__Block_byref_val_0結構體實例
*/
__Block_byref_val_0 val = {
0,// isa指針
&val,//forwarding成員,指向本身
0,// 版本號
sizeof(__Block_byref_val_0),
10 //原來int val的值
};
blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000);
return 0;
}
複製代碼
從main函數中,咱們能夠發現,Block轉換成Block的結構體類型**__main_block_impl_0的自動變量,__block變量val轉換爲block變量的結構體類型__Block_byref_val_0**的自動變量。它們都在棧上。因此Block的isa指針指向NSConcreteStackBlock。
除了NSConcreteStackBlock,還有兩種類型 NSConcreteGlobalBlock 和 NSConcreteMallocBlock。
類 | 設置對象的存儲域 |
---|---|
NSConcreteStackBlock | 棧 |
NSConcreteGlobalBlock | 全局區 |
NSConcreteMallocBlock | 堆 |
void (^blk)(void) = ^{printf("Global Block\n");};
int main
{
return 0;
}
複製代碼
另外只要沒有截獲自動變量,Block類型就是NSConcreteGlobalBlock。
先來理解爲何有個forwarding指向本身。
試想,Block若是截獲了自動變量,而後移到堆上,在別的做用域調用(很常見)。若是__block變量在棧上已經釋放了,Block訪問__block變量會失敗。因此係統須要在Block變成NSConcreteMallocBlock時,截獲的__block變量也複製到堆上。
Block何時會複製到堆上呢?
__block變量的配置存儲域 | Block從棧賦值到堆時的影響 |
---|---|
棧 | 從棧複製到堆並被Block持有 |
堆 | 被Block持有 |
當Block從棧複製到堆時,__block變量的forwarding 會從新指向其在堆中的內存地址。
這樣,不管是在Block語法中、Block語法外使用__block變量,仍是__block變量配置在棧上或對上,均可以順利地訪問同一個__block變量。
筆者在書上剛看到這句話時,有點暈,後來想了一段時間應該是如下意思,若是有誤,歡迎大神批鬥。
以下代碼,有註釋
__block int val = 0;
void (^blk)(void) = [^{++val;} copy];
++val;// 轉換爲++(val.__forwarding->val);即(棧上的val).__forwarding->val,最終指向堆上的val
blk();// 轉換爲++(val.__forwarding->val);即(堆上的val).__forwarding->val,最終指向堆上的val
NSLog(@"%d", val);
複製代碼
blk_t blk;
{
id array = [[NSMutablArray alloc] init];
blk = [^(id obj) {
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
} copy];
}
blk([NSObject alloc] init]);
blk([NSObject alloc] init]);
blk([NSObject alloc] init]);
複製代碼
還記得上面提到的截獲變量不修改,轉爲C++,Block結構體中的成員變量多了截獲的自動變量。
這裏,變量做用域結束時,理論上array被廢棄,但執行輸出結果爲數組count123。
這意味着array超出做用域而存在。
會不會也是Block結構體中的成員變量多了截獲的自動變量呢?
轉換爲C++後
struct __main_block_impl_0 {
struct __block_impl impl;// Block通用的成員變量
struct __main_block_desc_0* Desc;// Block的大小
// 指向數組的成員變量
id __strong array;
// 構造函數
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,id __strong _array, int flags=0) : array(_array) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// 本來的代碼塊 轉到一個C函數
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
id __strong array = __cself->array;
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
}
// 當Block從棧複製到堆時
// 經過此函數把截獲的對象引用數+1
// 至關於retain
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src)
{
_Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}
// 當Block從堆被廢棄時
// 經過此函數把截獲的對象release引用數-1
// 至關於對象的delloc方法
static void __main_block_dispose_0(struct __main_block_impl_0 *src)
{
_Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT);
}
// 計算block大小的結構體
// 該結構體有兩個函數
// copy 和 dispose
// 聲明的同時,初始化一個變量__main_block_desc_0_DATA
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long 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
};
複製代碼
調用block轉換以下。
blk_t blk;
{
id __strong array = [[NSMutableArray alloc] init];
// 構造函數
blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 0x22000000);
blk = [blk copy];
}
// 調用,第一個參數爲blk自己,第二個參數爲id類型對象
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
複製代碼
能夠看到,和猜想同樣,Block結構體中確實多了一個id __strong array;
咱們知道,咱們寫的C語言結構體不能帶有__strong修飾符的變量。緣由是編譯器不知道什麼時候進行C語言結構體的初始化和廢棄操做。
可是OC運行時庫能把握Block從棧複製到堆以及堆上的Block被廢棄的時機,所以Block用結構體中能夠管理好。
那麼同時用__block 和 __strong 修飾的對象呢?
上面提到過__block int val,val將變爲一個結構體,對象也同樣。
__block id obj = [[NSObject alloc] init];
// 至關於__block id __strong obj = [[NSObject alloc] init];
複製代碼
轉換爲
// 對象將會變成的結構體
struct __Block_byref_obj_0 {
void *__isa;// __block變量轉化後所屬的類對象
__Block_byref_val_0 *__forwarding;//指向對象自身的指針,後面解釋
int __flags;// 版本號
int __size;// 結構體大小
void (*__Block_byref_id_object_copy)(void*, void*);// retain對象
void (*__Block_byref_id_object_dispose)(void*);// release對象
__strong id obj;//指向對象
};
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
// 對象變成了__Block_byref_obj_0結構體實例
__Block_byref_obj_0 obj = {
0,
&obj,
0x2000000,
sizeof(__Block_byref_obj_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
[[NSObject alloc] init]
};
複製代碼
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
__block id __weak array2 = array;
blk =[^(id obj) {
[array2 addObject:obj];
NSLog(@"array count = %ld", [array2 count]);
} copy];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
複製代碼
輸出結果爲數組數目0。
這是因爲array在做用域結束時被釋放、廢棄,nil被賦值在array2中。
結論:Block中持有weak聲明的對象,對象引用數不會增長。
這種Block的類型是NSConcreteGlobalBlock。這種Block把代碼塊內容轉到一個C函數中,Block結構體較簡單。
這種Block的結構體中截獲的變量會變成成員變量。
截獲的對象也會變成成員變量(內存語義相同),Block複製到堆上時會調用__main_block_copy_0
,廢棄時調用__main_block_dispose_0
,對捕獲的強引用對象引用數形成影響。
而且構造函數、調用的C函數都會用到截獲的變量。
Block結構體中沒有全局變量和全局靜態變量,由於能夠直接用。但Block結構體會保存局部靜態變量的指針。
val會變成一個結構體__Block_byref_val_0
,其成員變量__forwarding
指向自己。
當Block從棧複製到堆時,會調用__main_block_copy_0
,val會經過_Block_object_assign
引用數+1。
當Block銷燬,會調用__main_block_dispose_0
,val會經過_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF)
引用數-1。
對象會變成一個結構體__Block_byref_obj_0
,其成員變量__strong id obj
指向對象,其成員變量__forwarding
指向自己。
若是是強引用對象,Block會經過__Block_byref_id_object_copy_131
,和__Block_byref_id_object_dispose_131
內部引用和釋放對象。弱引用不對對象生命週期產生影響。
到底何時才須要在ObjC的Block中使用weakSelf/strongSelf
Block屬性中內存語義用copy 仍是strong?
在ARC下,這兩種效果都會把Block 從棧上壓到堆上。但事實上,copy更接近Block的本質。
block 使用 copy 是從 MRC 遺留下來的「傳統」,在 MRC 中,方法內部的 block 是在棧區的,使用 copy 能夠把它放到堆區.在 ARC 中寫不寫都行:對於 block 使用 copy 仍是 strong 效果是同樣的,但寫上 copy 也無傷大雅,還能時刻提醒咱們:編譯器自動對 block 進行了 copy 操做。若是不寫 copy ,該類的調用者有可能會忘記或者根本不知道「編譯器會自動對 block 進行了 copy 操做」,他們有可能會在調用以前自行拷貝屬性值。這種操做多餘而低效。你也許會感受我這種作法有些怪異,不須要寫依然寫。若是你這樣想,實際上是你「日用而不知」。