1. block的本質(zhì)
block
本質(zhì)上是一個OC對象,它內(nèi)部也有isa指針,這個對象封裝了函數(shù)調(diào)用地址以及函數(shù)調(diào)用環(huán)境(函數(shù)參數(shù)、返回值、捕獲的外部變量等)。當我們定義一個block
,在編譯后它的底層存儲結(jié)構是怎樣的呢?
下面我們來看一個例子,定義了一個block,并在block里面訪問量block外面的變量age,它底層存儲結(jié)構如下圖所示,block底層就是一個結(jié)構體__main_block_impl_0
。
-
impl->isa
:就是isa指針,可見它就是一個OC對象。 -
impl->FuncPtr
:是一個函數(shù)指針,也就是底層將block中要執(zhí)行的代碼封裝成了一個函數(shù),然后用這個指針指向那個函數(shù)。 -
Desc->Block_size
:block占用的內(nèi)存大小。 -
age
:捕獲的外部變量age,可見block會捕獲外部變量并將其存儲在block的底層結(jié)構體中。
當我們調(diào)用block時(block()
),實際上就是通過函數(shù)指針FuncPtr
找到封裝的函數(shù)并將block的地址作為參數(shù)傳給這個函數(shù)進行執(zhí)行,把block傳給函數(shù)是因為函數(shù)執(zhí)行中需要用到的某些數(shù)據(jù)是存在block的結(jié)構體中的(比如捕獲的外部變量)。如果定義的是帶參數(shù)的block,調(diào)用block時是將block地址和block的參數(shù)一起傳給封裝好的函數(shù)。
2. block的變量捕獲機制
block外部的變量是可以被block捕獲的,這樣就可以在block內(nèi)部使用外部的變量了。不同類型的變量的捕獲機制是不一樣的。下面我們來看一個示例:
int c = 1000; // 全局變量
static int d = 10000; // 靜態(tài)全局變量
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10; // 局部變量
static int b = 100; // 靜態(tài)局部變量
void (^block)(void) = ^{
NSLog(@"a = %d",a);
NSLog(@"b = %d",b);
NSLog(@"c = %d",c);
NSLog(@"d = %d",d);
};
a = 20;
b = 200;
c = 2000;
d = 20000;
block();
}
return 0;
}
// ***************打印結(jié)果***************
2020-01-07 15:08:37.541840+0800 CommandLine[70672:7611766] a = 10
2020-01-07 15:08:37.542168+0800 CommandLine[70672:7611766] b = 200
2020-01-07 15:08:37.542201+0800 CommandLine[70672:7611766] c = 2000
2020-01-07 15:08:37.542222+0800 CommandLine[70672:7611766] d = 20000
關于運行結(jié)果我們后面再做講解,我們先來看一下定義的這個block的在編譯后底層存儲結(jié)構是怎么樣的呢?(可以在命令行運行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
將這個main.m
文件轉(zhuǎn)成編譯后的c/c++
文件,然后在這個文件搜索__main_block_impl_0
就可以找到這個block的結(jié)構體)。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
int *b;
};
我們可以發(fā)現(xiàn),我定義了4個變量,結(jié)果只有2個局部變量被捕獲了,而且2個局部變量的捕獲方式還不一樣。為什么會這樣呢?下面來一一解釋:
2.1 全局變量的捕獲
不管是普通全局變量還是靜態(tài)全局變量,block都不會捕獲。因為全局變量在哪里都可以訪問,所以block內(nèi)部不捕獲也是可以直接訪問全局變量的,所以外部更改全局變量的值時,block內(nèi)部打印的就是最新更改的值。
2.2 靜態(tài)局部變量的捕獲
我們發(fā)現(xiàn)定義的靜態(tài)局部變量b被block捕獲后,在block結(jié)構體里面是以int *b;
的形式來存儲的,也就是說block其實是捕獲的變量b的地址,block內(nèi)部是通過b的地址去獲取或修改b的值,所以block外部更改b的值會影響block里面獲取的b的值,block里面更改b的值也會影響block外面b的值。所以上面會打印b = 200
。
2.3 普通局部變量的捕獲
所謂的普通局部變量就是在一個函數(shù)或代碼塊中定義的類似int a = 10;
的變量,它其實是省略了auto
關鍵字,等價于auto int a = 10
,所以也叫auto變量
。和靜態(tài)局部變量不同的是,普通局部變量被block捕獲后再block底層結(jié)構體中是以int a;
的形式存儲,也就是說block捕獲的其實是a的值(也就是10),并且在block內(nèi)部重新定義了一個變量來存儲這個值,這個時候block外部和里面的a其實是2個不同的變量,所以外面更改a的值不會影響block里面的a。所以打印的結(jié)果是a = 10
。
那有人可能就有疑問了,為什么普通局部變量要捕獲值,跟靜態(tài)局部變量一樣捕獲地址不行嗎?是的,不行。因為普通局部變量a在出了大括號后就會被釋放掉了,這個時候如果我們在大括號外面調(diào)用這個block,block內(nèi)部通過a的指針去訪問a的值就會拋出異常,因為a已經(jīng)被釋放了。而靜態(tài)局部變量的生命周期是和整個程序的生命周期是一樣的,也就是說在整個程序運行過程中都不會釋放b,所以不會出現(xiàn)這種情況。
那有人又有疑問了,既然靜態(tài)局部變量一直都不會被釋放,那block為什么還要捕獲它,直接拿來用不就可以了嗎?這是因為靜態(tài)局部變量作用域只限制在這個大括號類,出了這個大括號,雖然它還存在,但是外面無法訪問它。而前面已經(jīng)介紹過,block里面的代碼在底層是被封裝成了一個函數(shù),那這個函數(shù)肯定是在b所在的大括號外面,所以這個函數(shù)是無法直接訪問到b的,所以block必須將其捕獲。
2.4 block捕獲變量小結(jié)
- 全局變量--不會捕獲,是直接訪問。
- 靜態(tài)局部變量--是捕獲變量地址。
- 普通局部變量--是捕獲變量的值。
所以我們判斷一個變量是否會被block捕獲關鍵就在于這個變量是局部變量還是全局變量。那我們來看一下以下幾種情況中block是否會捕獲self:
- (void)blockTest{
// 第一種
void (^block1)(void) = ^{
NSLog(@"%p",self);
};
// 第二種
void (^block2)(void) = ^{
self.name = @"Jack";
};
// 第三種
void (^block3)(void) = ^{
NSLog(@"%@",_name);
};
// 第四種
void (^block4)(void) = ^{
[self name];
};
}
要搞清楚這個問題,我們主要是要搞清楚self
是局部變量還是全局變量。可能很多人以為它是全局變量,其實他是一個局部變量。再強調(diào)一遍,self是局部變量
。那有人就疑惑了,它為什么是局部變量?它是在哪里定義的。要搞清楚這個問題,我們需要對OC的方法調(diào)用機制有一定了解。
OC中一個對象調(diào)用方法,其實就是給這個這個對象發(fā)送消息。比如我調(diào)用[self blockTest]
,它轉(zhuǎn)成C語言后就變成了objc_msgSend(self, @selector(blockTest))
。OC的blockTest
方法是沒有參數(shù)的,但是轉(zhuǎn)成objc_msgSend
后就多出來了2個參數(shù),一個就是self
,是指向函數(shù)調(diào)用者的,另一個參數(shù)就是要調(diào)用的這個方法。所以對于所有的OC方法來說,它們都有這2個默認的參數(shù),第一個參數(shù)就是self
,所以self
就是這么通過參數(shù)的形式傳進來的,它的確是一個局部變量。
這個問題解決了,那上面幾種情況就簡單了,這4中情況下block都會捕獲self
。對第一、二、四這3種情況都好理解,第三種情況可能有人會有疑惑,block里面都沒有用到self
為什么還會捕獲self
呢。其實_name
這種寫法并不是沒有self
,它只是將self
給省略了,它其實等同于self->_name
,所以這種情況要格外注意。
3. block的類型
block既然是一個OC對象,那它就有類,那它的類時什么呢?我們可以通過調(diào)用block的class
方法或object_getClass()
函數(shù)來得到block的類。
- (void)test{
int age = 10;
void (^block1)(void) = ^{
NSLog(@"-----");
};
NSLog(@"block1的類:%@",[block1 class]);
NSLog(@"block2的類:%@",[^{
NSLog(@"----%d",age);
} class]);
NSLog(@"block3的類:%@",[[^{
NSLog(@"----%d",age);
} copy] class]);
}
// ***************打印結(jié)果***************
2020-01-08 09:07:46.253895+0800 AppTest[72445:7921459] block1的類:__NSGlobalBlock__
2020-01-08 09:07:46.254027+0800 AppTest[72445:7921459] block2的類:__NSStackBlock__
2020-01-08 09:07:46.254145+0800 AppTest[72445:7921459] block3的類:__NSMallocBlock__
block的類有三種,就是上面打印的結(jié)果:__NSGlobalBlock__
、__NSStackBlock__
、__NSMallocBlock__
。有人可能會疑惑,打印第2和3這兩個block時為不像block1那樣先定義一個block1然后再打印block1。這是因為在ARC
模式下,如果一個__NSStackBlock__
類型的block被一個強指針指著,那系統(tǒng)會自動對這個block進行一次copy
操作將這個block變成__NSMallocBlock__
類型,這樣會影響運行的結(jié)果。
下面我們一一介紹一下這三種類型的區(qū)別:
3.1 __NSGlobalBlock__
如果一個block里面沒有訪問普通局部變量(也就是說block里面沒有訪問任何外部變量或者訪問的是靜態(tài)局部變量或者訪問的是全局變量),那這個block就是__NSGlobalBlock__
。__NSGlobalBlock__
類型的block在內(nèi)存中是存在數(shù)據(jù)區(qū)
的(也叫全局區(qū)或靜態(tài)區(qū),全局變量和靜態(tài)變量是存在這個區(qū)域的)。__NSGlobalBlock__
類型的block調(diào)用copy方法的話什么都不會做
。
下面我們再來看下__NSGlobalBlock__
的繼承鏈:
- (void)test{
void (^block)(void) = ^{
NSLog(@"-----");
};
NSLog(@"--- %@",[block class]);
NSLog(@"--- %@",[[block class] superclass]);
NSLog(@"--- %@",[[[block class] superclass] superclass]);
NSLog(@"--- %@",[[[[block class] superclass] superclass] superclass]);
}
// ***************打印結(jié)果***************
2020-01-08 11:03:34.331652+0800 AppTest[72667:7957820] --- __NSGlobalBlock__
2020-01-08 11:03:34.331777+0800 AppTest[72667:7957820] --- __NSGlobalBlock
2020-01-08 11:03:34.331883+0800 AppTest[72667:7957820] --- NSBlock
2020-01-08 11:03:34.331950+0800 AppTest[72667:7957820] --- NSObject
看了block最終也是繼承自NSObject,__NSGlobalBlock__
的繼承鏈為:__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
。
3.2 __NSStackBlock__
如果一個block里面訪問了普通的局部變量,那它就是一個__NSStackBlock__
,它在內(nèi)存中存儲在棧區(qū),棧區(qū)的特點就是其釋放不受開發(fā)者控制,都是由系統(tǒng)管理釋放操作的,所以在調(diào)用__NSStackBlock__
類型block時要注意,一定要確保它還沒被釋放。如果對一個__NSStackBlock__
類型block做copy
操作,那會將這個block從棧復制到堆上。
__NSStackBlock__
的繼承鏈是:__NSStackBlock__ : __NSStackBlock : NSBlock : NSObject
。
3.3 __NSMallocBlock__
一個__NSStackBlock__
類型block做調(diào)用copy
,那會將這個block從棧復制到堆上,堆上的這個block類型就是__NSMallocBlock__
,所以__NSMallocBlock__
類型的block是存儲在堆區(qū)。如果對一個__NSMallocBlock__
類型block做copy
操作,那這個block的引用計數(shù)+1。
__NSMallocBlock__
的繼承鏈是:__NSMallocBlock__ : __NSMallocBlock : NSBlock : NSObject
。
在ARC
環(huán)境下,編譯器會根據(jù)情況,自動將棧上的block復制到堆上。有一下4種情況會將棧block復制到堆上:
a. block作為函數(shù)返回值時:
typedef void (^MyBlock)(void);
- (MyBlock)createBlock{
int a = 10;
return ^{
NSLog(@"******%d",a);
};
}
b. 將block賦值給強指針時:
將定義的棧上的block賦值給強指針myBlock
,就變成了堆block。
- (void)test{
int a = 10; // 局部變量
void (^myBlock)(void) = ^{
NSLog(@"a = %d",a);
};
block();
}
c. 當block作為參數(shù)傳給Cocoa API時:
比如:
[UIView animateWithDuration:1.0f animations:^{
}];
d. block作為GCD的API的參數(shù)時:
比如:
dispatch_async(dispatch_get_main_queue(), ^{
});
另外在MRC
環(huán)境下,定義block屬性建議使用copy
關鍵字,這樣會將棧區(qū)的block復制到堆區(qū)。
@property (copy,nonatomic) void(^block)void;
在ARC
環(huán)境下,定義block屬性用copy
或strong
關鍵字都會將棧區(qū)block復制到堆上,所以這兩種寫法都可以。
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);
4. block對對象型的局部變量的捕獲
block對對象類型和對基本數(shù)據(jù)類型變量的捕獲是不一樣的,對象類型的變量涉及到強引用和弱引用的問題,強引用和弱引用在block底層是怎么處理的呢?
如果block是在棧上,不管捕獲的對象時強指針還是弱指針,block內(nèi)部都不會對這個對象產(chǎn)生強引用。所以我們主要來看下block在堆上的情況。
首先來看下強引用的對象被block捕獲后在底層結(jié)構體中是如何存儲的。這里用下面這條命令來將OC代碼轉(zhuǎn)成c/c++代碼:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
// OC代碼
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.age = 20;
void (^block)(void) = ^{
NSLog(@"age--- %ld",person.age);
};
block();
}
return 0;
}
// 底層結(jié)構體
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__strong person;
};
可以看到和基本數(shù)據(jù)類型不同的是,person
對象被block捕獲后,在結(jié)構體中多了一個修飾關鍵字__strong
。
我們再來看下弱引用對象被捕獲后是什么樣的:
// OC代碼
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.age = 20;
__weak Person *weakPerson = person;
void (^block)(void) = ^{
NSLog(@"age--- %ld",weakPerson.age);
};
block();
}
return 0;
}
// 底層block
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__weak weakPerson;
};
可見此時block中weakPerson
的關鍵字變成了__weak
。
在block中修飾被捕獲的對象類型變量的關鍵字除了__strong
、__weak
外還有一個__unsafe_unretained
。那這結(jié)果關鍵字起什么作用呢?
當block被拷貝到堆上時是調(diào)用的copy
函數(shù),copy
函數(shù)內(nèi)部會調(diào)用_Block_object_assign
函數(shù),_Block_object_assign
函數(shù)就會根據(jù)這3個關鍵字來進行操作。
- 如果關鍵字是
__strong
,那block內(nèi)部就會對這個對象進行一次retain
操作,引用計數(shù)+1,也就是block會強引用這個對象。也正是這個原因,導致在使用block時很容易造成循環(huán)引用。 - 如果關鍵字是
__weak
或__unsafe_unretained
,那block對這個對象是弱引用,不會造成循環(huán)引用。所以我們通常在block外面定義一個__weak
或__unsafe_unretained
修飾的弱指針指向?qū)ο螅缓笤赽lock內(nèi)部使用這個弱指針來解決循環(huán)引用的問題。
block從堆上移除時,則會調(diào)用block內(nèi)部的dispose
函數(shù),dispose
函數(shù)內(nèi)部調(diào)用_Block_object_dispose
函數(shù)會自動釋放強引用的變量。
4. __block修飾符的作用
在介紹__block
之前,我們先來看下下面這段代碼:
- (void)test{
int age = 10;
void (^block)(void) = ^{
age = 20;
};
}
這段代碼有什么問題嗎?編譯器會直接報錯,在block中不可以修改這個age
的值。為什么呢?
因為age
是一個局部變量,它的作用域和生命周期就僅限在是test
方法里面,而前面也介紹過了,block底層會將大括號中的代碼封裝成一個函數(shù),也就相當于現(xiàn)在是要在另外一個函數(shù)中訪問test
方法中的局部變量,這樣肯定是不行的,所以會報錯。
如果我想在block里面更改age
的值要怎么做呢?我們可以將age
定義成靜態(tài)局部變量static int age = 10;
。雖然靜態(tài)局部變量的作用域也是在test
方法里面,但是它的生命周期是和程序一樣的,而且block捕獲靜態(tài)局部變量實際是捕獲的age
的地址,所以block里面也是通過age
的地址去更改age
的值,所以是沒有問題的。
但我們并不推薦這樣做,因為靜態(tài)局部變量在程序運行過程中是不會被釋放的,所以還是要盡量少用。那還有什么別的方法來實現(xiàn)這個需求呢?這就是我們要講的__block
關鍵字。
- (void)test1{
__block int age = 10;
void (^block)(void) = ^{
age = 20;
};
block();
NSLog(@"%d",age);
}
當我們用__block
關鍵字修飾后,底層到底做了什么讓我們能在block里面訪問age
呢?下面我們來看下上面代碼轉(zhuǎn)成c++代碼后block的存儲結(jié)構是什么樣的。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
};
struct __Block_byref_age_0 {
void *__isa; // isa指針
__Block_byref_age_0 *__forwarding; // 如果這block是在堆上那么這個指針就是指向它自己,如果這個block是在棧上,那這個指針是指向它拷貝到堆上后的那個block
int __flags;
int __size; // 結(jié)構體大小
int age; // 真正捕獲到的age
};
我們可以看到,age
用__block
修飾后,在block的結(jié)構體中變成了__Block_byref_age_0 *age;
,而__Block_byref_age_0
是個結(jié)構體,里面有個成員int age;
,這個才是真正捕獲到的外部變量age
,實際上外部的age
的地址也是指向這里的,所以不管是外面還是block里面修改age
時其實都是通過地址找到這里來修改的。
所以age
用__block
修飾后它就不再是一個test1
方法內(nèi)部的局部變量了,而是被包裝成了一個對象,age
就被存儲在這個對象中。之所以說是包裝成一個對象,是因為__Block_byref_age_0
這個結(jié)構體的第一個成員就是isa
指針。
__block修飾變量的內(nèi)存管理:
__block
不管是修飾基礎數(shù)據(jù)類型還是修飾對象數(shù)據(jù)類型,底層都是將它包裝成一個對象(我這里取個名字叫__blockObj
),然后block結(jié)構體中有個指針指向__blockObj
。既然是一個對象,那block內(nèi)部如何對它進行內(nèi)存管理呢?
當block在棧上時,block內(nèi)部并不會對__blockObj
產(chǎn)生強引用。
當block調(diào)用copy
函數(shù)從棧拷貝到堆中時,它同時會將__blockObj
也拷貝到堆上,并對__blockObj
產(chǎn)生強引用。
當block從堆中移除時,會調(diào)用block內(nèi)部的dispose
函數(shù),dispose
函數(shù)內(nèi)部又會調(diào)用_Block_object_dispose
函數(shù)來釋放__blockObj
。