Block
原理Block
變量捕獲Block
類型copy
操作和Block
內部訪問對象類型的變量__block
修改變量及其本質__block
內存管理Block
循環(huán)引用問題
Block
是一種可以在C
、C++
以及Objective-C
代碼中使用,類似于“閉包(closure
)”的代碼塊分扎,借助Block
機制哥力,開發(fā)者可以將代碼像對象一樣在不同的上下文環(huán)境中進行傳遞。
(這里說的不同上下文環(huán)境,我舉個例子:比如在A
函數(shù)中定義了一個變量,它是一個局部變量,那么我要在B
函數(shù)中去訪問肋联,這里就屬于兩個不同的上下文環(huán)境)
一、Block
原理
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 20;
void (^block)(int,int) = ^(int a, int b){
NSLog(@"a = %d, b = %d, age = %d",a,b,age);
};
block(3,5);
}
return 0;
}
將上面main.m
編譯生成C++
代碼:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
main()
函數(shù)
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 20;
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, 3, 5);
}
return 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;
}
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __maib_block_desc_0 {
size_t reserved;
size_t Block_size;
};
我們定義block
變量刁俭,其實下面這句代碼:
void (*block)(int,int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
就是調用結構體__main_block_impl_0
內部的構造函數(shù)初始化一個結構體出來橄仍,然后取結構體地址&__main_block_impl_0
賦給block
指針,所以block
底層是下面結構體;調用構造函數(shù)傳了三個參數(shù):
(void *)__main_block_func_0
侮繁、&__main_block_desc_0_DATA
虑粥、age
其中(void *)__main_block_func_0
是下面函數(shù)的地址,這個函數(shù)就是封裝了block
執(zhí)行邏輯的函數(shù)宪哩,通過上面的構造函數(shù)傳給__block_impl
結構體的FuncPtr
:
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders__h_yv1h9mrx1155q6tq7brn8b9m0000gp_T_main_b54551_mi_0,a,b,age);
}
同樣娩贷,第二個參數(shù)類型&__main_block_desc_0_DATA
是下面結構體地址,最終通過構造函數(shù)賦給了Desc
锁孟,其中Block_size
表示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)};
在main()
函數(shù)中調用block(3, 5)
最終轉化為下面代碼彬祖,通過將block
強制轉換為__block_impl
(這里__block_impl
類型是__main_block_impl_0
結構體第一個成員,所以可以轉) 品抽,最終直接找到impl
中的FuncPtr
進行調用
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);
二储笑、Block
變量捕獲
Block
變量捕獲是指在Block
內部訪問外部變量時,如果外部變量是局部變量圆恤,則Block
內部會將其捕獲突倍,具體捕獲形式看外部的這個局部變量是auto
類型還是static
類型:
如果是auto
類型,直接將變量的值傳遞給Block
內部盆昙,Block
結構體內部會生成一個變量來存儲傳進來的值羽历,所以在Block
外邊改變age=20
,調用block()
時內部打印的結果依然是age=10
淡喜,因為此時進行的是值傳遞秕磷;
如果是static
類型,會將變量的地址傳遞給Block
內部拆火,block
結構體內部會生成一個指針變量來存儲傳進來的地址值跳夭,所以在block
外邊改變height=20
,調用block()
時內部打印的結果是height=20
,因為此時進行的是指針傳遞们镜;
下面進行驗證:
- 局部變量兩種情況:
// 局部變量兩種情況
int main(int argc, const char * argv[]) {
@autoreleasepool {
// case-1: auto變量,離開作用域就銷毀
auto int age = 10; //等價于 int age = 10;
// case-2: static變量
static int height = 10;
void (^block)(void) = ^{
NSLog(@"age is %d, height is %d",age, height);
};
age = 20;
height = 20;
block();
}
return 0;
}
打印結果:
age is 10, height is 20
編譯成C++
:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
int *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;
}
};
從編譯生成的結構體看出润歉,age
是值傳遞模狭,height
是指針傳遞;定義完block
就將10
和&height
捕獲到block
內部踩衩,后邊調用block
時訪問的結構體內部age
是捕獲到的值10嚼鹉,height
是捕獲到的地址&height
;
- 全局變量:因為是在全局區(qū)驱富,所以任何函數(shù)內部可以直接訪問
總結一下:
Block
的本質:Block
本質上也是一個OC
對象锚赤,它內部也有個isa
指針,但它是一個封裝了函數(shù)調用以及函數(shù)調用環(huán)境的OC
對象;
三褐鸥、Block
類型
Block
有三種類型线脚,可以通過調用class
方法或者isa
指針查看具體類型,最終都是繼承自NSBlock
類型:
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block)(void) = ^{
NSLog(@"Hello World!");
};
NSLog(@"%@",[block class]);
NSLog(@"%@",[[block class] superclass]);
NSLog(@"%@",[[[block class] superclass] superclass]);
NSLog(@"%@",[[[[block class] superclass] superclass] superclass]);
}
return 0;
}
打印結果:
05-block類型[46881:69217078] __NSGlobalBlock__
05-block類型[46881:69217078] __NSGlobalBlock
05-block類型[46881:69217078] NSBlock
05-block類型[46881:69217078] NSObject
三種類型:
-
__NSGlobalBlock__
(_NSConcreteGlobalBlock
) -
__NSStackBlock__
(_NSConcreteStackBlock
) -
__NSMallocBlock__
(_NSConcreteMallocBlock
)
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block1)(void) = ^{
NSLog(@"Hello");
};
int age = 10;
void (^block2)(void) = ^{
NSLog(@"Hello - %d", age);
};
NSLog(@"%@ %@ %@",[block1 class], [block2 class], [^{
NSLog(@"%d",age);
} class]);
}
return 0;
}
打印結果:
05-block類型[47475:69339707] __NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__
那不同類型Block
分別對應什么情況呢?
static int height = 30;
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 沒有訪問外部變量
void (^block1)(void) = ^{
NSLog(@"--------------");
};
// 訪問 auto 變量
int age = 10;
void (^block2)(void) = ^{
NSLog(@"--------------%d", age);
};
// 訪問 static 變量
void (^block3)(void) = ^{
NSLog(@"=-------------%d", height);
};
NSLog(@"%@ %@ %@",[block1 class], [block2 class], [block3 class]);
}
return 0;
}
打印結果:
05-block類型[48630:69576321] __NSGlobalBlock__ __NSMallocBlock__ __NSGlobalBlock__
可以看出浑侥,在沒有訪問外部變量的情況下姊舵,block1
是一個__NSGlobalBlock__
類型,存放在數(shù)據區(qū)寓落,此時的block1
就相當于我們定一個了一個函數(shù)括丁,函數(shù)中的代碼沒有訪問另外一個函數(shù)(此處為main()
)中的變量;同理伶选,block3
雖然訪問外部變量史飞,但static
變量是全局的,同樣相當于單獨拿出去定義一個和main()
函數(shù)上下文無關的函數(shù)仰税;
由于block2
訪問了auto
變量构资,相當于在block2
封裝的函數(shù)中訪問了另外一個函數(shù)內部的變量(main()
函數(shù)中的局部變量age
),此時block2
變?yōu)?code>__NSStackBlock__肖卧,因為它需要保存這個局部變量蚯窥,由于是在ARC
環(huán)境,會自動對__NSStackBlock__
類型進行copy
操作塞帐,所以 block2
打印類型是一個 __NSMallocBlock__
類型拦赠;
關閉ARC
在MRC
環(huán)境下打印:
打印結果:
05-block類型[49786:69814242] __NSGlobalBlock__ __NSStackBlock__ __NSGlobalBlock__
可以看出block2
確實是一個__NSStackBlock__
類型葵姥;
四荷鼠、copy
操作和Block
內部訪問對象類型的變量
copy
操作分MRC
和ARC
兩種情況:
-
MRC
環(huán)境:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
// 情況一:沒有訪問外部 auto 變量
// 它是一個 NSGlobalBlock
// 內存是放在數(shù)據段
void (^block)(void) = ^{
NSLog(@"---------");
};
// 情況二:訪問外部 auto 變量 age
// 它是一個 NSStackBlock 隨時會被回收
// 內存是在棧上
// 通過 copy 操作轉變?yōu)?NSMallocBlock 把它放到堆上保活
void (^block2)(void) = [^{
NSLog(@"---------%d",age);
} copy];
// 因為在 MRC 環(huán)境 不用時要進行 release 操作
[block2 release];
}
return 0;
}
-
ARC
環(huán)境:
在ARC
環(huán)境下榔幸,編譯器會根據情況自動將棧上的Block
拷貝到堆上允乐,即自動進行一次copy
操作,比如以下情況:
- 情況一:
Block
作為函數(shù)返回值
// 定義一個block類型
typedef void (^DJTBlock)(void);
// block作為函數(shù)返回值
DJTBlock myblock()
{
// case1: 這里沒有訪問auto變量 是一個NSGlobalBlock
return ^{
NSLog(@"------------");
};
// 相當于下面這樣寫
// DJTBlock block = ^{
// NSLog(@"------------");
// };
// return block;
//-----------------------------------------------------------------
// case2: 這里訪問了auto 是一個NSSackBlock 作為函數(shù)返回值ARC下自動copy成NSMallocBlock
// int age = 10;
// return ^{
// NSLog(@"------------%d",age);
// };
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// ARC環(huán)境下 調用myblock()函數(shù)
// DJTBlock作為myblock函數(shù)的返回值 編譯器自動進行一次 copy 操作
// 所以 block變量指向的 DTJBlock 此時已經在堆上
DJTBlock block = myblock();
block();
// 打印 block 類型
NSLog(@"%@",[block class]);
}
return 0;
}
打印結果:
05-block--copy[64907:9167520] ------------
05-block--copy[64907:9167520] __NSGlobalBlock__
打印結果是一個NSGlobalBlock
類型削咆,這是因為在函數(shù)my block()
內部沒有訪問auto
變量(上面block
類型有闡述)牍疏,而對NSGlobalBlock
類型的Block
執(zhí)行copy
操作生成的Block
還是NSGlobalBlock
,所以如果將返回改為myblock()
函數(shù)內注釋部分拨齐,就會打印__NSMallocBlock__
鳞陨。
- 情況二:將
Block
賦值給__strong
強指針時
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
// Block被強指針指著
DJTBlock block = ^{
NSLog(@"------------%d",age);
};
block();
// 打印 block 類型
NSLog(@"%@",[block class]);
}
return 0;
}
打印結果:
05-block--copy[69520:9293376] ------------10
05-block--copy[69520:9293376] __NSMallocBlock__
- 情況三:
Block
作為Cocoa API
中方法各含有usingBlock
的方法參數(shù)時:
NSArray *array = @[];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
這個參數(shù)Block
也是一個堆上的block
;
- 情況四:
Block
作為GCD API
的方法參數(shù)時:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0*NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
Block
內部訪問對象類型的變量
先看一個有趣的現(xiàn)象:
// DJTPerson.h
@interface DJTPerson : NSObject
@property(nonatomic, assign) int age;
@end
// DJTPerson.m
@implementation DJTPerson
- (void)dealloc
{
NSLog(@"DJTPerson----dealloc");
}
@end
// main.m
#import "DJTPerson.h"
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
{
DJTPerson *person = [[DJTPerson alloc] init];
person.age = 10;
}
NSLog(@"-----------------");// 打斷點
}
return 0;
}
在上面NSLog(@"-----------------");
處打斷點,運行程序發(fā)現(xiàn)控制臺打印:
05-block訪問對象類型的auto變量[77563:9561984] DJTPerson----dealloc
(lldb)
說明在斷點前的中括號結束瞻惋,person
變量就已經釋放厦滤,接著我們定義一個block
,在內部訪問person
的age
屬性:
int main(int argc, const char * argv[]) {
@autoreleasepool {
DJTBlock block;
{
DJTPerson *person = [[DJTPerson alloc] init];
person.age = 10;
block = ^{
NSLog(@"-----------%d",person.age);
};
}
NSLog(@"-----------------");// 打斷點
}
return 0;
}
通用在NSLog(@"-----------------");
處打斷點歼狼,運行程序發(fā)現(xiàn)控制臺無打印掏导,說明person
沒被回收。
為什么被第二種情況下person
沒有被回收呢羽峰?為了驗證我們將代碼簡化并編譯成C++
來進行底層原理分析:
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
DJTPerson *person = [[DJTPerson alloc] init];
person.age = 10;
DJTBlock block = ^{
NSLog(@"-----------%d",person.age);
};
}
return 0;
}
日常操作命令:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
在編譯生成的C++
文件中查看生成的block
結構體:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
DJTPerson *person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, DJTPerson *_person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
由于person
是DJTPerson *
類型趟咆,所以捕獲到block
內部也是DJTPerson *
類型添瓷,即struct __main_block_impl_0
結構體內部可以看到有一個DJTPerson *
類型變量person
;下面先從一個角度理解為什么person
沒有被釋放:
在上面代碼中忍啸,我們定義的Block
是被一個DJTBlock
類型的變量block
強引用的仰坦,即這句代碼:
DJTBlock block = ^{
NSLog(@"-----------%d",person.age);
};
在ARC
環(huán)境下,被強引用的這個Block
(訪問了auto
變量)會自動拷貝到堆上计雌,而這個Block
內部(編譯成C++
即為struct __main_block_impl_0
結構體)又有一個DJTPerson*
類型的指針指向外面這個person
對象悄晃,所以只要這個Block
在,那么這個強指針就在凿滤,所以外邊的person
對象不會被釋放妈橄;
換成MRC
環(huán)境:
// DJTPerson.h
@interface DJTPerson : NSObject
@property(nonatomic, assign) int age;
@end
// DJTPerson.m
@implementation DJTPerson
- (void)dealloc
{
[super dealloc];
NSLog(@"DJTPerson----dealloc");
}
@end
// main.m
#import "DJTPerson.h"
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
DJTBlock block;
{
DJTPerson *person = [[DJTPerson alloc] init];
person.age = 10;
block = ^{
NSLog(@"-----------%d",person.age);
};
[person release];
}
NSLog(@"---------------");// 打斷點
}
return 0;
}
依然在NSLog(@"---------------");
打斷點,發(fā)現(xiàn)控制臺打印結果:
05-block訪問對象類型的auto變量[83896:9803518] DJTPerson----dealloc
發(fā)現(xiàn)person
被釋放翁脆,這是因為即使block
內部訪問了person
對象眷蚓,MRC
環(huán)境下,block
內部訪問了auto
變量反番,它是一個棧上block
沙热,但并不會自動拷貝到堆上,由于它是一個NSStackBlock
罢缸,內部并不會對外部person
強引用(這里說強引用并不準確篙贸,在MRC
環(huán)境沒有強引用說法,應該描述為沒有對外邊person
進行retain
操作枫疆,但為了好理解 so...)爵川,所以在執(zhí)行完[person release]
以后,雖然Block
還沒有離開其作用域(Block
作用域到return 0;
前到大括號)息楔,但person
就被釋放寝贡;可以通過[block copy]
將其復制到堆上,這樣內部就會對外邊的person
強引用(其實是retain操作
)從而敝狄溃活person
圃泡,當然在Block
銷毀的時候,內部對person
還會進行一次release
操作愿险,這樣一加一減洞焙,就保持了平衡;
要點:椪玻空間的Block
(NSStackBlock
)是不會對外邊auto
對象進行保活(ARC
環(huán)境表現(xiàn)為不會強引用熔任,MRC
下表現(xiàn)為不會進行retain
操作)褒链,只有拷貝到堆上(NSMallocBlock
)才會對其自動保活疑苔。
回到ARC
環(huán)境:
看一下__weak
作用:
int main(int argc, const char * argv[]) {
@autoreleasepool {
DJTBlock block;
{
DJTPerson *person = [[DJTPerson alloc] init];
person.age = 10;
// 這里使用 __weak 修飾
__weak DJTPerson *weakPerson = person;
block = ^{
NSLog(@"-----------%d",weakPerson.age);
};
}
NSLog(@"---------------"); // 打斷點
}
return 0;
}
依然在NSLog(@"---------------");
處打斷點甫匹,打印結果為:
05-block訪問對象類型的auto變量[87323:9930285] DJTPerson----dealloc
這說明,即使在ARC
環(huán)境,Block
被拷貝到堆上兵迅,由于我們用__weak
類型的__weakPerson
訪問了外部auto
變量抢韭,它也不會對外部person
進行強引用。
同樣我們把上述代碼編譯成C++
恍箭,由于弱引用需要運行時機制來支持刻恭,所以我們不能進行靜態(tài)編譯,還需要運行時調用扯夭,指定運行時系統(tǒng)版本鳍贾,所以編譯命令如下:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
在生成的C++
代碼中找到__main_block_impl_0
結構體,發(fā)現(xiàn)是一個弱引用:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
DJTPerson *__weak weakPerson;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, DJTPerson *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
總結一下:
-
Block
在棧上交洗,無論ARC
骑科、MRC
環(huán)境,block
內部都不會對外部對象類型的auto
變量產生強引用构拳,就算Block
內部生成強指針咆爽,也不會對外部person
產生強引用,因為Block
自己就在棧上置森,隨時可能被銷毀斗埂; -
Block
在堆上:
在ARC
環(huán)境下,訪問外部對象類型的auto
變量暇藏,編譯后:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
DJTPerson *__strong person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, DJTPerson *__strong _person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
在 __main_block_desc_0
結構體中蜜笤,多了兩個函數(shù):__main_block_copy_0
和__main_block_dispose_0
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};
分別看它們實現(xiàn):
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*/);}
當對Block
進行copy
操作時,會調用這個__main_block_copy_0
函數(shù)盐碱,在它內部調用_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/)
把兔,會對外部person
對象產生強引用或者弱引用,這取決于block
內部使用__strong
指針還是__weak
指針訪問瓮顽。
當Block
從堆上移除县好,會調用__main_block_dispose_0
函數(shù),它內部調用_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
暖混,會對外部person
對象進行一次release
操作缕贡。
在MRC
環(huán)境下,也是由這兩個函數(shù)決定是否進行retain
和release
操作拣播。
五晾咪、__block
修改變量及其本質
我們先看下面一段代碼:
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
DJTBlock block = ^{
age = 20; // 報錯:Variable is not assignable (missing __block type specifier)
NSLog(@"-----------%d",age);
};
}
return 0;
}
ARC
下直接在Block
內部修改age
會報錯,這就相當于在block
生成的結構體中FuncPtr
指向的函數(shù)中去修改main
函數(shù)中的局部變量(如果這里age
是static
或者全局變量贮配,可以修改谍倦,因為這兩種變量一直在內存中),上下文環(huán)境發(fā)生了改變泪勒,所以不能直接訪問age
昼蛀;我們使用__block
修飾age
變量宴猾,然后編譯成C++
:
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
DJTBlock block = ^{
age = 20;
NSLog(@"-----------%d",age);
};
}
return 0;
}
會生成下面結構體:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到在Block
結構體中多了__Block_byref_age_0 *age;
,看一下 __Block_byref_age_0
發(fā)現(xiàn)它也是一個結構體:
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
這里看出編譯器將 __block
修飾的變量(這里是age
)包裝成一個對象__Block_byref_age_0
(因為它內部有isa
指針叼旋,所以可以認為它是個對象)仇哆,Block
內部(__main_block_impl_0
結構體中)并不會直接擁有這個變量age
,而是擁有__Block_byref_age_0
這個結構體夫植,然后__Block_byref_age_0
結構體中有一個int age
變量讹剔,我們在Block內部改變age = 20
,實際上就是賦值給__Block_byref_age_0
結構體中的age
變量偷崩。
我們對__block int age = 10
轉化成的C++
代碼進行簡化:
// __block int age = 10;對應下面c++代碼:
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
// 進行簡化
__Block_byref_age_0 age = {
0,
&age,
0,
sizeof(__Block_byref_age_0),
10
};
對應到下面結構體初始化:
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
__forwarding
指針傳入&age
指向__Block_byref_age_0 age
結構體自己(這里&age
是結構體地址辟拷,不要混淆),10賦值給了__Block_byref_age_0
結構體內部的age
變量阐斜;我們再看下修改age
為20的代碼:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
// 這里是 age = 20;
(age->__forwarding->age) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders__h_yv1h9mrx1155q6tq7brn8b9m0000gp_T_main_05a705_mi_0,(age->__forwarding->age));
}
可以發(fā)現(xiàn)先通過 __cself->age
找到__Block_byref_age_0
結構體衫冻,然后(age->__forwarding->age) = 20;
通過__forwarding
指針修改結構體內部的age
變量,__forwarding
指向結構體自己谒出,那為什么要多此一舉通過__forwarding
指針去修改內部age
隅俘,而不通過結構體指針直接去修改呢?這是為了保證Block
被copy
到堆上時笤喳,不管訪問棧上還是堆上Block
为居,通過forwarding
指針都是找到堆上。
這里如果__block
修飾的是一個對象類型杀狡,比如下面代碼:
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
__block NSObject *obj = [[NSObject alloc] init];
DJTBlock block = ^{
obj = nil;
age = 20;
};
}
return 0;
}
轉換為C++
同樣會多生成一個對應的結構體蒙畴,只不過內部會多出兩個方法copy``和dispose
方法來負責相應的內存管理:
// __block age 對應的結構體
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
// __block NSObject 對應的結構體
struct __Block_byref_obj_1 {
void *__isa;
__Block_byref_obj_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *__strong obj;
};
下面看一個例子:
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *mutarray = [NSMutableArray array];
DJTBlock block = ^{
[mutarray addObject:@(12)];
[mutarray addObject:@(13)];
};
}
return 0;
}
這里不會報錯,是因為我們并沒有修改mutarray
指針呜象,而是在使用mutarray
指針膳凝,除非我們修改mutarray
指針的值,比如 mutarray = nil;
才需要__block
來修飾恭陡;
六蹬音、__block
的內存管理
我們知道,當Block
內部訪問外部對象類型的變量時休玩,如下面簡單代碼:
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *object = [[NSObject alloc] init];
DJTBlock block = ^{
NSLog(@"%p", object);
};
block();
}
return 0;
}
block
編譯成C++
后的結構:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSObject *__strong object; //內部強引用外部變量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _object, int flags=0) : object(_object) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到在block
結構體內部會生成一個強指針指向外邊的object
對象著淆,并且在block
被拷貝到堆上時,調用__main_block_desc_0
中的copy
函數(shù)拴疤,對這個指針指向的對象進行一次retain
操作永部,即引用計數(shù)+1
,當然如果用__weak
修飾object
會生NSObject *__weak object;
此時不會強引用;
那當我們用__block
修飾變量時呐矾,比如分別修飾基礎數(shù)據類型age
扬舒,和對象類型obj1
,如下代碼:
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
__block NSObject *obj1 = [[NSObject alloc] init];
NSObject *object = [[NSObject alloc] init];
DJTBlock block = ^{
NSLog(@"%d %p %p", age,obj1, object);
};
block();
}
return 0;
}
編譯成C++
:
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
struct __Block_byref_obj1_1 {
void *__isa;
__Block_byref_obj1_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *__strong obj1;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSObject *__strong object;
__Block_byref_age_0 *age; // by ref
__Block_byref_obj1_1 *obj1; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _object, __Block_byref_age_0 *_age, __Block_byref_obj1_1 *_obj1, int flags=0) : object(_object), age(_age->__forwarding), obj1(_obj1->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
經__block
修飾的變量age
和obj1
分別生成構體__Block_byref_age_0
和 __Block_byref_obj1_1
凫佛,它們的本質就是OC
對象讲坎,所以在block
對應的結構體內部生成兩個結構體指針指向這兩個對象,即
__Block_byref_age_0 *age; // by ref
__Block_byref_obj1_1 *obj1; // by ref
它們其實和object
一樣愧薛,因為__block
修飾的變量也是轉換成結構體晨炕,而且內部有isa
指針,其實就是OC
對象毫炉,所以也會在__main_block_desc_0
中生成兩個函數(shù):copy
和dispose
瓮栗,來管理對象的內存,可以看下結構:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src)
{
_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->obj1, (void*)src->obj1, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->object, (void*)src->object, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src)
{
_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_dispose((void*)src->obj1, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_dispose((void*)src->object, 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};
所以__block
內存管理可以總結一下:
當
Block
在棧上時瞄勾,內部并不會對__block
修飾的外部變量產生強引用-
當
Block
被copy
到堆上時,會調用Block
內部的copy
函數(shù),而copy
函數(shù)內部會調用_Block_object_assign
函數(shù)孩饼,它內部會對__block
變量形成強引用(retain
)萝衩。
-
當
Block
從堆上移除時,會調用Block
內部的dispose函數(shù)趾疚,dispose
函數(shù)內部會調用_Block_object_dispose
函數(shù)缨历,在_Block_object_dispose
函數(shù)中會對引用的__block
變量進行引用計數(shù)-1
(release
)
下面我們對比下Block
內部訪問外部變量幾種情況:
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
//直接將20存放在Block生成的結構體中
int num = 20;
//Block結構體內部生成一個強指針 強引用object對象
NSObject *object = [[NSObject alloc] init];
// Block內部生成一個弱指針 弱引用object對象
__weak NSObject *weakObject = object;
// Block內部生成一個結構體指針,指針指向的結構體內部存儲著變量age
__block int age = 10;
//Block內部生成一個結構體指針糙麦,指針指向的結構體內部存儲著變量obj1
__block NSObject *obj1 = [[NSObject alloc] init];
DJTBlock block = ^{
NSLog(@"%d %d %p %p %p",num, age, obj1, object, weakObject);
};
block();
}
return 0;
}
編譯成C++
看看block
結構體辛孵,和上邊注釋的一致:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int num;
NSObject *__strong object;
NSObject *__weak weakObject;
__Block_byref_age_0 *age; // by ref
__Block_byref_obj1_1 *obj1; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, NSObject *__strong _object, NSObject *__weak _weakObject, __Block_byref_age_0 *_age, __Block_byref_obj1_1 *_obj1, int flags=0) : num(_num), object(_object), weakObject(_weakObject), age(_age->__forwarding), obj1(_obj1->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
這里主要對比一下 對象類型的auto
變量和__block
修飾的變量內存管理的區(qū)別:
- 相同點:
- 當
Block
在棧上時,對它們都不會產生強引用 - 當
Block
拷貝到堆上時赡磅,都會通過copy
函數(shù)來處理它們:
(1)__block
變量(假設變量名叫做a
)
(2)對象類型的_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
auto
變量(假設變量名叫做p
)_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
- 當
Block
從堆上移除時魄缚,都會通過dispose
函數(shù)來釋放它們
(1)__block
變量(假設變量名叫做a
)
(2)對象類型的_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*
auto
變量(假設變量名叫做p
)_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
- 當
- 不同點(主要是在引用的問題上)
(1)對象類型的auto
變量,根據傳進來時是__strong
還是__weak
類型決定調用copy
函數(shù)時Block
內部對傳進來的變量進行強還是弱引用焚廊。
(2)如果時__block
類型的變量冶匹,比如__block int age = 20;
,它被封裝成一個OC
對象节值,調用copy
函數(shù)時Block
內部直接對它產生強引用徙硅,對它的內存進行管理,不存在__weak
修飾int age
這種操作搞疗,所以沒有弱引用這一說嗓蘑。(這里強引用的是age
轉換成的結構體對象,真正的age
變量的值存儲在結構體里邊)匿乃;
但是如果是下面代碼
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *object = [[NSObject alloc] init];
__block __weak NSObject *weakObject = object;
DJTBlock block = ^{
NSLog(@"%p %p", object, weakObject);
};
block();
}
return 0;
}
編譯成C++
:
struct __Block_byref_weakObject_0 {
void *__isa;
__Block_byref_weakObject_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *__weak weakObject;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSObject *__strong object;
__Block_byref_weakObject_0 *weakObject; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _object, __Block_byref_weakObject_0 *_weakObject, int flags=0) : object(_object), weakObject(_weakObject->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到在__block
修飾變量生成的結構體 __Block_byref_weakObject_0
內部桩皿,通過__weak
弱引用變量weakObject
,即Block
結構體內部是一個強指針指向__block
生成的結構體幢炸,即這句代碼
__Block_byref_weakObject_0 *weakObject;
(注意雖然名字中有`weak`但這是一個強指針)
而在結構體__Block_byref_weakObject_0
內部:
NSObject *__weak weakObject;
這才是一個弱指針泄隔,指向外部傳入的弱引用對象weakObject
,它表達了外部傳入變量的類型是__weak
還是__strong
注意:這里在MRC
下有個特殊情況宛徊,在__block
生成的結構體內部佛嬉,始終都是弱引用逻澳,不會對外邊對象進行強引用。
在
MRC
環(huán)境下驗證暖呕, 下面代碼在block();
調用前person
就已經掛了斜做,說明確實內部沒有強引用:
#import "DJTPerson.h"
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block DJTPerson *person = [[DJTPerson alloc] init];
DJTBlock block = [^{
NSLog(@" %p", person);
} copy];
[person release];
block();
}
return 0;
}
七、Block
相關問題
-
Block
的原理是怎樣的湾揽?本質是什么瓤逼? -
__block
的作用是什么?有什么使用注意點库物? -
Block
的屬性修飾詞為什么是copy
霸旗?使用Block
有哪些使用注意? -
Block
在修改NSMutableArray
戚揭,需不需要添加__block
诱告?
理解上邊原理再回答這些問題應該不難吧。