前言
block在網絡上的文章也比較多,
本文將開發中block使用細節和block實現原理結合起來,
加上個人的理解,
幫助大家更好地理解block和使用block。
問題
- block的意義
- block為什么不能修改外部變量?這里的外部變量又指的是什么?
- 被block引用的對象,引用計數為何+=2?
- 循環引用究竟是為何引起的?
- __block 又是什么原理?
- block與copy
等等
block產生的意義
程序始終都要遵循逐行執行的原則,
而block 可以理解為 邏輯觸發執行
定時器 可以理解為 時間觸發執行
舉個有點意思的例子:
背景:我現在身處異世界,我有很多酷炫的技能
我現在有這樣一個技能,發動這個技能,我可以在當前這個地方留一個分身并安排好任務,然后我可以繼續做我自己的事情了,等我再次使用這個技能時,我的分身將開始處理這項任務,處理完成后,我的分身將會消失。
隱含的問題:給分身安排具體任務的時候,這個任務在未來是否能夠完成是未知的,因為我們不知道未來會發生什么,
代碼亦是如此。
進一步理解:程序是嚴密而又真實的,所以并不存在什么高科技呀,黑魔法呀~
block其實就是用程序實現了代碼緩存和對象緩存,相應的對象緩存在block對象中,
執行block,就是把緩存的代碼執行一遍,而相應的對象的狀態,可能會因為執行完緩存下來的代碼而發生變化。
到此,希望你對block會產生了那么一點點的興趣~
依舊還是要從源碼說起
強調:以下講的變量
a 默認指的是 自動變量auto,不是對象類型
#import <UIKit/UIKit.h>
int main(int argc, char * argv[]) {
@autoreleasepool {
int a = 0;
void (^block)(void) = ^{
NSLog(@"%d",a);
};
block();
return 0;
}
}
將其翻譯成底層c++文件, 一點一點看
main函數里的代碼
int a = 10;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
這是一大串什么?
不要著急,我們從上到下,從左往右進行說明。
首先,block初始化這一行
// 原代碼
void (^block)(void) = ^{
NSLog(@"%d",a);
};
// 翻譯成c++代碼
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
// 簡化后
block = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA,a);
void (*block)(void)
函數指針,參數void,返回值void((void (*)())
上面函數指針的類型,作用是強轉&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a))
分解來看
分解第一步:__main_block_impl_0 組成
__main_block_impl_0是一個結構體,包含一個構造函數和三個變量
一個普通的結構體類型,一個結構體指針類型,
還有一個和外部的變量a,名稱一樣,類型一樣。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
分解第二步:__main_block_impl_0 的 構造函數
第一個參數void *fp
,傳入的是 &__main_block_func_0
__main_block_func_0
是一個靜態函數,
它的參數又是__main_block_impl_0
這個結構體指針
即 FuncPtr 存儲的是 __main_block_func_0 靜態函數地址
impl.FuncPtr = fp;
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_lb_tby1gwds2fnb89dzkf4cq3xh0000gn_T_main_9bc6d9_mi_0,a);
}
是不是快要繞暈了呢?
在靜態函數__main_block_func_0
內,
先是獲取__main_block_impl_0結構體內的變量a,然后打印出來。
從這一點可以看出 靜態函數的作用就是寫在block內部的代碼的容器和入口。
而__main_block_impl_0結構體內的變量a的值來自外部,是在結構體的構造函數內進行了賦值操作。
得到如下結論:
獲取到的基礎變量的值在block初始化的時候已經確定了,
block外部的變量a在后續無論做什么操作,都不會影響block內部保存的變量a
這就是在block內部直接修改自動變量會報錯的原因,
如果這里直接允許修改了,在目前條件下,也僅僅能做到block內部的變量a進行重新賦值操作,和外部變量a沒有關系,產生歧義。
當然,使用__block
修飾的自動變量可以進行修改,那么又是什么原理呢?我們先繼續解讀當前的源碼
第二個參數 &__main_block_desc_0
__main_block_desc_0靜態結構體存儲的是block的基礎信息
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)};
第三個參數為我們用到的外部的變量a,賦值給結構體內的變量a
第四個參數沒有傳,默認為0 (輔助變量,可以忽略,不會影響block的理解)
分解第三步:__main_block_impl_0 中的 __block_impl
存儲的block的信息,相應的參數在構造函數內進行了賦值操作
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
最后 block初始化代碼和執行代碼放在一起看
void (^block)(void) = ^{
NSLog(@"%d",a);
};
// c++代碼
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
// 簡化
block = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a)
block();
// c++代碼
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
// 簡化
block->FuncPtr(block);
block->FuncPtr 指的就是 __main_block_func_0這個函數的地址,參數為block本身
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_lb_tby1gwds2fnb89dzkf4cq3xh0000gn_T_main_19f603_mi_0,a);
}
串起來再看block構造
block初始化:block 通過 __main_block_impl_0結構體構造函數進行初始化,同時生成__main_block_func_0靜態函數,并將其地址以及其他相關信息儲存在__block_impl這個結構體成員變量中。
其中,__block_impl這個結構體成員變量是__main_block_impl_0的首地址。
block調用:block指針指向的是__main_block_impl_0 的首地址,即__block_impl的地址,所以可以強轉為(__block_impl *)類型,并訪問其成員FuncPtr,指向的是靜態函數地址,并傳入參數__main_block_impl_0,也就是block自己。
名稱 | 類型 | 是否隨block內容改變 | 生成順序 | 說明 |
---|---|---|---|---|
__block_impl | 結構體 | NO | 1 | 底層結構體,屬于__main_block_impl_0成員 |
__main_block_impl_0 | 結構體 | YES | 2 | 緩存變量/對象,主結構體 |
__main_block_func_0 | 靜態函數 | YES | 2 | 緩存代碼,地址存放在__block_impl中 |
該緩存代碼指的是:
總結:
將外部變量/對象的信息緩存在__main_block_impl_0中,
將代碼緩存在靜態函數中,
靜態函數在緩存代碼的時候需要用到外部變量/對象的信息
執行block就是執行了該靜態函數
如果到此有不理解的地方可能c++基礎較薄弱,百度一下輔助查看
最后
本文說明了block的用意,揭開了黑魔法的初級面紗,細講了block基礎源碼,解釋了基礎類型的變量為何不能在block內部直接修改
后續會借此基礎之上,繼續解讀目錄中的問題