iOS開發中block隨處可見,什麼是block呢?block是一個匿名函數,是一個代碼塊,把代碼放在這個代碼塊中,在需要使用的時候進行調用。block會封裝函數以及函數的調用環境:
-封裝函數:是指block會把block內部的參數返回值、執行體封裝成一個函數,並且存儲該函數的內存地址。
-封裝函數的調用環境:是指block會捕獲變量,並把這些變量存儲起來。
一、block的類型和使用
iOS開發中有三種類型的block,他們分別是全局block,堆block、棧block。
全局block(NSGlobalBlock):沒有訪問外界局部普通變量的block就是全局block,存儲在全局區。
堆block(NSMallocBlock):對棧block進行copy操作返回的block就是堆block,存儲在堆區。
棧block(NSStackBlock):訪問了外界普通局部變量的block就是棧block,存儲在棧區。
注意:本章節的代碼均在MRC環境下開啓的調試,MRC下新建的僅訪問局部普通變量的block是棧block,進行copy之後變成堆block。ARC下編譯器會自動將創建的棧block轉換成堆block,如果不做轉換可以在block前添加__weak關鍵字。例如void(^__weak testBlock)() = ^{}這樣定義即可。
測試代碼
- (void)main{
int sValue = 20;
static int gValue = 20;
void(^gBlock)(void) = ^(){
NSLog(@"value=%d", gValue);
};
id cgBlock = [gBlock copy];
void(^sBlock)(void) = ^(){
NSLog(@"value=%d", sValue);
};
id csBlock = [sBlock copy];
NSLog(@"gBlock=%@,copy之後%@",gBlock,cgBlock);
NSLog(@"sBlock=%@,copy之後%@",sBlock,csBlock);
}
運行結果:
gBlock=<__NSGlobalBlock__: 0x107728490>,copy之後<__NSGlobalBlock__: 0x107728490>
sBlock=<__NSStackBlock__: 0x7ffee84e1b68>,copy之後<__NSMallocBlock__: 0x600002e20960>
通過如上代碼,我們知道如果僅訪問的局部靜態變量,那麼他仍然是個全部block,全部block copy之後仍然是個全局block(仍指向原來的內存區域)。如果訪問了局部普通變量,那麼他就是棧block,拷貝之後成了一個全新的堆block(指向的內存區域地址發生了變化)。
在使用的過程中,包括三個部分block的聲明(定義)、block的實現,block的調用:
block的聲明的完整寫法:返回值類型
+ (
+ ^
+ 屬性名稱
+ )
+ 參數列表
:
//作爲屬性
@property (nonatomic, copy) int (^calcHashValue)(id value);
//作爲函數參數:
- (void)getHashValue:(int(^)(id value)) calcHashValue
//可以採用別名的方式簡化block的定義:
typeof int (^CalcHashValue)(id value);
//作爲屬性
@property (nonatomic, copy) CalcHashValue calcHashValue;
//作爲函數參數:
- (void)getHashValue:(CalcHashValue)calcHashValue;
block的實現完整寫法:^
+ 返回值類型
+ 參數列表
+ 函數體
+;
:
//返回值類型:沒有的話可以不寫,也可以寫`void`;
//參數列表 :用小括號把參數列表包裹起來,參數之間用逗號分隔。沒有參數的話可以寫成`(void)`或者不寫。
self.calcHashValue = ^int(id value){ return 22222222;};
//無參數,無返回值案例:
typedef void(^TodoValue)(void);
@property (nonatomic, copy) TodoValue todoValue;
self.todoValue = ^void(void){};或者self.todoValue = ^{};
block的調用:
//需要注意的是在不能保證block有實現的時候,一定要檢查是否爲空
if(self.calcHashValue) self.calcHashValue(self);
block在使用的時候,會有一些需要注意的問題:
1.block內部不能修改局部變量(直接報錯:Variable is not assignable (missing __block type specifier)
)。如果需要修改則需要在局部變量的定義時加上__block
關鍵字。
__block int localValue = 10;
void(^sBlock)(void) = ^(){
NSLog(@"localValue=%d", localValue);//localValue=10
localValue = 20;
};
sBlock();
NSLog(@"localValue=%d", localValue);//localValue=20
2.block實現之後調用之前修改局部變量,block中拿到的仍然是舊值。如果要獲得最新值則需要在局部變量定義時加上__block
關鍵字。
//不加block關鍵字
int localValue = 10;
void(^sBlock)(void) = ^(){
NSLog(@"localValue=%d", localValue); //localValue=10
};
localValue = 20;
sBlock();
//加block關鍵字
__block int localValue = 10;
void(^sBlock)(void) = ^(){
NSLog(@"localValue=%d", localValue);//localValue=20
};
localValue = 20;
sBlock();
這個block關鍵字到底是做了什麼呢,我們通過clang看下block底層是如何實現的。
xcrun -sdk iphoneos clang -arch arm64e -rewrite-objc NXBlock.m
其中原始代碼如下:
@interface NXBlock : NSObject
- (void)t1;
- (void)t2;
@end
@implementation NXBlock
- (void)t1{
int localValue = 10;
void(^noneBlock)(void) = ^(){
NSLog(@"localValue=%d", localValue);
};
noneBlock();
}
- (void)t2{
__block int localValue = 10;
void(^withBlock)(void) = ^(){
NSLog(@"localValue=%d", localValue);
};
withBlock();
}
@end
生成.cpp文件後我們拷貝出相關的代碼片段:
如下本段是localValue沒有使用__block修飾的:
struct __NXBlock__t1_block_impl_0 {
struct __block_impl impl;
struct __NXBlock__t1_block_desc_0* Desc;
int localValue;
__NXBlock__t1_block_impl_0(void *fp, struct __NXBlock__t1_block_desc_0 *desc, int _localValue, int flags=0) : localValue(_localValue) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __NXBlock__t1_block_func_0(struct __NXBlock__t1_block_impl_0 *__cself) {
int localValue = __cself->localValue; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_fz_f2dd5f4545ggwp26d9pyjfwc0000gn_T_NXBlock_7c795f_mi_0, localValue);
}
static struct __NXBlock__t1_block_desc_0 {
size_t reserved;
size_t Block_size;
} __NXBlock__t1_block_desc_0_DATA = { 0, sizeof(struct __NXBlock__t1_block_impl_0)};
static void _I_NXBlock_t1(NXBlock * self, SEL _cmd) {
int localValue = 10;
void(*noneBlock)(void) = ((void (*)())&__NXBlock__t1_block_impl_0((void *)__NXBlock__t1_block_func_0, &__NXBlock__t1_block_desc_0_DATA, localValue));
((void (*)(__block_impl *))((__block_impl *)noneBlock)->FuncPtr)((__block_impl *)noneBlock);
}
如下本段是localValue使用__block修飾的:
struct __Block_byref_localValue_0 {
void *__isa;
__Block_byref_localValue_0 *__forwarding;
int __flags;
int __size;
int localValue;
};
struct __NXBlock__t2_block_impl_0 {
struct __block_impl impl;
struct __NXBlock__t2_block_desc_0* Desc;
__Block_byref_localValue_0 *localValue; // by ref
__NXBlock__t2_block_impl_0(void *fp, struct __NXBlock__t2_block_desc_0 *desc, __Block_byref_localValue_0 *_localValue, int flags=0) : localValue(_localValue->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __NXBlock__t2_block_func_0(struct __NXBlock__t2_block_impl_0 *__cself) {
__Block_byref_localValue_0 *localValue = __cself->localValue; // bound by ref
NSLog((NSString *)&__NSConstantStringImpl__var_folders_fz_f2dd5f4545ggwp26d9pyjfwc0000gn_T_NXBlock_7c795f_mi_1, (localValue->__forwarding->localValue));
}
static void __NXBlock__t2_block_copy_0(struct __NXBlock__t2_block_impl_0*dst, struct __NXBlock__t2_block_impl_0*src) {
_Block_object_assign((void*)&dst->localValue, (void*)src->localValue, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __NXBlock__t2_block_dispose_0(struct __NXBlock__t2_block_impl_0*src) {
_Block_object_dispose((void*)src->localValue, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static struct __NXBlock__t2_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __NXBlock__t2_block_impl_0*, struct __NXBlock__t2_block_impl_0*);
void (*dispose)(struct __NXBlock__t2_block_impl_0*);
} __NXBlock__t2_block_desc_0_DATA = { 0, sizeof(struct __NXBlock__t2_block_impl_0), __NXBlock__t2_block_copy_0, __NXBlock__t2_block_dispose_0};
static void _I_NXBlock_t2(NXBlock * self, SEL _cmd) {
__attribute__((__blocks__(byref))) __Block_byref_localValue_0 localValue = {(void*)0,(__Block_byref_localValue_0 *)&localValue, 0, sizeof(__Block_byref_localValue_0), 10};
void(*withBlock)(void) = ((void (*)())&__NXBlock__t2_block_impl_0((void *)__NXBlock__t2_block_func_0, &__NXBlock__t2_block_desc_0_DATA, (__Block_byref_localValue_0 *)&localValue, 570425344));
((void (*)(__block_impl *))((__block_impl *)withBlock)->FuncPtr)((__block_impl *)withBlock);
}
我們通過一個表格來對比兩者的異同:
對比 | 無修飾 | __block修飾 | static修飾 | __weak修飾 |
---|---|---|---|---|
定義 | int localValue = 10; | __block int localValue = 10; | static int localValue = 10; | __weak id weak self = self; |
完整結構與構造函數 | struct __NXBlock__t1_block_impl_0{}: 1.包括impl、Desc和localValue三個變量。 2.其中localValue這個外部局部變量在內部的定義仍然是int localValue。 3.構造函數包括調用的函數指針+描述+int型參數_localValue+flags,分別賦值給impl.FuncPtr、Desc、localValue和impl.Flags。 |
struct __NXBlock__t2_block_impl_0{}: 1.包括impl、Desc和localValue三個變量。 2.其中localValue這個外部局部變量在內部的定義爲__Block_byref_localValue_0 *localValue;是一個結構體指針; 3.構造函數包括調用的函數指針+描述+__Block_byref_localValue_0型參數_localValue+flags,分別賦值給impl.FuncPtr、Desc、localValue和impl.Flags。 |
struct __NXBlock__t3_block_impl_0{}; 1.包括impl、Desc和localValue三個變量。 2.其中localValue這個外部變量在內部的定義是int localValue。 3.構造函數包括調用的函數指針+描述+int類型參數_localValue+flags,分別賦值給impl.FuncPtr、Desc、localValue和impl.Flags。 |
struct __NXBlock__t4_block_impl_0{}; 1.包括impl、Desc和localValue三個變量。 2.其中localValue這個外部變量在內部的定義是__weak id localValue。 3.構造函數包括調用的函數指針+描述+weak id類型參數_localValue+flags,分別賦值給impl.FuncPtr、Desc、localValue和impl.Flags |
外部變量在內部的定義 | struct->localValue訪問原始數據; | 封裝在struct __Block_byref_localValue_0{}結構體中 1. 整形int類型變量localValue存儲了外部傳入的localValue值. 2.結構體指針類型__Block_byref_localValue_0 *的變量指向自己(localValue(_localValue->__forwarding)說明了這一點)。 3.struct->localValue->__forwarding->localValue即是原始localValue; |
(*struct.localValue)訪問原始數據。 | struct.localValue訪問原始數據 |
block函數指針 | __NXBlock__t1_block_func_0(...)即是外部調用block的執行函數。 1.函數包括一個完整block的參數__cself。 2.通過__cself->localValue獲得外界的值。 |
__NXBlock__t2_block_func_0(...)即是外部調用block的執行函數。 1.函數包括一個完整block的參數__cself。 2.通過__cself->localValue->__forwarding->localValue獲得外界的值。 |
__NXBlock__t3_block_func_0(...)即是外部調用block的執行函數。 1.函數包括完整block的參數__cself。 2.通過(*__cself.localvalue)獲得外界的值。 |
__NXBlock__t4_block_func_0(...)即是外部調用block的執行函數。 1.函數包括完整block的參數__cself。 2.通過__cself.localvalue獲得外界的值 |
描述結構 | __NXBlock__t1_block_desc_0: 1:保留字段reserved默認值爲0; 2.Block_size記錄完成結構體的大小; |
__NXBlock__t2_block_desc_0; 1.保留字段reserved默認值爲0。 2.Block_size記錄完成結構體的大小; 3.copy函數指針; 4.dispose函數指針; |
__NXBlock__t3_block_desc_0: 1:保留字段reserved默認值爲0; 2.Block_size記錄完成結構體的大小; |
__NXBlock__t4_block_desc_0: 1:保留字段reserved默認值爲0; 2.Block_size記錄完成結構體的大小; |
copy函數 | / | __NXBlock__t2_block_copy_0; | / | / |
dispose函數 | / | __NXBlock__t2_block_dispose_0; | / | / |
__Block_byref_localValue_0構造過程中傳給__forwarding的是__Block_byref_localValue_0類型,是變量自身,也就是byref.__forwarding指向的是自己,這個是block在棧區的情況。當棧區block被複制到堆區的時候,byref結構體也會被複制一份,並且複製出來的copy. __forwarding = copy;並且byref. __forwarding = copy了。這樣以來,無論是通過原有的訪問原有的block還是拷貝的block,那麼通過block. byref. __forwarding獲取到的都是堆內存中的那一份。
我們可以通過一段代碼驗證這個結果:
__block int localValue = 10;
NSString *(^stackBlock)(void) = ^NSString *(){
return [NSString stringWithFormat:@"&localValue=%p", &localValue];
};
NSLog(@"拷貝前:%@-%@", stackBlock, stackBlock());
NSString *(^mallocBlock)(void) = [stackBlock copy];
NSLog(@"拷貝後:%@-%@", stackBlock, stackBlock());
NSLog(@"拷貝後:%@-%@", mallocBlock, mallocBlock());
打印結果
拷貝前:<__NSStackBlock__: 0x7ffee17f6b60>-&localValue=0x7ffee17f6ba8
拷貝後:<__NSStackBlock__: 0x7ffee17f6b60>-&localValue=0x600000472878
拷貝後:<__NSMallocBlock__: 0x600000a5d200>-&localValue=0x600000472878
可以看到拷貝後,新生成了一個堆block對象並且block捕獲的localValue的地址發生了變化,由0x7ffee17f6ba8變成了0x600000472878,而且原有的棧block捕獲的localValue的地址也由0x7ffee17f6ba8變成了0x600000472878,跟堆block保持一致。
細節邏輯可以在源碼中看到:
// src points to stack
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;
簡單總結:
1.沒有使用__block
關鍵字修飾的localValue是一個簡單int類型,傳入內部也是的int類型的localValue變量.
2.使用__block
關鍵字修飾的localValue會生成爲__Block_byref的結構體,傳入內部也是__Block_byref類型的localValue結構體指針。
3.棧上的block進行copy:block本身會在堆上開闢內存;__Block_byref在堆上新開闢內存;捕獲的外部變量也會在堆上新開闢內存。原有的棧block的__Block_byref的__forwarding會指向堆block的__Block_byref。
二、block中的循環引用
如果對象A強持有對象B:
-初始化完成後A的引用計數爲1,B的引用計數爲1。B的引用計數會因爲A的持有而+1變成2。
-在AB出作用域後系統會給AB分別發送一個release消息,A的引用計數-1變成0;B的引用計數-1變成1。
-A引用計數變成0調用dealloc方法進行銷燬,A調用dealloc方法時也會給B發送一個release消息,B的引用計數-1變成0,則B會調用dealloc方法進行銷燬。
如果對象A強持有對象B,B也強持有A:
-初始化完成後AB引用計數爲1,相互賦值後兩者的引用計數都變成2。
-AB出作用域後系統分別給AB發送release消息。A的引用計數變成1,B的引用計數變成1。
-兩者維持引用計數爲1而得不到釋放,造成內存泄漏。
而block中的循環引用出現的通常是由於self強制有block,而block又強持有self造成的循環引用。如果能通過weak斷開引用環那麼問題就解決了。
2.1.使用weak(/strong)修飾斷開環的方式避免循環引用:
準備一段代碼:
@interface NXTester: NSObject
@property (nonatomic, copy) void(^work)(void);
@property (nonatomic, copy) void(^test)(void);
@end
@implementation NXTester
- (void)dealloc{
NSLog(@"NXTester-dealloc");
}
@end
案例1:如下代碼會造成循環引用,tester持有work,work持有tester,形成環構成循環引用。解決方案看案例2.
NXTester *tester = [[NXTester alloc] init];
tester.work = ^{
tester;
};
tester.work();
案例2:如下代碼不會造成循環引用,tester持有work,work持有weakself, weakself弱持有tester。弱引用斷開了這個閉環,不會循環引用。
NXTester *tester = [[NXTester alloc] init];
__weak NXTester *weakself = tester;
tester.work = ^{
weakself;
};
tester.work();
案例3:如下代碼會造成循環引用,註釋掉的部分也會循環引用。因爲strongself強持有test,test又強持有strongself,形成環構成星環引用。
NXTester *tester = [[NXTester alloc] init];
__weak NXTester *weakself = tester;
tester.work = ^{
//weakself.test = ^{
// weakself;
//};
//weakself.test();
//或者
__strong NXTester *strongself = weakself;
strongself.test = ^{
strongself;
};
strongself.test();
};
tester.work();
案例4:如下代碼會造成循環引用。通過weak斷開了引用環。
NXTester *tester = [[NXTester alloc] init];
__weak NXTester *weakself = tester;
tester.work = ^{
__strong NXTester *strongself = weakself;
__weak NXTester *weakweakself = strongself;
strongself.test = ^{
weakweakself;
};
strongself.test();
};
tester.work();
如上我們看到第一種解決循環引用的方式是weak-stong修飾。block內部需要訪問的變量用在block外先用weak修飾,內部使用時在用strong修飾。
2.2、使用參數傳遞來解決循環引用的問題:
以上案例中,我們需要在block內部捕獲外部的變量。我們也可以通過參數傳遞的方法來解決。
@interface NXTester: NSObject
@property (nonatomic, copy) void(^work)(NXTester *value);
@end
@implementation NXTester
- (void)dealloc{
NSLog(@"NXTester-dealloc");
}
@end
使用:
NXTester *tester = [[NXTester alloc] init];
tester.work = ^(NXTester *value){
value;
};
tester.work(tester);
2.3.使用完畢後將對象設置爲nil
如果一個通過block執行任務後不再需要保留了,那麼可以在block中將對象設置爲nil,或在不使用之後把block或者對象設置爲nil都可以解決問題。這種方案不推薦。
注意事項:
有些場景下不會活着不一定會造成循環引用,關鍵看有沒有無法斷開的引用環。