基本使用
block常見的使用方式如下:
// 無參無返回值
void(^MyBlockOne)(void) = ^(void) {
NSLog(@"無參數(shù), 無返回值");
};
MyBlockOne();
// 有參無返回值
void (^MyBlockTwo)(int a) = ^(int a) {
NSLog(@"a = %d", a);
};
MyBlockTwo(10);
//有參有返回值
int (^MyBlockThree)(int, int) = ^(int a, int b) {
NSLog(@"return %d", a + b);
return 10;
};
MyBlockThree(10, 20);
// 無參有返回值
int (^MyBlockFour)(void) = ^(void) {
NSLog(@"return 10");
return 10;
};
// 聲明為某種類型
typedef int (^MyBlock) (int, int);
@property (nonatomic, copy) MyBlock block;
Block的本質(zhì) - OC對(duì)象
結(jié)論: block的內(nèi)部存在isa指針,其本質(zhì)就是封裝了函數(shù)調(diào)用和函數(shù)調(diào)用環(huán)境的OC對(duì)象。
證明方法一:底層結(jié)構(gòu)窺探
main函數(shù)中定義一個(gè)block,如下
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block)(void) = ^(void) {
NSLog(@"this is first block");
};
block();
}
return 0;
}
終端進(jìn)入項(xiàng)目所在目錄,通過xcrun 命令將OC代碼轉(zhuǎn)為C++代碼:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
轉(zhuǎn)換結(jié)果如下:
// 1. block 的結(jié)構(gòu)體
struct __main_block_impl_0 {
struct __block_impl 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.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// block 內(nèi)部impl結(jié)構(gòu)體,存儲(chǔ)isa指針,block方法的地址。
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr; // 方法地址
};
// block 的描述信息,如: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)};
// 2. block 的方法實(shí)現(xiàn)
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hc_wwwl26516td3w0ds9cx80c280000gp_T_main_cf18a7_mi_0);
}
// 3. main方法的實(shí)現(xiàn)
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
將生成的main方法簡(jiǎn)化后得:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
block->FuncPtr(block);
}
return 0;
}
看簡(jiǎn)化后的代碼,你是不是有疑問, 為什么block->FuncPtr(block) 這句話能調(diào)用成功,明明FuncPtr 是__block_impl 類型里的成員,為什么可以直接使用block調(diào)用 ?
原因其實(shí)很簡(jiǎn)單,因?yàn)樵?code>block結(jié)構(gòu)體__main_block_impl_0內(nèi),__block_impl是第一個(gè)成員變量,因此block的地址和impl的地址是相同的。兩者可以進(jìn)行強(qiáng)制轉(zhuǎn)換。
根據(jù)轉(zhuǎn)換結(jié)果:
-
OC中定義的block底層其實(shí)就是一個(gè)C++ 的結(jié)構(gòu)體__main_block_impl_0。結(jié)構(gòu)體有兩個(gè)成員變量impl、Desc,分別是結(jié)構(gòu)體類型__block_impl、__main_block_desc_0。 - 結(jié)構(gòu)體
__block_impl內(nèi)包含了isa指針和指向函數(shù)實(shí)現(xiàn)的指針FuncPtr。 - 結(jié)構(gòu)體
__main_block_desc_0內(nèi)Block_size成員存儲(chǔ)著Block的大小。
由上可知,block內(nèi)部有一個(gè)isa指針,因此,block本質(zhì)其實(shí)就是一個(gè)OC對(duì)象。
證明方法二:代碼層面
如果block是一個(gè)OC對(duì)象,那它最終肯定繼承自NSObject類(NSProxy除外),因此我們可以直接打印出block繼承鏈看一下就知道了。
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block)(void) = ^(void) {
NSLog(@"this is first block");
};
NSLog(@"class = %@", [block class]);
NSLog(@"superclass = %@", [block superclass]);
NSLog(@"superclass superclass = %@", [[block superclass] superclass]);
NSLog(@"superclass superclass superclass = %@", [[[block superclass] superclass] superclass]);
}
return 0;
}
輸出結(jié)果:
2020-07-28 19:25:24.475317+0800 LearningBlock[39445:591948] class = __NSGlobalBlock__
2020-07-28 19:25:24.475707+0800 LearningBlock[39445:591948] superclass = __NSGlobalBlock
2020-07-28 19:25:24.475762+0800 LearningBlock[39445:591948] superclass superclass= NSBlock
2020-07-28 19:25:24.475808+0800 LearningBlock[39445:591948] superclass superclass superclass= NSObject
block 的繼承鏈: __NSGlobalBlock -> NSBlock -> NSObject
可以看出block最終繼承自NSObject的。isa指針其實(shí)就是由NSObject來的。 因此block本質(zhì)就是一個(gè)OC對(duì)象。
Block 的變量捕獲(Capture)
為了保證block內(nèi)部能夠正常訪問外部的值,block有個(gè)變量捕獲的機(jī)制。下面來一起來探索一下block的變量捕獲機(jī)制。
代碼:
int a = 10; // 全局變量, 程序運(yùn)行過程一直存在內(nèi)存。
int main(int argc, const char * argv[]) {
@autoreleasepool {
auto int b = 20; // 局部變量,默認(rèn)是auto修飾,一般可以不寫auto,所在作用域結(jié)束后會(huì)被銷毀。
static int c = 30; // 靜態(tài)變量,程序運(yùn)行過程中一直存在內(nèi)存。
void(^block)(void) = ^(void) {
NSLog(@"a = %d, b = %d, c = %d", a, b, c);
};
// 觀察調(diào)用block時(shí),a,b,c 的值是多少呢?
a = 11;
b = 21;
c = 31;
block(); // 調(diào)用block
}
return 0;
}
打印輸出:
2020-07-28 19:43:41.729849+0800 LearningBlock[39648:603167] a = 11, b = 20, c = 31
由打印結(jié)果來看,b沒有改變, 而a和c 的值都發(fā)生了變化。 原因是什么呢?下面一起看下
運(yùn)行下面的轉(zhuǎn)換語句,將當(dāng)前的OC代碼轉(zhuǎn)換C++, 方便我們看到更本質(zhì)的東西:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
轉(zhuǎn)換后的代碼如下:
int a = 10; // 全局變量
struct __main_block_impl_0 { // block的結(jié)構(gòu)體
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int b; // 新生成的成員變量b,用于存放外部局部變量b的值
int *c; // 新生成的成員變量c,指針類型, 用于存儲(chǔ)外部靜態(tài)局部變量c的引用。
// 構(gòu)造函數(shù)
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _b, int *_c, int flags=0) : b(_b), c(_c) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int b = __cself->b; // 通過cself進(jìn)行訪問內(nèi)部的成員變量b
int *c = __cself->c; // 通過cself獲取靜態(tài)局部變量c的引用
// 直接訪問全局變量a
NSLog((NSString *)&__NSConstantStringImpl__var_folders_vt_j2sf07q142992_z55yg_170w0000gp_T_main_256a11_mi_0, a , b, (*c));
}
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)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
auto int b = 20;
static int c = 30;
void (*Myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, b, &c));
a = 11;
b = 21;
c = 31;
((void (*)(__block_impl *))((__block_impl *)Myblock)->FuncPtr)((__block_impl *)Myblock);
}
return 0;
}
有上可以觀察到:
block結(jié)構(gòu)體__main_block_impl_0內(nèi)部生成了新的成員變量b和*c, 分別用于存放外部傳進(jìn)來的b和c的地址,這就是我們所說的捕獲。而對(duì)于全局變量a則沒有進(jìn)行捕獲,在使用時(shí)是直接訪問的。
由此可得出:
-
block內(nèi)部對(duì)auto和static類型的變量進(jìn)行了捕獲,但是不會(huì)捕獲全局變量。 - 雖然block對(duì)
auto和static變量都進(jìn)行了捕獲,但是不同的是,auto變量是值傳遞,而static變量則是地址傳遞。因此當(dāng)外部的static變量值發(fā)生變化時(shí),block內(nèi)部也跟著會(huì)改變,而外部的auto變量值發(fā)生變化,block內(nèi)部的值不會(huì)發(fā)生改變。
[圖片上傳失敗...(image-3381f6-1601373145542)]
思考??
相信你會(huì)有這樣的疑問,為什么block會(huì)捕獲auto和static類型的局部變量,而不會(huì)捕獲全局變量呢?(全局變量表示不服,block你怎么搞區(qū)別對(duì)待呢?), 那么block的變量捕獲究竟有什么講究呢?
其實(shí)是這樣的
- 首先對(duì)于
auto類型的局部變量,其生命周期太短了,離開了其所在的作用域后,auto變量的內(nèi)存就會(huì)被系統(tǒng)回收了,而block的調(diào)用時(shí)機(jī)是不確定的,如果block不對(duì)它進(jìn)行捕獲,那么當(dāng)block運(yùn)行時(shí)再訪問auto變量時(shí),因?yàn)樽兞恳驯幌到y(tǒng)回收,那么就會(huì)出現(xiàn)壞內(nèi)存訪問或者得到不正確的值。 - 對(duì)于局部的
static變量,因?yàn)槠涑跏蓟?,在程序運(yùn)行過程中就會(huì)一直存在內(nèi)存中,而不會(huì)被系統(tǒng)回收,但是由于因?yàn)槭蔷植孔兞康脑颍湓L問的作用域有限,block想訪問它就要知道去哪里訪問,所以block才需要對(duì)其進(jìn)行捕獲,但與auto變量不同的是,block只需捕獲static變量的地址即可。 - 對(duì)于
全局變量,因?yàn)槠湓诔绦蜻\(yùn)行過程一直都在,并且其訪問作用域也是全局的,所以block可以直接找到它,而不需要對(duì)它進(jìn)行捕獲。
所以,block的變量捕獲原則其實(shí)很簡(jiǎn)單,如果block內(nèi)部能直接訪問到的變量,那就不捕獲(捕獲也是浪費(fèi)空間), 如果block內(nèi)部不能直接訪問到變量,那就需要進(jìn)行捕獲(不捕獲就沒得用)。
Block的類型
block有3種類型,可以通過調(diào)用class方法或者isa指針查看具體類型,最終都是繼承自NSBlock類型.
- NSGlobalBlock
- NSStackBlock
- NSMallocBlock
為了準(zhǔn)確分析block的類型,先把ARC給關(guān)閉,使用MRC。
[圖片上傳失敗...(image-dd67c0-1601373145542)]
int main(int argc, const char * argv[]) {
@autoreleasepool {
auto int age = 10; // 局部變量,默認(rèn)是auto,一般可以不寫auto,所處作用域結(jié)束后會(huì)被銷毀。
static int height = 20; // 靜態(tài)變量,程序運(yùn)行過程中一直存在內(nèi)存。
void(^block1)(void) = ^(void) {
NSLog(@"1111111111"); // 沒有捕獲了auto變量
};
void(^block2)(void) = ^(void) {
NSLog(@"age = %d", age); // 捕獲了auto變量
};
void(^block3)(void) = ^(void) {
NSLog(@"height = %d", height); // 捕獲了static變量
};
NSLog(@"block1 class: %@", [block1 class]); // __NSGlobalBlock__
NSLog(@"block2 class: %@", [block2 class]); // __NSStackBlock__
NSLog(@"block2 copy class: %@", [[block2 copy] class]); //__NSMallocBlock__
NSLog(@"block3 class: %@", [block3 class]); //__NSGlobalBlock__
}
return 0;
}
// 輸出結(jié)果:
2020-07-28 20:41:43.283331+0800 LearningBlock[40390:637401] block1 class: __NSGlobalBlock__
2020-07-28 20:41:43.283755+0800 LearningBlock[40390:637401] block2 class: __NSStackBlock__
2020-07-28 20:41:43.283877+0800 LearningBlock[40390:637401] block2 copy class: __NSMallocBlock__
2020-07-28 20:41:43.283924+0800 LearningBlock[40390:637401] block3 class: __NSGlobalBlock__
由上可知:
-
block類型取值如下:- 沒有捕獲
auto變量,那么block的為__NSGlobalBlock__類型。 - 捕獲了
auto變量,那么block為__NSStackBlock__類型。 - 對(duì)
__NSStackBlock__類型的block進(jìn)行copy操作,則block就會(huì)變成__NSMallocBlock__類型.
image
- 沒有捕獲
-
block這幾種類型的主要區(qū)別是:在內(nèi)存中的存放區(qū)域不同。(即生命的周期不同)-
__NSGlobalBlock__存在數(shù)據(jù)段。 -
__NSStackBlock__存放在??臻g。 -
__NSMallocBlock__存放在堆空間。
-
檢驗(yàn)題:
新建一個(gè)Person類, 如下:
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
- (void)test;
@end
@implementation Person
- (void)test {
void (^block)(void) = ^{
NSLog(@"person name = %@", _name);
};
}
@end
問題: 在Person.m的test方法中的block對(duì)self有沒有進(jìn)行捕獲呢?
答案是有,block會(huì)捕獲self. 分析如下:
首先將Person.m 通過xcrun命令轉(zhuǎn)換為C++, 得到如下內(nèi)容:
//test 方法內(nèi)的block方法
struct __Person__test_block_impl_0 {
struct __block_impl impl;
struct __Person__test_block_desc_0* Desc;
Person *self;
__Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//test 方法
static void _I_Person_test(Person * self, SEL _cmd) {
void (*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hc_wwwl26516td3w0ds9cx80c280000gp_T_Person_14871d_mi_1, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)block, sel_registerName("class")));
}
觀察轉(zhuǎn)換后的代碼可以看到:
- 我們平常寫的OC方法,其實(shí)默認(rèn)就有隱藏的兩個(gè)參數(shù),
(Person *self, SEL _cmd), 分別是方法的調(diào)用者 self和方法選擇器 sel。 - 方法的參數(shù)一般是局部變量,block會(huì)對(duì)局部變量進(jìn)行捕獲的。
Block的copy操作
我們?nèi)粘J褂玫腷lock一般是__NSMallocBlock__類型的,原因有如下:
- 對(duì)于
__NSGlobalBlock__類型的block, 因?yàn)闆]有捕獲auto變量, 所以正常一般都是直接使用函數(shù)實(shí)現(xiàn)。 - 對(duì)于
__NSStackBlock__類型的block, 因?yàn)槠浯娣旁跅I?,其?nèi)部使用變量容易被系統(tǒng)回收掉,從而導(dǎo)致一些異常的情況。比如下面:(要先將項(xiàng)目切成MRC,因?yàn)锳RC下編譯器會(huì)根據(jù)情況做copy操作,會(huì)影響分析)
typedef void (^MJBlock)(void);
MJBlock block;
void test() {
int a = 10; // test方法結(jié)束后,a的內(nèi)存就被回收了。
block = ^(void) {
NSLog(@"a = %d", a);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block(); // block里打印的是被回收了的a
}
return 0;
}
輸出結(jié)果:
2020-09-27 10:05:28.616920+0800 Interview01-block的copy[7134:29679] a = -272632776
- 對(duì)于
__NSMallocBlock__類型的block, 因?yàn)樗谴鎯?chǔ)在堆上,所以就不存在__NSStackBlock__類型block的問題。
上面演示的是在MRC環(huán)境下的, 那么在ARC環(huán)境下又是如何的呢?
在ARC環(huán)境下,編譯器會(huì)自動(dòng)根據(jù)情況將棧上的block復(fù)制到堆上。比如一下情況:
-
block作為函數(shù)返回值時(shí)。 - 將
block賦值給__strong指針時(shí)。 -
block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時(shí)。 -
block作為GCD API方法參數(shù)時(shí)。
typedef void (^MJBlock)(void);
MJBlock myblock()
{
int a = 10;
return ^{
NSLog(@"--------- %d", a); // 1. 作為方法返回值時(shí)。會(huì)自動(dòng)copy
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
MJBlock block = ^{ // 2.賦值給strong指針時(shí),會(huì)自動(dòng)copy
NSLog(@"---------%d", age);
};
NSArray *arr = @[@10, @20];
[arr enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
// 3. block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時(shí)。會(huì)自動(dòng)copy
}];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 4. block作為GCD API方法參數(shù)時(shí)。會(huì)自動(dòng)copy
});
}
return 0;
}
根據(jù)上面的情況,在MRC和ARC下block屬性的寫法可以有差異:
MRC下block屬性的建議寫法
@property (copy, nonatomic) void (^block)(void); // 賦值時(shí)會(huì)自動(dòng)copy到堆上
ARC下block屬性的建議寫法
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);
對(duì)象類型的auto變量
基本數(shù)據(jù)類型的auto變量我們已經(jīng)分析了,那么對(duì)象類型的auto變量是不是和基本數(shù)據(jù)類型的一樣還是有什么特別之處呢?下面我們一起來分析下:(記得先將工程切回ARC模式)
如下代碼:
@interface LCPerson : NSObject
@property (nonatomic, assign) int age;
@end
@implementation LCPerson
- (void)dealloc {
NSLog(@"%s", __func__); // 銷毀代碼
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"11111111");
{
LCPerson *person = [[LCPerson alloc] init];
person.age = 10;
}
NSLog(@"22222222");
}
return 0;
}
// 輸出結(jié)果:
2020-09-27 10:36:43.856070+0800 LearningBlock[16016:56873] 11111111
2020-09-27 10:36:43.856442+0800 LearningBlock[16016:56873] -[LCPerson dealloc]
2020-09-27 10:36:43.856474+0800 LearningBlock[16016:56873] 22222222
我們定義了一個(gè)LCPerson類,在main.m中做測(cè)試,由輸出結(jié)果可以看出,person對(duì)象的釋放是在1111111 和 22222222之間, 這我們應(yīng)該都可以理解。(局部作用域)
我們繼續(xù)~
加入Block之后,我們?cè)儆^察一下。
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"11111111");
MyBlock block;
{
LCPerson *person = [[LCPerson alloc] init];
person.age = 10;
block = ^(void){
NSLog(@"person age = %d", person.age);
};
}
NSLog(@"22222222");
}
NSLog(@"3333333");
return 0;
}
輸出結(jié)果:
2020-09-27 10:52:27.578241+0800 LearningBlock[20478:70040] 11111111
2020-09-27 10:52:27.578627+0800 LearningBlock[20478:70040] 22222222
2020-09-27 10:52:27.578688+0800 LearningBlock[20478:70040] -[LCPerson dealloc]
2020-09-27 10:52:27.578729+0800 LearningBlock[20478:70040] 3333333
根據(jù)結(jié)果,我們可以發(fā)現(xiàn)加入了block之后,person的銷毀是在222222之后發(fā)生的,即person所在的作用域結(jié)束后,person對(duì)象沒有立即釋放。 那么block究竟對(duì)person干了什么,導(dǎo)致person對(duì)象沒能及時(shí)釋放呢? 為了分析,我們將上面的代碼先簡(jiǎn)化一下。簡(jiǎn)化如下
int main(int argc, const char * argv[]) {
@autoreleasepool {
LCPerson *person = [[LCPerson alloc] init];
person.age = 10;
void (^block)(void) = ^(void){
NSLog(@"person age = %d", person.age);
};
block();
}
return 0;
}
將上面OC代碼轉(zhuǎn)換為C++代碼:(支持ARC、指定運(yùn)行時(shí)系統(tǒng)版本)
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
轉(zhuǎn)換后的C++代碼如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
LCPerson *__strong person; // strong類型的指針
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, LCPerson *__strong _person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
LCPerson *__strong person = __cself->person; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_vt_j2sf07q142992_z55yg_170w0000gp_T_main_5882d6_mi_0, ((int (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("age")));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static struct __main_block_desc_0 {
size_t reserved;
size_t 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(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LCPerson *person = ((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LCPerson"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL, int))(void *)objc_msgSend)((id)person, sel_registerName("setAge:"), 10);
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, person, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
通過觀察可以發(fā)現(xiàn),block內(nèi)部對(duì)person進(jìn)行了捕獲。并且與捕獲基本數(shù)據(jù)類型的auto變量不同的是,捕獲對(duì)象類型時(shí)__main_block_desc_0結(jié)構(gòu)體多了兩個(gè)函數(shù),分別是copy和dispose,這兩個(gè)函數(shù)與被捕獲對(duì)象的引用計(jì)數(shù)的處理有關(guān)。
- 當(dāng)
block從棧上拷貝到堆上時(shí),copy函數(shù)被調(diào)用,接著它會(huì)調(diào)用_Block_object_assign函數(shù),處理被捕獲對(duì)象的引用計(jì)數(shù),如果捕獲變量時(shí)是使用__strong修飾,那么對(duì)象的引用計(jì)數(shù)就會(huì)+1. 如果捕獲時(shí)是__weak修飾,則引用計(jì)數(shù)不變。(下面會(huì)驗(yàn)證) - 當(dāng)
block被回收,即釋放時(shí),dispose函數(shù)被調(diào)用,接著它會(huì)調(diào)用_Block_object_dispose函數(shù),如果捕獲變量時(shí)是使用__strong修飾,那么對(duì)象的引用計(jì)數(shù)就會(huì)-1. 如果捕獲變量時(shí)是__weak修飾,則引用計(jì)數(shù)不變。(下面會(huì)驗(yàn)證)
我們知道,在ARC環(huán)境下,將block賦值給__strong指針,block會(huì)自動(dòng)調(diào)用copy函數(shù)。所以 person對(duì)象離開了局部作用域后沒有釋放的原因就很明確了,是因?yàn)?code>block調(diào)用copy函數(shù)時(shí),將person對(duì)象的引用計(jì)數(shù)增加了1,所以當(dāng)局部作用域結(jié)束時(shí),person對(duì)象的引用計(jì)數(shù)并不為0,因此不會(huì)釋放。 而當(dāng)block的作用域結(jié)束,block調(diào)用dispose函數(shù),將person的引用計(jì)數(shù)減為0,然后person才會(huì)釋放。
如上面所說,那如果是在MRC環(huán)境下,person對(duì)象離開局部作用域后就會(huì)銷毀了, 因?yàn)樵?code>MRC環(huán)境下,將block賦值給__strong指針是不會(huì)觸發(fā)copy函數(shù)的,所以person對(duì)象應(yīng)該可以正常釋放。
驗(yàn)證一: 將工程切換到MRC模式下,測(cè)試剛才的代碼,如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"11111111");
MyBlock block;
{
LCPerson *person = [[LCPerson alloc] init];
person.age = 10;
block = ^(void){
NSLog(@"person age = %d", person.age);
};
[person release]; // MRC下需要手動(dòng)管理內(nèi)存
}
NSLog(@"22222222");
}
NSLog(@"3333333");
return 0;
}
// 輸出結(jié)果:
2020-09-27 11:39:05.493388+0800 LearningBlock[33422:105156] 11111111
2020-09-27 11:39:05.493800+0800 LearningBlock[33422:105156] -[LCPerson dealloc]
2020-09-27 11:39:05.493833+0800 LearningBlock[33422:105156] 22222222
2020-09-27 11:39:05.493857+0800 LearningBlock[33422:105156] 3333333
觀察輸出結(jié)果,和預(yù)料中的一樣。person對(duì)象離開局部作用域后正常釋放。
驗(yàn)證二: 用weak修飾的對(duì)象類型的auto變量. (記得切回ARC環(huán)境)
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"11111111");
MyBlock block;
{
LCPerson *person = [[LCPerson alloc] init];
person.age = 10;
// 弱指針
__weak LCPerson *weakPerson = person;
block = ^(void){
NSLog(@"person age = %d", weakPerson.age);
};
}
NSLog(@"22222222");
}
NSLog(@"3333333");
return 0;
}
// 輸出結(jié)果:
2020-09-27 12:00:20.461929+0800 LearningBlock[39325:122309] 11111111
2020-09-27 12:00:20.462321+0800 LearningBlock[39325:122309] -[LCPerson dealloc]
2020-09-27 12:00:20.462361+0800 LearningBlock[39325:122309] 22222222
2020-09-27 12:00:20.462391+0800 LearningBlock[39325:122309] 3333333
觀察輸出結(jié)果,和預(yù)料中的一樣。person對(duì)象離開局部作用域后正常釋放。
總結(jié):
-
當(dāng)
block內(nèi)部訪問了對(duì)象類型的auto變量時(shí)- 如果
block是在棧上,將不會(huì)對(duì)auto變量產(chǎn)生強(qiáng)引用
- 如果
-
如果
block被拷貝到堆上- 會(huì)調(diào)用
block內(nèi)部的copy函數(shù) -
copy函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_assign函數(shù) -
_Block_object_assign函數(shù)會(huì)根據(jù)auto變量的修飾符(__strong、__weak、__unsafe_unretained)做出相應(yīng)的操作,形成強(qiáng)引用(retain)或者弱引用
- 會(huì)調(diào)用
-
如果
block從堆上移除- 會(huì)調(diào)用
block內(nèi)部的dispose函數(shù) -
dispose函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_dispose函數(shù) -
_Block_object_dispose函數(shù)會(huì)自動(dòng)釋放引用的auto變量(release)
- 會(huì)調(diào)用
__block修飾符
-
__block可以用于解決block內(nèi)部無法修改auto變量值的問題 -
__block不能修飾全局變量、靜態(tài)變量(static) - 編譯器會(huì)將
__block變量包裝成一個(gè)對(duì)象.
下面一起驗(yàn)證一下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int a = 10;
void (^block)(void) = ^{
a = 20;
NSLog(@"a = %d", a);
};
block();
}
return 0;
}
// 輸出結(jié)果:
a = 20
將上面OC代碼轉(zhuǎn)換為C++代碼:(支持ARC、指定運(yùn)行時(shí)系統(tǒng)版本)
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
得到轉(zhuǎn)換后結(jié)果:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref 這就捕獲到的a的引用
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a) = 20; // 修改值a的值。
NSLog((NSString *)&__NSConstantStringImpl__var_folders_vt_j2sf07q142992_z55yg_170w0000gp_T_main_ca9eb0_mi_0, (a->__forwarding->a));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
static struct __main_block_desc_0 {
size_t reserved;
size_t 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(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10}; // 這就是__block 修飾的a變量。
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344)); // 傳入的是a變量的地址。
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
由上面可以看到,OC代碼 __block int a = 10 轉(zhuǎn)換為C++之后變?yōu)榱耍?/p>
__Block_byref_a_0 a = {0, &a, 0, sizeof(__Block_byref_a_0), 10};
__Block_byref_a_0是一個(gè)結(jié)構(gòu)體,結(jié)構(gòu)如下:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
}
所以在OC中用__block修飾一個(gè)變量, 編譯器會(huì)自動(dòng)生成一個(gè)全新的OC對(duì)象。
__block的內(nèi)存管理
__block 的在block中的內(nèi)存管理和對(duì)象類型的auto變量類似(但也有區(qū)別)。
當(dāng)
block在棧上時(shí),并不會(huì)對(duì)__block變量產(chǎn)生強(qiáng)引用-
當(dāng)
block被copy到堆時(shí)- 會(huì)調(diào)用
block內(nèi)部的copy函數(shù) -
copy函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_assign函數(shù) -
_Block_object_assign函數(shù)會(huì)對(duì)__block變量形成強(qiáng)引用(retain)。(這點(diǎn)就是和對(duì)象類型的auto變量有區(qū)別的地方,對(duì)于對(duì)象類型的auto變量,_Block_object_assign函數(shù)會(huì)根據(jù)auto變量的修飾符(__strong、__weak、__unsafe_unretained)做出相應(yīng)的操作, 而__block則是直接強(qiáng)引用 )
image
- 會(huì)調(diào)用
-
當(dāng)
block從堆中移除時(shí)- 會(huì)調(diào)用
block內(nèi)部的dispose函數(shù) -
dispose函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_dispose函數(shù) -
_Block_object_dispose函數(shù)會(huì)自動(dòng)釋放引用的__block變量(release)
- 會(huì)調(diào)用
__block的__forwarding指針
被__block修飾的對(duì)象類型
通過上面我們知道了用__block修飾的基本數(shù)據(jù)類型的處理。那用__block修飾的對(duì)象類型的處理是不是一樣的呢? 下面我們一起看下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block LCPerson *person = [[LCPerson alloc] init];
person.age = 10;
void(^block)(void) = ^(void) {
NSLog(@"person age %d", person.age);
};
block();
}
return 0;
}
通過xcrun命令:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
轉(zhuǎn)換成C++后,得到結(jié)果如下:
struct __Block_byref_person_0 {
void *__isa;
__Block_byref_person_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*); // 管理person的內(nèi)存
void (*__Block_byref_id_object_dispose)(void*); // 管理person的內(nèi)存
LCPerson *__strong person; //arc環(huán)境下, copy 和 dispose函數(shù),會(huì)根據(jù)person的修飾類型(__strong、__weak)來對(duì)person做相應(yīng)的內(nèi)存管理。
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_person_0 *person; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_person_0 *_person, int flags=0) : person(_person->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_person_0 *person = __cself->person; // bound by ref // 這里就是強(qiáng)引用
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hc_wwwl26516td3w0ds9cx80c280000gp_T_main_213c56_mi_0, ((int (*)(id, SEL))(void *)objc_msgSend)((id)(person->__forwarding->person), sel_registerName("age")));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);}
static struct __main_block_desc_0 {
size_t reserved;
size_t 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(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_person_0 person = {(void*)0,(__Block_byref_person_0 *)&person, 33554432, sizeof(__Block_byref_person_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LCPerson"), sel_registerName("alloc")), sel_registerName("init"))};
((void (*)(id, SEL, int))(void *)objc_msgSend)((id)(person.__forwarding->person), sel_registerName("setAge:"), 10);
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_person_0 *)&person, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
當(dāng)block從棧拷貝到堆上時(shí),會(huì)調(diào)用block的copy方法,同時(shí)還會(huì)調(diào)用__Block_byref_person_0結(jié)構(gòu)體里的__Block_byref_id_object_copy方法,__Block_byref_id_object_copy內(nèi)部會(huì)調(diào)用_Block_object_assign方法,處理結(jié)構(gòu)體__Block_byref_person_0內(nèi)部的person指針?biāo)笇?duì)象的引用計(jì)數(shù)。
總結(jié)如下:
當(dāng)
__block變量在棧上時(shí),不會(huì)對(duì)指向的對(duì)象產(chǎn)生強(qiáng)引用-
當(dāng)
__block變量被copy到堆時(shí)- 會(huì)調(diào)用
__block變量內(nèi)部的copy函數(shù) -
copy函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_assign函數(shù) -
_Block_object_assign函數(shù)會(huì)根據(jù)所指向?qū)ο蟮男揎椃?code>__strong、__weak、__unsafe_unretained)做出相應(yīng)的操作,形成強(qiáng)引用(retain)或者弱引用(注意:這里僅限于ARC時(shí)會(huì)retain,MRC時(shí)不會(huì)retain)
- 會(huì)調(diào)用
-
如果
__block變量從堆上移除- 會(huì)調(diào)用
__block變量內(nèi)部的dispose函數(shù) -
dispose函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_dispose函數(shù) -
_Block_object_dispose函數(shù)會(huì)自動(dòng)釋放指向的對(duì)象(release)
- 會(huì)調(diào)用
對(duì)象類型的 auto變量 和 __block變量處理的異同:
當(dāng)block在棧上時(shí),對(duì)它們都不會(huì)產(chǎn)生強(qiáng)引用
-
當(dāng)block拷貝到堆上時(shí),都會(huì)通過copy函數(shù)來處理它們
- __block變量(假設(shè)變量名叫做a)
- _Block_object_assign((void)&dst->a, (void)src->a, 8/BLOCK_FIELD_IS_BYREF/);
-
對(duì)象類型的auto變量(假設(shè)變量名叫做p)
- _Block_object_assign((void)&dst->p, (void)src->p, 3/BLOCK_FIELD_IS_OBJECT/);
-
當(dāng)block從堆上移除時(shí),都會(huì)通過dispose函數(shù)來釋放它們
- __block變量(假設(shè)變量名叫做a)
- _Block_object_dispose((void)src->a, 8/BLOCK_FIELD_IS_BYREF*/);
-
對(duì)象類型的auto變量(假設(shè)變量名叫做p)
- _Block_object_dispose((void)src->p, 3/BLOCK_FIELD_IS_OBJECT*/);
循環(huán)引用問題
在開發(fā)過程中我們經(jīng)常會(huì)遇到block循環(huán)引用的問題, 如下:
typedef void (^MyBlock)(void);
@interface LCPerson : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, copy) MyBlock block;
@end
@implementation LCPerson
- (void)dealloc {
NSLog(@"%s", __func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
LCPerson *person = [[LCPerson alloc] init];
person.age = 10;
person.block = ^{
NSLog(@"person age %d", person.age);
};
NSLog(@"211212121122");
}
return 0;
}
// 輸出結(jié)果:
2020-09-28 20:01:48.358822+0800 LearningBlock[41115:298402] 211212121122
由打印結(jié)果可以看出,person并沒有釋放(沒有調(diào)用person的dealloc方法)。那是什么原因?qū)е碌哪兀渴茄h(huán)引用。 下面我們來分析一下:
-
@property (nonatomic, copy) MyBlock block;從這句話可以看出,person強(qiáng)引用著block. -
block內(nèi)部訪問了person對(duì)象的age屬性,根據(jù)上面所學(xué),我們知道block會(huì)對(duì)person進(jìn)行捕獲,并且在arc環(huán)境下,block賦值給__strong指針時(shí)會(huì)自動(dòng)調(diào)用copy方法,將block從??截惖蕉焉? 這樣會(huì)導(dǎo)致person的引用計(jì)數(shù)加1,即block強(qiáng)引用著person。
所以person與block相互強(qiáng)引用著,出現(xiàn)了循環(huán)引用,所以person對(duì)象不會(huì)釋放。
那么該如何解決呢? 下面說下在ARC環(huán)境和MRC環(huán)境分別如何處理?
解決循環(huán)引用問題 - ARC
在ARC環(huán)境下,我們可以通過使用關(guān)鍵字__weak、__unsafe_unretained來解決。如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
LCPerson *person = [[LCPerson alloc] init];
person.age = 10;
__weak LCPerson *weakPerson = person;
// 或者 __unsafe_unretained LCPerson *weakPerson = person;
person.block = ^{
NSLog(@"person age %d", weakPerson.age);
};
NSLog(@"211212121122");
}
return 0;
}
// 打印結(jié)果:
2020-09-28 20:30:19.659679+0800 LearningBlock[41212:307877] 211212121122
2020-09-28 20:30:19.660256+0800 LearningBlock[41212:307877] -[LCPerson dealloc]
示意圖如下:
還可以使用__block解決, 如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block LCPerson *person = [[LCPerson alloc] init];
person.age = 10;
person.block = ^{
NSLog(@"person age %d", person.age);
person = nil;
};
person.block(); // 必須調(diào)用
NSLog(@"211212121122");
}
return 0;
}
// 打印結(jié)果:
2020-09-28 20:35:32.531704+0800 LearningBlock[41256:310297] person age 10
2020-09-28 20:35:32.532221+0800 LearningBlock[41256:310297] -[LCPerson dealloc]
2020-09-28 20:35:32.532310+0800 LearningBlock[41256:310297] 211212121122
使用__block解決,必須調(diào)用block,不然無法將循環(huán)引用打破。
疑問: __weak和__unsafe_unretained關(guān)鍵字有什么區(qū)別呢?
使用__weak和__unsafe_unretained關(guān)鍵字都能達(dá)到弱引用的效果。這兩者主要的區(qū)別在于,使用__weak關(guān)鍵字修飾的指針,在所指的對(duì)象銷毀時(shí),指針存儲(chǔ)的地址會(huì)被清空(即置為nil), 而__unsafe_unretained則不會(huì)。
解決循環(huán)引用問題 - MRC
- MRC環(huán)境是沒有
__weak關(guān)鍵字的,所以可以使用__unsafe_unretained關(guān)鍵字解決。(與ARC差不多,這里就不演示了) - 同樣也可以是
__block關(guān)鍵字解決。如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block LCPerson *person = [[LCPerson alloc] init];
person.age = 10;
person.block = ^{
NSLog(@"person age %d", person.age);
person = nil;
};
[person release]; // MRC需要手動(dòng)添加內(nèi)存管理代碼
NSLog(@"211212121122");
}
return 0;
}
與ARC不同的是,MRC下使用__block解決循環(huán)引用問題,不要求一定要調(diào)用block。原因上面__block修飾的對(duì)象類型里有說到:
_Block_object_assign函數(shù)會(huì)根據(jù)所指向?qū)ο蟮男揎椃?code>__strong、__weak、__unsafe_unretained)做出相應(yīng)的操作,形成強(qiáng)引用(retain)或者弱引用(注意:這里僅限于ARC時(shí)會(huì)retain,MRC時(shí)不會(huì)retain)
后話
這篇文章有點(diǎn)亂,還有待改進(jìn)。寫博客真的費(fèi)時(shí)間,不過能加深印象,也不錯(cuò)。