當需要執行異步操作,或同步多個操作時,塊(Block)會非常有用。這一篇文章將介紹 Block 的本質。如果你對 block 還不瞭解,推薦先查看Block的用法。
1. Block的本質
Block 是封裝了函數調用及函數調用環境的 Objective-C 對象,內部也有一個 isa 指針。即 Block 本質上也是一個 Objective-C 對象。
下面寫一個簡單的 block:
int age = 10;
void(^myblock)(void) = ^{
NSLog(@"age: %d", age);
};
myblock();
使用 clang 命令將上述代碼轉化爲 C++,方便查看 block 內部結構:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
轉化後如下:
int age = 10;
// 定義block變量
void(*myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
// 調用block
((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);
1.1 聲明 block
1.1.1 __main_block_impl_0
通過轉化的 C++ 代碼可以看到,block定義中調用了__main_block_impl_0
函數,並將其地址賦值給myblock
。進一步查看__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。會將傳入的_age賦值給成員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
結構體內的構造函數對變量進行賦值,最終返回__main_block_impl_0
結構體,也就是最終返回給myblock
變量的是__main_block_impl_0
結構體。
1.1.2 __main_block_func_0
__main_block_impl_0
函數的第一個參數是__main_block_func_0
,其定義如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_05_pj1lwvjs50j3gx6vjtvxcvf80000gn_T_main_63c9df_mi_0, age);
}
__main_block_func_0
函數內存儲着 block 內代碼。其函數內部先取出局部變量 age,後面調用NSLog
。
也就是將 block 內的代碼封裝到__main_block_func_0
函數,將__main_block_func_0
函數地址傳遞給__main_block_impl_0
。
1.1.3 __main_block_desc_0
__main_block_impl_0
函數的第二個參數是__main_block_desc_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)};
__main_block_desc_0
中存儲着兩個成員,reserved
和Block_size
。並且爲reserved
賦值0,爲Block_size
賦值sizeof(struct __main_block_impl_0)
,即block的大小。
最終,將__main_block_desc_0
結構體傳給__main_block_impl_0
中,賦值給desc。
1.1.4 age
__main_block_impl_0
函數的第三個參數是age
,即定義的局部變量。
如果在 block 中使用了局部變量,block 聲明的時候會將 age 作爲參數傳入,即 block 會捕獲(capture)age。如果 block 中沒有使用 age,則只會給__main_block_impl_0
函數傳入__main_block_func_0
和__main_block_desc_0_DATA
參數。
由於 block 在聲明時捕獲了局部變量,在聲明後、調用前修改局部變量值,不會影響 block 內捕獲到的局部變量值。如下所示:
int age = 10;
void(^myblock)(void) = ^{
NSLog(@"age: %d", age);
};
age = 11;
myblock();
執行後,控制檯打印如下:
age: 10
block 在定義之後已將局部變量age值存儲在__main_block_impl_0
結構體,調用時直接從結構體中取出。聲明後修改局部變量的值不會影響__main_block_impl_0
結構體捕獲的值。
1.1.5 __block_impl
__main_block_impl_0
結構體第一個成員是__block_impl
結構體,__block_impl
結構體如下:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
isa
指針指向類對象,FuncPtr
指針存儲着__main_block_func_0
函數地址,即 block 內代碼地址。
__block_impl
結構體第一個成員就是 isa 指針。Objective-C對象本質上也是結構體,第一個成員也是 isa 指針。因此,block 本質上也是一個 OC 對象。__main_block_impl_0
函數的構造函數將傳入 block 的值存儲到__main_block_impl_0
結構體中,最終將__main_block_impl_0
結構體地址賦值給myblock。
分析__main_block_impl_0
構造函數,特點如下:
-
__main_block_func_0
封裝了函數地址,其中先取出局部變量,再調用 block 內代碼。 -
__main_block_desc_0_DATA
封裝 block 大小。 -
age
是 block 捕獲的局部變量。 -
__main_block_impl_0
結構體中的__block_impl
結構體包含了isa指針、FuncPtr。
1.2 調用 block
// 調用block
((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);
將上述代碼中的強制轉換移除後,變爲下面的代碼:
(myblock->FuncPtr)(myblock);
調用myblock就是通過myblock找到FuncPtr
指針,然後進行調用。
myblock是指向__main_block_impl_0
結構體的指針,內部並沒有FuncPtr
指針,爲什麼這裏可以直接訪問?這是因爲__main_block_impl_0
結構體第一個成員是__block_impl
,而__block_impl
也是一個結構體,即__main_block_impl_0
可以改爲以下內容:
struct __main_block_impl_0 {
// 使用__block_impl直接替換
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
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_impl_0
結構體地址就是__block_impl
的地址,這樣也可以查找到FuncPtr
指針。
block 底層的數據結構也可以使用下面圖片表示:
2. 變量捕獲
除了包含可執行代碼,塊還具有捕獲塊以外值的能力。如果在一個方法內聲明瞭一個塊,該塊可以獲取方法內任何變量,也就是可以捕獲局部變量。
2.1 局部變量
2.1.1 auto變量
局部變量默認是 automatic variable 類型,簡寫爲auto
,一般省略不寫。當程序進入、離開局部變量作用域時,會自動分配、釋放內存。
auto
會自動捕獲到 block 內,__main_block_impl_0
結構體內增加了存儲局部變量的成員。block 內訪問auto
變量的方式是值傳遞,即直接將auto
變量傳遞給__main_block_impl_0
函數。
2.1.2 static變量
static
變量會一直存儲在內存中。block 會捕獲static
修飾的局部變量,訪問時使用指針訪問。
下面分別添加使用auto
、static
修飾的局部變量:
auto int age = 10;
static int weight = 125;
void(^myblock)(void) = ^{
NSLog(@"age: %d, weight: %d", age, weight);
};
myblock();
生成C++後如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 捕獲了age、weight。
int age;
int *weight;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_weight, int flags=0) : age(_age), weight(_weight) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
int *weight = __cself->weight; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_05_pj1lwvjs50j3gx6vjtvxcvf80000gn_T_main_e2f202_mi_0, age, (*weight));
}
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 age = 10;
static int weight = 125;
// age直接傳遞值,weight傳遞指針。
void(*myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &weight));
((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);
}
return 0;
}
可以看到,__main_block_impl_0
捕獲了age、weight,並且給__main_block_impl_0
函數傳遞age時直接傳遞值,傳遞weight時傳遞的是指針。
2.2 全局變量
block 是否會捕獲全局變量?以及如何使用?
添加以下全局變量:
int height = 170;
static int number = 11;
生成C++代碼如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
int *weight;
// 並沒有捕獲全局變量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_weight, int flags=0) : age(_age), weight(_weight) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
int *weight = __cself->weight; // bound by copy
// 直接使用height、number
NSLog((NSString *)&__NSConstantStringImpl__var_folders_05_pj1lwvjs50j3gx6vjtvxcvf80000gn_T_main_964e22_mi_0, age, (*weight), height, number);
}
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 age = 10;
static int weight = 125;
void(*myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &weight));
((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);
}
return 0;
}
可以看到__main_block_func_0
並沒有添加任何全局變量,而是直接使用。這是因爲全局變量會一直存放在內存中,全局都可以使用。
3. Block 的類型
既然 block 也是 OC 對象,那麼 block 是什麼類型呢?
聲明一個 block,並打印其父類,如下所示:
void(^myblock)(void) = ^{
NSLog(@"github.com/pro648");
};
NSLog(@"%@", [myblock class]);
NSLog(@"%@", [[myblock class] superclass]);
NSLog(@"%@", [[[myblock class] superclass] superclass]);
NSLog(@"%@", [[[[myblock class] superclass] superclass] superclass]);
輸出如下:
__NSGlobalBlock__
NSBlock
NSObject
(null)
即 block 的繼承關係是:__NSGlobalBlock__
: NSBlock
: NSObject
。進一步證實了 block 本質上也是一個 OC 對象。
定義三個不同的 block,分別打印其類型:
// 沒有調用外部變量的block
void(^myblock1)(void) = ^{
NSLog(@"github.com/pro648");
};
// 訪問auto變量
int age = 10;
void(^myblock2)(void) = ^{
NSLog(@"age: %d", age);
};
// 直接調用block的 class
NSLog(@"%@ %@ %@", [myblock1 class], [myblock2 class], [^{
NSLog(@"%d", age);
} class]);
打印如下:
__NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__
將上述代碼轉換爲C++,可以看到三個 block 類型都是_NSConcreteStackBlock
類型。這可能是 runtime 運行時進行了某種轉換,使用 clang 生成的C++代碼僅供參考,不能保證和運行時完全一致。
三種類型的block在內存中的位置如下:
__NSGlobalBlock__
、__NSStackBlock__
、__NSMallocBlock__
三種類型的block是按照以下規則產生的:
block 類型 | 環境 |
---|---|
__NSGlobalBlock__ |
沒有訪問auto變量 |
__NSStackBlock__ |
訪問了auto變量 |
__NSMallocBlock__ |
__NSStackBlock__ 調用了copy方法 |
3.1 __NSGlobalBlock__
當 block 內沒有訪問auto
變量時,block 爲__NSGlobalBlock__
類型,__NSGlobalBlock__
存在數據段中,程序結束纔會回收內存。但因爲其與普通函數沒有區別,很少使用__NSGlobalBlock__
類型的 block。
3.2 __NSStackBlock__
在 block 內訪問了auto
變量爲__NSStackBlock__
類型。
__NSStackBlock__
類型的 block 存放在棧中。棧的內存由系統自動分配和釋放,超出變量作用域後自動釋放。由於棧中代碼超出作用域之後,內存就會被銷燬,而有可能內存銷燬之後纔去調用它,此時就會出現問題。
ARC 自動管理內存時會幫助我們做很多事情,爲了方便理解其本質,先關閉 ARC 使用 MRC 管理內存。進入TARGETS > Build Settings > Objective-C Automatic Reference Counting,修改其值爲 NO。
關閉 ARC 後,使用以下代碼驗證問題:
void (^myblock)(void);
void test() {
// __NSStackBlock__
int age = 10;
myblock = ^{
NSLog(@"age: %d", age);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
myblock();
}
return 0;
}
執行後控制檯輸出如下:
age: -272632840
這是因爲myblock是在棧中的,即__NSStackBlock__
類型的。當test
函數執行完畢後,棧內存中 block 已經被系統回收。
3.3 __NSMallocBlock__
爲了避免函數執行完畢棧內存立即被回收,可以將__NSStackBlock__
block copy 到堆中。以下是修改後的代碼:
void (^myblock)(void);
void test() {
// __NSMallocBlock__
int age = 10;
// 將 block 從棧中複製到堆中。
myblock = [^{
NSLog(@"age: %d", age);
} copy];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
myblock();
}
return 0;
}
執行後控制檯輸出如下:
age: 10
block 調用 copy 後,類型改變如下所示:
block類型 | 內存區域 | 調用copy的效果 |
---|---|---|
__NSGlobalBlock__ |
數據段 | 什麼都不做,類型不變。 |
__NSStackBlock__ |
棧 | 從棧複製到堆,類型變爲__NSMallocBlock__
|
__NSMallocBlock__ |
堆 | 引用計數加一,類型不變。 |
使用 MRC 管理內存時,經常需要使用 copy 保存 block,將棧上的 block 複製到堆上,超出作用域時 block 不會被釋放,後續需調用 release 銷燬 block。ARC 環境下,系統會自動調用 copy 操作,使 block 不被銷燬;不再使用時,自動調用 release 引用計數減一。
4. ARC 在某些情況下會對 block 自動進行一次 copy 操作,將其從棧區移動到堆區
出現以下情況時,ARC 會自動對 block 執行一次 copy 操作,將其從棧區移動到堆區:
- 當 block 作爲函數返回值時。
- 當 block 被強指針引用時。
- 當 Cocoa API 方法名包含usingBlock,且 block 作爲參數時,或 block 作爲 GCD API 方法參數。
4.1 當 block 作爲函數返回值時
typedef void (^MyBlock)(void);
MyBlock test() {
int age = 10;
// myblock 作爲函數返回值,ARC 會自動進行copy。
MyBlock myblock = ^{
NSLog(@"age: %d", age);
};
return myblock;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyBlock myblock = test();
NSLog(@"%@", [myblock class]);
}
return 0;
}
在 ARC 環境下,參數返回值爲 block 類型時,系統會對 ARC 自動執行一次 copy 操作,使其變爲__NSMallocBlock__
類型。在 MRC 環境下,超出作用域後 block 會被銷燬,此時再調用會引起閃退。
4.2 當 block 被強指針引用時
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
MyBlock myblock = ^{
NSLog(@"age: %d", age);
};
NSLog(@"%@",[myblock class]);
}
return 0;
}
由於 block 訪問了auto
變量,其是__NSStackBlock__
類型。在 MRC 環境中,不會自動進行 copy 操作,輸出是__NSStackBlock__
;在 ARC 環境中,有強指針引用時會自動執行 copy 操作,將 block 從棧中移動到堆中。
修改上述代碼如下,即取消強指針對 block 的引用:
int age = 10;
// 取消強指針的引用
NSLog(@"%@",[^{
NSLog(@"age: %d", age);
} class]);
可以看到輸出爲:
__NSStackBlock__
手動調用 copy,如下所示:
int age = 10;
NSLog(@"%@", [[^{
NSLog(@"age: %d", age);
} copy] class]);
輸出爲:
__NSMallocBlock__
這也進一步證明了 ARC 環境下,有強指針引用 block 時會自動調用 copy 方法。
4.3 當 Cocoa API 方法名包含usingBlock,且 block 作爲參數時,或 block 作爲 GCD API 的方法參數
當 Cocoa API 方法名包含usingBlock,且 block 作爲參數時,或 block 作爲 GCD API 的方法參數。ARC 會根據情況自動將棧上的 block copy到堆上。
// Cocoa API
NSArray *arr = @[@1];
[arr enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
// 這個 block 在堆上
}];
// GCD API
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 這個 block 在堆上
});
block 作爲屬性時與其它屬性類似,但 MRC 環境下,只能使用copy
修飾。因爲,block 訪問auto
變量時,block 是__NSStackBlock__
類型,超出作用域 block 會被自動銷燬。如果想要在外部繼續訪問、調用 block,就需要將 block 從棧中複製到堆中,因此需用copy
修飾。
在 ARC 環境下,系統會在需要時自動進行 copy 操作。此時屬性可以使用strong
,但copy
更能表明用意。
5. Block 內引用對象
之前 block 內只引用過基本數據類型,這一部分介紹 block 內引用對象類型。如下所示:
int main(int argc, const char * argv[]) {
@autoreleasepool {
{
Person *person = [[Person alloc] init];
person.age = 10;
^{
NSLog(@"person.age = %d", person.age);
}();
}
NSLog(@"--------");
}
return 0;
}
執行後控制檯輸出如下:
person.age = 10
-[Person dealloc]
--------
可以看到在打印虛線前person
已經釋放。此時,block 是棧類型 block,即__NSStackBlock__
。棧區 block 即便引用了對象,也會在超出作用域時一起釋放。
更新上述代碼如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyBlock myblock;
{
Person *person = [[Person alloc] init];
person.age = 10;
myblock = ^{
NSLog(@"person.age = %d", person.age);
};
myblock();
}
NSLog(@"--------");
}
return 0;
}
執行後輸出如下:
person.age = 10
--------
-[Person dealloc]
可以看到執行到虛線位置時,person對象並沒有釋放。這是因爲 block 內部對person對象進行了強引用,block 又被 myblock 強指針引用,即 block 是堆類型。堆類型的 block 會對外部對象強引用。
使用以下命令生成 C++ 代碼,查看其底層實現:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 main.m
查看 C++ 代碼,block 定義如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__strong person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__strong _person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__main_block_desc_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};
與 block 內引用基本數據類型相比,__main_block_desc_0
內增加了copy
和dispose
兩個參數,用於管理對象內存。
copy
操作調用的是__main_block_copy_0
,如下所示:
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*/);
}
最終調用_Block_object_assign
函數,_Block_object_assign
會對引用的對象person
進行引用計數操作。如果引用的對象是__strong
修飾(默認是__strong
,即忽略時就是__strong
),則引用計數加一;如果使用的__weak
修飾,則引用計數不變。
當 block 執行完畢,會調用dispose
方法,dispose
底層會調用以下方法:
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->person,
3/*BLOCK_FIELD_IS_OBJECT*/);
}
__main_block_dispose_0
內部會調用_Block_object_dispose
方法。如果之前 copy 時使用了強引用,此時引用計數減一;如果之前使用了弱引用,直接取消對原來對象的弱引用。
6. Block 內修改外部變量
如果外部變量是auto
類型,block 通過值傳遞的方式捕獲變量。由於是值傳遞的方式進行的,其不能修改外部變量。如果需要外部變量,可以通過以下兩種方式:
- 使用 static 修飾外部變量。
- 使用
__block
修飾外部變量。
6.1 使用 static 修飾外部變量
使用 static 修飾的變量會一直存在內存中,程序結束前不會被釋放。block 捕獲時通過引用方式進行,即傳遞地址。因此,使用 static 修飾的外部變量可以直接修改值。
6.2 使用__block
修飾外部變量
使用 static 修飾的變量會一直存放在內存中,直到程序結束,這不利於性能優化。
使用__block
修飾外部變量,也可以達到在 block 內修改成員變量的目的,那__block
底層是如何實現的呢?
__block
不能修飾全局變量、靜態變量。
下面代碼使用__block
修飾局部變量:
__block int age = 10;
MyBlock myblock = ^{
age = 20;
NSLog(@"age: %d", age);
};
myblock();
使用以下命令將其轉換爲 C++:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 main.m
轉換後的 block 定義如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// age 被封裝成了對象。
__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
對象類型,__Block_byref_age_0
聲明如下:
struct __Block_byref_age_0 {
// 也有isa指針,即也是對象類型。
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
// 值
int age;
};
__Block_byref_age_0
結構體也有isa指針,即也是對象類型。
使用__block
修飾的age
被轉換爲:
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {
(void*)0,(__Block_byref_age_0 *)&age,
0,
sizeof(__Block_byref_age_0),
10
};
__main_block_func_0
函數被轉換爲:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
// 使用age的forwarding指向age。
(age->__forwarding->age) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_05_pj1lwvjs50j3gx6vjtvxcvf80000gn_T_main_d88942_mi_0, (age->__forwarding->age));
}
使用age
的__forwarding
取出變量地址,這樣即使 block 從棧移動到了堆上,也可以正確修改變量值。
7. 對象類型的auto
變量、__block
變量
在 block 內訪問了使用auto
、__block
修飾的對象類型的變量:
如果 block 在棧上,將不會對變量產生強引用。
-
如果 block 被拷貝到堆上
會調用 block 內部的copy函數。
copy函數內部會調用
_Block_object_assign
函數。-
_Block_object_assign
函數會根據變量修飾符__strong
、__weak
、__unsafe_unretained
做出相應操作,類似於 retain(形成強引用、弱引用)。使用
__block
修飾的變量只有在 ARC 環境中會根據__strong
、__weak
、__unsafe_unretained
修飾符進行強引用,在 MRC 環境中不會進行強引用。
-
如果 block 從堆上移除:
- 會調用 block 內部的 dispose 函數。
- dispose 函數內部會調用
_Block_object_dispose
函數。 -
_Block_object_dispose
函數會自動釋放引用的變量,類似於 release。
8. Block 的循環引用
使用 block 容易產生循環引用。如果類中定義了一個 block,在 block 內又訪問了類的屬性,就會導致循環引用。
Person
類中聲明瞭屬性age
和 myblock,main.m
文件中爲 block 賦值,如下所示:
typedef void(^MyBlock)(void);
@interface Person : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, copy) MyBlock myblock;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.age = 10;
person.myblock = ^{
NSLog(@"age: %d", 20);
};
}
NSLog(@"-------");
return 0;
}
執行後輸出如下:
-[Person dealloc]
-------
可以看到person
先釋放,後打印虛線。
更新myblock
賦值語句如下:
person.myblock = ^{
NSLog(@"age: %d", person.age);
};
再次執行後,控制檯只輸出了虛線,person
類沒有被釋放。
因爲person
強引用了myblock
,此時myblock
在堆上;myblock
內訪問了person
對象,堆上的 block 會對對象進行強引用。此時person
強引用myblock
,myblock
強引用person
,形成了循環引用。
不止訪問
person
會產生循環引用,在person
類裏的 block 內訪問成員變量也會產生循環引用,因爲訪問成員變量本質上是在調用self->instance
,即仍然訪問了self
。此外,OC 方法轉換爲 C 語言方法後,默認帶有兩個參數。第一個是 id 類型的
self
,第二個參數是SEL
類型的_cmd
,因此,平常訪問的self
也是局部變量。void test(id self, SEL _cmd) { }
ARC 環境下有以下三種解決循環引用的方案:
- 使用
__weak
修飾變量。 - 使用
__unsafe_unretained
修飾變量。 - 使用
__block
修飾變量,同時在 block 內將變量設置爲nil,最後確保調用 block。
下面詳細介紹解決循環引用的方案。
8.1 使用__weak
修飾變量
使用__weak
修飾變量,更新如下:
__weak typeof(person) weakPerson = person;
person.myblock = ^{
NSLog(@"age: %d", weakPerson.age);
};
將其轉換爲 C++ 代碼,__main_block_impl_0
函數如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 對捕獲的person進行弱引用。
Person *__weak weakPerson;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
此時執行後,超出person
作用域,person
就會釋放。
8.2 使用__unsafe_unretained
修飾變量
使用__unsafe_unretained
修飾變量也可以解決循環引用問題。
__unsafe_unretained
與__weak
區別在於:
-
__weak
:不會產生強引用。指向的對象銷燬時,會自動讓指針置爲nil。 -
__unsafe_unretained
:不會產生強引用,但沒有__weak
安全。指向對象銷燬時,指針存儲地址不變,但內存已經被回收,再次訪問時產生野指針錯誤。
8.3 使用__block
修飾變量,同時在 block 內將變量設置爲nil,最後確保調用 block
使用__block
也可以解決循環引用問題:
// 1.添加__block修飾符
__block Person *person = [[Person alloc] init];
person.age = 10;
person.myblock = ^{
NSLog(@"age: %d", person.age);
// 2.置爲nil
person = nil;
};
// 3.調用block()
person.myblock();
使用__block
解決循環引用問題時,上述三步缺一不可。其缺點就是必須調用 block,如果沒有調用 block,就無法在 block 執行完畢後將person
置爲nil,就無法解決循環引用問題。
在 MRC 環境中,有以下兩種方案解決循環引用問題:
- 使用
__unsafe_unretained
,MRC 不支持弱指針__weak
。 - 直接使用
__block
。在 MRC 環境中,__block
結構體不會對結構體內對象進行強引用,不會產生循環引用。
參考資料:
- Cocoa blocks as strong pointers vs copy
- A look inside blocks: Episode 3 (Block_copy)
- How blocks are implemented (and the consequences)
- Objective-C Blocks Ins And Outs
歡迎更多指正:https://github.com/pro648/tips
本文地址:https://github.com/pro648/tips/blob/master/sources/Block的本質.md