iOS底層原理探索-Block本質(一)
首先,在學習之前,增加一些動力。經常在面試中,會被問及到這些問題:
block的本質是什麼?
__block的作用是什麼?原理是什麼?有哪些使用注意點?
我們知道block在使用的時候,一般用copy修飾,用copy修飾發生了什麼?具體過程是怎樣的?
帶著這些疑問,我們開始今天的學習。
block的資料結構長什麼樣?
首先,我們寫一個簡單的block,以及block的呼叫:
int age = 10;
void(^block)(int, int) = ^(int a, int b){
NSLog(@"呼叫該block----%d", age);
};
block(100, 100);
通過clang編譯指令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
int age = 10; void(*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)); ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 100, 100);
可以看到,block最後被轉換為__main_block_impl_0型別
__main_block_impl_0型別是個什麼樣的結構存在的呢?
通過檢視定義能夠知道,__main_block_impl_0
的定義為:
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age;//函式呼叫的外部引數 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
其中,__main_block_impl_0裡面第一個型別__block_impl和第二個型別__main_block_desc_0的定義分別為:
struct __block_impl {
void *isa;//isa指標
int Flags;
int Reserved;
void *FuncPtr;//函式地址
};
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
首先,我們看到__main_block_func_0是一個函式;
main函式中__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)將引數__main_block_func_0傳到了block裡面,賦值給__main_block_impl_0裡面的fp,然後做了 impl.FuncPtr = fp。
最後在執行block的時候,是執行的block->FuncPtr,就呼叫到了impl.FuncPtr,也就是fp,也就是__main_block_func_0
block內部直觀表示大致如下:
總結:
- block是一個具有isa指標的oc物件
- block是封裝了函式以及函式呼叫環境的OC物件
封裝的函式是指block{}內部的程式碼,被轉換成一個函式__main_block_func_0,並將函式地址封裝在了__main_block_impl_0(block型別)內部的impl.FuncPtr
函式呼叫環境是指,函式呼叫的時候需要的引數,從圖中可以看出,函式需要的變數age,已經被封裝在了__main_block_impl_0裡面。
接下來,我們分析下block轉換為底層原始碼的程式碼
int age = 10;
void(block)(int, int) = ((void ()(int, int))&__main_block_impl_0((void )__main_block_func_0, &__main_block_desc_0_DATA, age));
上句程式碼是Block的定義
一般小括號為強制轉換,為了方便觀察,可以將小括號以及小括號裡面的內容刪掉。
簡化為:
void(block)(int, int) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age));
等號後面,是一個函式,函式名為__main_block_impl_0,函式有三個引數。並且獲取函式地址後賦值給block物件。也就是block是一個指標變數。其內部存放的是__main_block_impl_0型別的地址。
而檢視__main_block_impl_0的定義
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;//函式呼叫的外部引數
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;//block的型別是_NSConcreteStackBlock
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__main_block_impl_0
函式與結構體名一樣,該函式沒有寫返回值,但其實是返回結構體本身,該函式稱為建構函式。
那麼,block其實指向的是__main_block_impl_0結構體的地址。
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)};
sizeof(struct __main_block_impl_0):__main_block_impl_0即block的大小
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 100, 100);
上句程式碼為block的執行,簡化後的結果為:
(block->FuncPtr)(block, 100, 100);
利用block找到FuncPtr函式,進行呼叫。
看一下(block->FuncPtr)(block, 100, 100);
block即__main_block_impl_0型別,裡面並沒有FuncPtr函式
__main_block_impl_0裡面的__block_impl型別裡面才有FuncPtr函式,怎麼block直接就呼叫了FuncPtr函式呢?
這是因為,裡面有個強制轉換操作,將block強制轉換為__block_impl *型別,這樣,就可以直接訪問__block_impl裡面的FuncPtr函式,即__block_impl->FuncPtr。
那,為什麼這個可以將__main_block_impl_0強制轉換為__block_impl型別呢?
這是因為,結構體__block_impl型別是__main_block_impl_0型別的第一個成員,那麼__main_block_impl_0型別的記憶體地址跟__block_impl型別的記憶體地址是一樣的,因此,可以強制轉換。
從另一個角度去分析,__main_block_impl_0裡面的__block_impl是一個結構體,而不是指標,相當於直接把__block_impl型別的內容放入__main_block_impl_0之中,也就相當於可以直接進行__main_block_impl_0->FuncPtr訪問。
block捕獲機制
block內部訪問區域性變數
來段簡單的程式碼:
int age = 10;
void(^block)(void) = ^{
NSLog(@"呼叫該block----%d", age);
};
age = 20;
block();
很容易,我們知道最後的執行結果是:
呼叫該block----10
那麼,是怎樣一個原理呢?
同樣,我們通過clang命令,將程式碼轉換為底層程式碼:
int age = 10;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
age = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
可以看到,block將age作為引數,傳到__main_block_impl_0裡面。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
在__main_block_impl_0裡面,block自己定義了一個同名變數age。
並通過age(_age)將_age的值賦值給age,即
age(_age) 等價於 age = _age;
執行
age = 20;
只是將int age = 10變為int age = 20,並沒有改變block裡面age的值
執行
block();
呼叫block實現,就呼叫了__main_block_impl_0裡面的FuncPtr函式,而FuncPtr函式裡面已經封裝了age
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_91_sht6gqgs1xj8b0_gczwc0ygc0000gn_T_main_65a3fe_mi_0, age);
}
該age是__cself->age,即block內部的age。而block內部的age=10。
因此,打印出來的age=10;也就是常說的值傳遞。
為什麼值傳遞的值不可以賦值或者修改呢?
這個我們留到下一小節進行講述
block內部訪問static修飾的區域性變數
如果用static修飾,會是怎麼樣呢?
int age = 10;
static int height = 170;
void(^block)(void) = ^{
NSLog(@"呼叫該block----%d, %d", age, height);
};
age = 20;
height = 180;
block();
結果:呼叫該block----10, 180
int age = 10;
static int height = 170;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));
age = 20;
height = 180;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
從上面可以看出,age傳的是值,height是傳的指標
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;//新建同名變數age
int *height;//新建同名變數height,但是此height是指標變數
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__main_block_impl_0內部,新建的age是int,而height是指標int *。
執行height = 180;
執行block();
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
int age = __cself->age; // bound by copy
int *height = __cself->height; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_91_sht6gqgs1xj8b0_gczwc0ygc0000gn_T_main_48f888_mi_0, age, (*height));
}
呼叫的時候,age是值傳遞,height是指標
由於height傳的值是指標,*height已經修改為180,因此,block內部的height也被修改,因此最後打印出來的height是180
block內部訪問全域性變數
如果是全域性變數或者static修飾的全域性變數,執行結果又有什麼不一樣呢?
int age = 10;
static int height = 170;
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block)(void) = ^{
NSLog(@"呼叫該block----%d, %d", age, height);
};
age = 20;
height = 190;
block();
}
return 0;
}
執行結果:呼叫該block----20, 190
轉換為底層程式碼後:
從底層原始碼可以看出,__main_block_impl_0內部沒有新定義age,或者height。
說明block內部並沒有捕獲外部的全域性變數。
最後呼叫函式,列印的age和height是全域性變數。
通過以上程式碼,我們可以發現,block訪問外部變數,有一個變數捕獲機制(capture)捕獲機制
怎麼理解捕獲呢?
在block內部專門新建一個變數,用來儲存外部的值,稱為捕獲。
通過以上例子,可以得出:
block訪問外部變數總結:
為什麼使用auto修飾的變數,block捕獲的值,而使用static修飾的區域性變數,block捕獲的是指標呢?
這是因為,auto修飾的變數,隨時可能被銷燬,因此,需要及時把值捕獲進去。
而static修飾的變數,在程式整個生命週期都存在,所以,可以對變數進行修改,因此只需要捕獲指標即可。
全域性變數,儲存在靜態全域性區,整個程式的生命週期都存在,因此,不需要捕獲
- (void)test
{
void(^block)(void) = ^{
NSLog(@"呼叫該block----%@", self);
};
block();
}
問:該block裡面的self,是否會被捕獲?
同樣,使用clang轉換為底層程式碼,可以看到block定義:
struct __YZPerson__test_block_impl_0 {
struct __block_impl impl;
struct __YZPerson__test_block_desc_0* Desc;
YZPerson *self;
__YZPerson__test_block_impl_0(void *fp, struct __YZPerson__test_block_desc_0 *desc, YZPerson *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到,捕獲到了self。
問:為什麼會捕獲self呢?
- (void)test
{
void(^block)(void) = ^{
NSLog(@"呼叫該block----%@", self);
};
block();
}
最後轉化為:
static void _I_YZPerson_test(YZPerson * self, SEL _cmd) {
void(*block)(void) = ((void (*)())&__YZPerson__test_block_impl_0((void *)__YZPerson__test_block_func_0, &__YZPerson__test_block_desc_0_DATA, self, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
可以看出,在test函式裡面,其實是有兩個隱式變數:self和SEL型別的_cmd。
self是以引數傳進去的,因此,self屬於區域性變數,需要進行捕獲。
既然這樣的話,那麼:
- (void)test
{
void(^block)(void) = ^{
NSLog(@"呼叫該block----%@", _name);
};
block();
}
_name又是如何存在的呢?捕獲還是不捕獲?捕獲的話是直接捕獲還是怎樣的呢?
不多說,咱還是直接看底層程式碼:
從圖片中可以看到,block並沒有捕獲name,而是通過捕獲的self,訪問的_name。
其實可以理解,因為name屬於YZPerson裡面的一個屬性,_name是YZPerson裡面的一個成員變數,_name其實是self->_name,因此,是通過捕獲self,訪問_name成員變數的。
這樣說明了,在block內部通過訪問成員變數,就相當於裡面引用了self,因此,還是需要留意迴圈引用的問題。
block型別
block有三種類型:
__NSGlobalBlock__
__NSStackBlock__
__NSMallocBlock__
block的三種類型,可以通過呼叫class方法或者isa指標檢視具體型別,最終都是繼承NSBlock型別,再往上是繼承NSObject型別。
舉個例子:
void(^block)(void) = ^{
NSLog(@"呼叫該block----");
};
NSLog(@"1-%@", [block class]);
NSLog(@"2-%@", [[block class] superclass]);
NSLog(@"3-%@", [[[block class] superclass] superclass]);
NSLog(@"4-%@", [[[[block class] superclass] superclass] superclass]);
NSLog(@"5-%@", [[[[[block class] superclass] superclass] superclass] superclass]);
NSLog(@"6-%@", [[[[[[block class] superclass] superclass] superclass] superclass] superclass]);
執行結果:
2020-03-25 11:08:14.326871+0800 block學習[53532:3297757] 1-__NSGlobalBlock__
2020-03-25 11:08:14.327226+0800 block學習[53532:3297757] 2-__NSGlobalBlock
2020-03-25 11:08:14.327276+0800 block學習[53532:3297757] 3-NSBlock
2020-03-25 11:08:14.327317+0800 block學習[53532:3297757] 4-NSObject
2020-03-25 11:08:14.327349+0800 block學習[53532:3297757] 5-(null)
2020-03-25 11:08:14.327377+0800 block學習[53532:3297757] 6-(null)
可以看出,該block的型別是 NSGlobalBlock,其繼承關係是:NSGlobalBlock : __NSGlobalBlock : NSBlock : NSObject
至於為什麼NSObject的superclass是nil可以參考iOS中物件的本質。
問:那,三種類型具體什麼時候是哪種型別呢?
先上個總結圖:
具體的實驗結果可以參考iOS之Block基本使用
其中,在ARC下,你會發現,
int a = 3;//區域性變數
void(^block)(void) = ^{
NSLog(@"呼叫了block, a = %d", a);
};
NSLog(@"%@", block);
結果:<__NSMallocBlock__: 0x28343ea60>
按照之前的總結圖,block的儲存型別不應該是NSStackBlock嗎?怎麼打印出來的卻是NSMallocBlock?
這是因為,在ARC中,系統以及自動幫我們做了copy操作。從而將本應該是NSStackBlock經過copy操作後,變為NSMallocBlock。
每一種型別的block呼叫copy後的結果如下:
為什麼ARC需要幫我們把本儲存在NSStackBlock的block經過copy操作,轉移儲存在NSMallocBlock上呢?
一個在MRC下的例子:
void(^block)(void);
void test()
{
int age = 10;
block = ^{
NSLog(@"呼叫該block----%d", age);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
return 0;
}
結果:
呼叫該block-----272632728
可以看到,age的值不是10,而是一串莫名其妙的數字
這是因為,block引用了auto變數,block型別是NSStackBlock。
在test()括號執行完畢後,block其實已經被釋放了,再次呼叫block,裡面的age就不是10了。
因此,我們需要將存在棧上的block通過copy操作,轉移儲存在堆上。將生命週期交給程式設計師自己控制。
總結:
在ARC環境下,編譯器會根據以下情況自動將棧上的block複製到堆上:
block作為函式返回值時
將block賦值給strong指標時(即block有強指標引用)
block作為Cocoa API中方法名含有usingBlock的方法引數時
block作為GCD API的方法引數時
三個block型別具體儲存在哪一個區域
block與copy、retain、release操作
對不同型別的block,呼叫其retainCount,觀看其有何不同點:
以下是驗證程式:
NSGlobalBlock
- (void)viewDidLoad {
[super viewDidLoad];
//沒有訪問auto變數,儲存在NSGlobalBlock
void(^block)(void) = ^{
};
NSLog(@"%@", block);
[block retain];
[block retain];
[block retain];
NSLog(@"[block retainCount] = %d", [block retainCount]);
NSLog(@"%@", block);
}
列印結果:
2020-07-15 18:42:26.133369+0800 block2[9804:1002328] <__NSGlobalBlock__: 0x10fd29178>
2020-07-15 18:42:26.133506+0800 block2[9804:1002328] [block retainCount] = 1
2020-07-15 18:42:26.133586+0800 block2[9804:1002328] <__NSGlobalBlock__: 0x10fd29178>
NSStackBlock
- (void)viewDidLoad {
[super viewDidLoad];
int a = 3;//區域性變數
//訪問auto變數,儲存在NSStackBlock
void(^block)(void) = ^{
NSLog(@"呼叫了block, a = %d", a);
};
NSLog(@"%@", block);
[block retain];
[block retain];
[block retain];
NSLog(@"[block retainCount] = %d", [block retainCount]);
NSLog(@"%@", block);
}
列印結果:
2020-07-15 18:43:43.936774+0800 block2[9825:1003381] <__NSStackBlock__: 0x7ffeebdf7f88>
2020-07-15 18:43:43.936910+0800 block2[9825:1003381] [block retainCount] = 1
2020-07-15 18:43:43.936996+0800 block2[9825:1003381] <__NSStackBlock__: 0x7ffeebdf7f88>
NSMallocBlock
- (void)viewDidLoad {
[super viewDidLoad];
int a = 3;//區域性變數
//訪問auto變數,儲存在NSStackBlock
void(^block)(void) = [^{
NSLog(@"呼叫了block, a = %d", a);
} copy];//呼叫copy,儲存在NSMallocBlock
NSLog(@"%@", block);
[block retain];
[block retain];
[block retain];
NSLog(@"[block retainCount] = %d", [block retainCount]);
NSLog(@"%@", block);
}
列印結果:
2020-07-15 18:44:48.964249+0800 block2[9842:1004304] <__NSMallocBlock__: 0x600000cd53e0>
2020-07-15 18:44:48.964366+0800 block2[9842:1004304] [block retainCount] = 1
2020-07-15 18:44:48.964456+0800 block2[9842:1004304] <__NSMallocBlock__: 0x600000cd53e0>
block與copy、retain、release操作的總結:
不同於NSObjec的copy、retain、release操作:
Block_copy與copy等效,Block_release與release等效;
對Block不管是retain、copy、release都不會改變引用計數retainCount,retainCount 始終是1;
NSGlobalBlock:retain、release、copy操作都無效;
NSStackBlock:retain、release操作無效,必須注意的是,NSStackBlock在函式返回後,Block記憶體將被回收。即使retain也沒用。容易犯的錯誤是[[mutableAarry addObject:stackBlock],在函數出棧後,從mutableAarry中取到的stackBlock已經被回收,變成了野指標。正確的做法是先將stackBlock copy到堆上,然後加入陣列:[mutableAarry addObject:[[stackBlock copy] autorelease]]。
支援copy,copy之後生成新的NSMallocBlock型別物件。
NSMallocBlock:支援retain、release,雖然retainCount始終是1,但記憶體管理器中仍然會增加、減少計數。
copy之後不會生成新的物件,只是增加了一次引用,類似retain;
儘量不要對Block使用retain操作。