在iOS中烤低,block編程使用得很頻繁,我們不僅要會用block氯窍,更需要理解block的底層實現(xiàn)原理。筆者在面試中蹲堂,block問題是必問的荞驴。
什么是block
block是iOS中對閉包的實現(xiàn),什么是閉包呢贯城?閉包(英語:Closure)熊楼,又稱詞法閉包(Lexical Closure)或函數(shù)閉包(function closures),是在支持頭等函數(shù)的編程語言中實現(xiàn)詞法綁定的一種技術。閉包在實現(xiàn)上是一個結構體鲫骗,它存儲了一個函數(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]);
運行結果為:
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í)行如下代碼彤守,打印結果符合預期
__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]);
運行結果如下:
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)變量和全局變量 |
運行上面的代碼毯侦,結果如下:
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ù)結構和變量捕獲
在Block內(nèi)部無法修改外部數(shù)據(jù)的原因是在Block中訪問外部變量時,都會對其進行一份拷貝,需要注意這里的拷貝是直接拷貝,如果你在Block內(nèi)部和外部對變量對象進行打印,則可以看到其地址是不同的。Objective-C提供了_block關鍵字,使用它可以直接訪問原始變量钝诚。
變量捕獲
可以按照上面分析思路颖御,得出結論
變量類型 | 捕獲到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思路,分析得出結論如下圖舀透∷ㄆ保總結就是對于__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分析結果來處理;
- 動態(tài)分析河泳。用MLeaksFinder(只能檢測OC泄露)或者Instrument或者OOMDetector(能檢測OC與C++泄露)沃呢。