在iOS中,block編程使用得很頻繁,我們不僅要會用block,更需要理解block的底層實現(xiàn)原理。筆者在面試中,block問題是必問的。
什么是block
block是iOS中對閉包的實現(xiàn),什么是閉包呢?閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函數(shù)閉包(function closures),是在支持頭等函數(shù)的編程語言中實現(xiàn)詞法綁定的一種技術。閉包在實現(xiàn)上是一個結(jié)構(gòu)體,它存儲了一個函數(shù)(通常是其入口地址)和一個關聯(lián)的環(huán)境(相當于一個符號查找表)。環(huán)境里是若干對符號和值的對應關系,它既要包括約束變量(該函數(shù)內(nèi)部綁定的符號),也要包括自由變量(在函數(shù)外部定義但在函數(shù)內(nèi)被引用),有些函數(shù)也可能沒有自由變量。
block類型
block是一個OC對象,block類型有NSStackBlock、NSMallocBlock、NSGlobalBlock、,分別分配在棧、堆、全局存儲區(qū)域中。他們都繼承于NSObject。下面代碼證明打印了NSGlobalBlock的繼承鏈
void (^block)(void) = ^{
NSLog(@"akon");
};
NSLog(@"block.class = %@", [block class]);
NSLog(@"block.class.superclass = %@", [[block class] superclass]);
NSLog(@"block.class.superclass.superclass = %@", [[[block class] superclass] superclass]);
NSLog(@"block.class.superclass.superclass.superclass = %@", [[[[block class] superclass] superclass] superclass]);
運行結(jié)果為:
2020-11-13 18:39:02.919351+0800 BlockTestDemo[86009:2083840] block.class = __NSGlobalBlock__
2020-11-13 18:39:02.919562+0800 BlockTestDemo[86009:2083840] block.class.superclass = NSBlock
2020-11-13 18:39:02.919713+0800 BlockTestDemo[86009:2083840] block.class.superclass.superclass = NSObject
2020-11-13 18:39:02.923424+0800 BlockTestDemo[86009:2083840] block.class.superclass.superclass.superclass = (null)
下面表格列出了MRC和ARC環(huán)境下block類型
MRC下block類型
| 類型 | 環(huán)境 |
|---|---|
| NSGlobalBlock | 只訪問了靜態(tài)變量(包括全局靜態(tài)變量和局部靜態(tài)變量)和全局變量 |
| NSStackBlock | 沒訪問靜態(tài)變量和全局變量 |
| NSMallocBlock | NSStackBlock調(diào)用了copy |
執(zhí)行如下代碼,打印結(jié)果符合預期
__weak typeof(self)weakSelf = self;
static int a = 0;
void (^block1)(void) = ^{
a = 1;
b = 1; //b為全局變量
};
__block int c = 0;
void (^block2)(void) = ^{
NSLog(@"age:%d", weakSelf.age);
c = 1;
};
NSLog(@"block1.class = %@", [block1 class]);
NSLog(@"block2.class = %@", [block2 class]);
NSLog(@"block2 copy.class = %@", [[block2 copy] class]);
運行結(jié)果如下:
2020-11-14 22:45:54.457496+0800 BlockTestDemo[13178:426318] block1.class = __NSGlobalBlock__
2020-11-14 22:45:54.457616+0800 BlockTestDemo[13178:426318] block2.class = __NSStackBlock__
2020-11-14 22:45:54.457720+0800 BlockTestDemo[13178:426318] block2 copy.class = __NSMallocBlock__
ARC下block類型
| 類型 | 環(huán)境 |
|---|---|
| NSGlobalBlock | 只訪問了靜態(tài)變量(包括全局靜態(tài)變量和局部靜態(tài)變量)和全局變量 |
| NSMallocBlock | 沒訪問靜態(tài)變量和全局變量 |
運行上面的代碼,結(jié)果如下:
2020-11-14 22:45:54.457052+0800 BlockTestDemo[13178:426318] block1.class = __NSGlobalBlock__
2020-11-14 22:45:54.457211+0800 BlockTestDemo[13178:426318] block2.class = __NSMallocBlock__
2020-11-14 22:45:54.457356+0800 BlockTestDemo[13178:426318] block2 copy.class = __NSMallocBlock__
ARC下自動copy
- 我們看到block2為NSMallocBlock,這是因為編譯器做了優(yōu)化,在ARC下除了NSGlobalBlock_就是NSMallocBlock,沒有NSStackBlock;在MRC NSMallocBlock生成的條件是對block調(diào)用了copy操作。
- 在ARC環(huán)境下,編譯器會根據(jù)情況自動將棧上的block復制到堆上,copy的情況如下:
1、block作為函數(shù)返回值時
2、 將block賦值給_strong指針時
3、block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時
4、block作為GCD API的方法參數(shù)時
在ARC中對NSStackBlock調(diào)用copy變成NSMallocBlock,NSMallocBlock調(diào)用copy還是NSMallocBlock,引用計數(shù)+1,NSGlobalBlock調(diào)用copy啥都不做。 - copy底層原理
1、通過_Block_object_assign來對OC對象進行強引用或弱引用
2、通過_Block_object_dispose對OC進行清理
block數(shù)據(jù)結(jié)構(gòu)和變量捕獲
block數(shù)據(jù)結(jié)構(gòu)
寫下如下代碼,然后在終端進入.m文件所在目錄,執(zhí)行命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ArcClass.m 我們可以看到在當前目錄生成ArcClass.cpp文件。
int age = 18;
void (^block)(void) = ^{
NSLog(@"age is %d",age);
};
block();
我們可以看到上面的代碼變成了
int age = 18;
// block定義
void (*block)(void) = ((void (*)())&__ArcClass__TestArc_block_impl_0((void *)__ArcClass__TestArc_block_func_0, &__ArcClass__TestArc_block_desc_0_DATA, age));
// block調(diào)用
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
上面代碼刪除掉一些強制轉(zhuǎn)換的代碼簡化如下
int age = 18;
// block定義
void (*block)(void) = & __ArcClass__TestArc_block_impl_0(
&__ArcClass__TestArc_block_func_0,
& __ArcClass__TestArc_block_desc_0_DATA,
age
);
// block調(diào)用
block->FuncPtr(block);
我們可以看到block是指向__ArcClass__TestArc_block_impl_0對象的指針,結(jié)構(gòu)體__ArcClass__TestArc_block_impl_0定義如下:
struct __ArcClass__TestArc_block_impl_0 {
struct __block_impl impl;
struct __ArcClass__TestArc_block_desc_0* Desc;
int age;
__ArcClass__TestArc_block_impl_0(void *fp, struct __ArcClass__TestArc_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
該結(jié)構(gòu)體把age直接賦值給了_age,執(zhí)行的是拷貝操作。
- 結(jié)構(gòu)體中第一個成員變量是struct __block_impl impl;
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
__block_impl 的成員變量isa代表了該block屬于啥類型,本例中為_NSConcreteStackBlock ,F(xiàn)uncPtr代表block的調(diào)用方法,本例中為__ArcClass__TestArc_block_func_0
- 第二個成員變量是struct __ArcClass__TestArc_block_desc_0* Desc;
static struct __ArcClass__TestArc_block_desc_0 {
size_t reserved;
size_t Block_size;
} __ArcClass__TestArc_block_desc_0_DATA = { 0, sizeof(struct __ArcClass__TestArc_block_impl_0)};
desc描述了__ArcClass__TestArc_block_impl_0的大小
結(jié)構(gòu)體中第三個是成員變量age
該結(jié)構(gòu)體把age直接賦值給了_age,執(zhí)行的是拷貝操作。block調(diào)用實際上執(zhí)行的是__ArcClass__TestArc_block_func_0方法
下面為 block方法代碼NSLog(@"age is %d",age);的實現(xiàn)
static void __ArcClass__TestArc_block_func_0(struct __ArcClass__TestArc_block_impl_0 *__cself) {
//這里訪問age是bound by copy ,即拷貝。
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x0_cw796jjd255431nlsdwjt9840000gn_T_ArcClass_6c36ef_mi_0,age);
}
從上面的分析可以看到,定義一個block的時候,底層生成了一個代表block的結(jié)構(gòu)體__ArcClass__TestArc_block_impl_0,該結(jié)構(gòu)體有一個__block_impl類型的impl成員變量和代表捕獲變量的成員變量。其中impl的isa 代表了block的類型,F(xiàn)uncPtr代表了block的實際調(diào)用方法,該方法的參數(shù)為__ArcClass__TestArc_block_impl_0。
變量捕獲
可以按照上面分析思路,得出結(jié)論
| 變量類型 | 捕獲到block內(nèi)部 | 變量類型 |
|---|---|---|
| 局部非OC變量 | √ | 值傳遞 |
| 局部變量 static、OC對象 | √ | 指針傳遞 |
| 全局變量 | × | 直接訪問 |
可以看到全局變量,b
lock內(nèi)部不會直接捕獲,其他變量會捕獲。
__block變量
__block作用
- __block只能修飾非靜態(tài)局部變量,不能修飾靜態(tài)變量和全局變量,否則編譯器報錯。
- 當需要在block內(nèi)部修改一個局部變量時,需要加__block ,否則,編譯不過。下面的代碼,編譯報錯:Variable is not assignable (missing __block type specifier)。加上__block編譯通過,name會變成lbj
NSString* name = @"akon";
void (^block)(void) = ^{
name = @"lbj";
};
block();
底層實現(xiàn)
-
類似剛才的轉(zhuǎn)成cpp思路,分析得出結(jié)論如下圖??偨Y(jié)就是對于__block變量,底層會封裝成一個對象,其中通過__forwarding指向自己,來訪問真實的變量。
image 為什么要通過__forwarding訪問?
這是因為,如果__block變量在棧上,就可以直接訪問,但是如果已經(jīng)拷貝到了堆上,訪問的時候,還去訪問棧上的,就會出問題,所以,先根據(jù)__forwarding找到堆上的地址,然后再取值
循環(huán)引用
循環(huán)引用原因
當對象A和對象B互相引用時會造成循環(huán)引用。
循環(huán)引用解決方案
竟然對象A和對象B互相引用會造成循環(huán)引用,那就要斷開這個循環(huán)引用,可以通過__weak或者__unsafe_unretained,這兩者的區(qū)別是__unsafe_unretained當引用對象變?yōu)閚il時__unsafe_unretained對象不會自動置為nil,導致變?yōu)橐爸羔?,再次使用會崩潰?/p>
常見循環(huán)引用及解決
1) 在VC的cellForRowAtIndexPath方法中cell的block直接引用self或者直接以_形式引用屬性造成循環(huán)引用。
cell.clickBlock = ^{
self.name = @"akon";
};
cell.clickBlock = ^{
_name = @"akon";
};
解決方案:把self改成weakSelf;
__weak typeof(self)weakSelf = self;
cell.clickBlock = ^{
weakSelf.name = @"akon";
};
注意有的時候我們會在block里面寫成__strong typeof(weakSelf) strongSelf = weakSelf,然后再用strongSelf調(diào)用方案,這樣做的原因是防止在block執(zhí)行過程中weakSelf突然變成nil。
2)在cell的block中直接引用VC的成員變量造成循環(huán)引用。
//假設 _age為VC的成員變量
@interface TestVC(){
int _age;
}
cell.clickBlock = ^{
_age = 18;
};
解決方案有兩種:
- 用weak-strong dance
__weak typeof(self)weakSelf = self;
cell.clickBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
strongSelf->age = 18;
};
- 把成員變量改成屬性
//假設 _age為VC的成員變量
@interface TestVC()
@property(nonatomic, assign)int age;
@end
__weak typeof(self)weakSelf = self;
cell.clickBlock = ^{
weakSelf.age = 18;
};
3)delegate屬性聲明為strong,造成循環(huán)引用。
@interface TestView : UIView
@property(nonatomic, strong)id<TestViewDelegate> delegate;
@end
@interface TestVC()<TestViewDelegate>
@property (nonatomic, strong)TestView* testView;
@end
testView.delegate = self; //造成循環(huán)引用
解決方案:delegate聲明為weak
@interface TestView : UIView
@property(nonatomic, weak)id<TestViewDelegate> delegate;
@end
4)在block里面調(diào)用super,造成循環(huán)引用。
cell.clickBlock = ^{
[super goback]; //造成循環(huán)應用
};
解決方案,封裝goback調(diào)用
__weak typeof(self)weakSelf = self;
cell.clickBlock = ^{
[weakSelf _callSuperBack];
};
- (void) _callSuperBack{
[self goback];
}
5)block聲明為strong
解決方案:聲明為copy
6)NSTimer使用后不invalidate造成循環(huán)引用。
解決方案:
- NSTimer用完后invalidate;
- NSTimer分類封裝
+ (NSTimer *)ak_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)(void))block
repeats:(BOOL)repeats{
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(ak_blockInvoke:)
userInfo:[block copy]
repeats:repeats];
}
+ (void)ak_blockInvoke:(NSTimer*)timer{
void (^block)(void) = timer.userInfo;
if (block) {
block();
}
}
--
- 用YYWeakProxy來創(chuàng)建定時器
怎么檢測循環(huán)引用
- 靜態(tài)代碼分析。 通過Xcode->Product->Anaylze分析結(jié)果來處理;
- 動態(tài)分析。用MLeaksFinder(只能檢測OC泄露)或者Instrument或者OOMDetector(能檢測OC與C++泄露)。
