iOS愛上底層-Block實現與原理
前言
很多人在面試的時候都會被問到Block,那麼Block分為哪幾種型別呢? 其實Block共有6種型別,其中三種常用級別,分別是:_NSConcreteGlobalBlock
_NSConcreteStackBlock
_NSConcreteMallocBlock
,三種系統級別 ,分別是_NSConcreteAutoBlock
_NSConcreteFinalizingBlock
_NSConcreteWeakBlockVariable
,本次只介紹3種常用型別。
Block常見的三種型別
- 全域性Block(
__NSGlobalBlock__
)
- 堆Block(
__NSMallocBlock__
- 棧Block(
__NSStackBlock__
)
Block的本質是什麼?
把左邊OC(左邊)的程式碼編譯成C++(右邊),我們會發現原本的block在C++裡面被拆解成一個叫__main_block_impl_0
的建構函式,我們在往上看可以發現,它其實就是一個結構體物件。而這個建構函式傳了兩個值__main_block_func_0
和__main_block_desc_0_DATA
。__main_block_func_0
:就是Block所執行的內容,在C++底層則變成了一個函式。__main_block_desc_0_DATA
:有兩個成員:reserved
和Block_size
。Block是如何捕獲外界變數的?
我們建立一個區域性變數a=10,然後看C++程式碼,可以發現在__main_block_impl_0
結構體中建立了一個屬性int a,並且在函式實現中建立了一個臨時變數a,將結構體中的屬性賦值給他。由於這次拷貝是值拷貝,所以在函式裡面不能對當前的屬性進行修改。為了能夠改變a的值,要加上__block
,如下圖:
我們可以發現__block
修飾的臨時變數在C++中變成了一個結構體__Block_byref_a_0
。並且在呼叫函式的時候,傳的是a的指標,並且在函式的實現中,__Block_byref_a_0 *a = __cself->a
則是進行一次指標拷貝,所以用__block
解決Block迴圈引用的幾種方式
//迴圈引用
self.name = @"1234";
self.block = ^{
NSLog(@"%@",self.name);
};
self.block();
複製程式碼
很多人都知道Block會引起迴圈引用,如同上面這段程式碼,當self持有block,block持有self,就產生了迴圈引用。接下來就介紹3種解決迴圈引用的方式
1、__block
:我們只需要在self.name的下面建立一箇中介者vc(__block ViewController *vc = self
),然後將block裡面的self.name替換成vc.name,用完之後將vc置為nil即可。(俗稱中介者模式)
// 迴圈引用
self.name = @"1234";
__block ViewController *vc = self;
self.block = ^{
NSLog(@"%@",vc.name);
vc = nil;
};
self.block();
複製程式碼
2、傳參:我們只需要將當前的self當做引數傳入到block裡面,block內部會建立一個臨時變數vc,此時我們就打破了互相持有,就會解決迴圈引用的問題了
// 迴圈引用
self.name = @"1234";
self.block = ^(ViewController *vc){
NSLog(@"%@",vc.name);
};
self.block(self);
複製程式碼
3、weak and strong:這種方式也是最多人用的,既建立weak,但是有一個問題,如果新增一個延遲執行,weakSelf就會提前釋放,導致訪問不到外界變數,所以我們又需要在blcok裡面strong一下
// 迴圈引用
self.name = @"1234";
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(2 * NSEC_PER_SEC)),dispatch_get_main_queue(),^{
NSLog(@"%@",strongSelf.name);
});
};
self.block();
複製程式碼
Block打破迴圈引的原理
在彙編裡面,Block會掉用一個叫_Block_object_assign
的api。而解決迴圈引用的關鍵之處也在下面的程式碼中。
void _Block_object_assign(void *destArg,const void *object,const int flags) {
const void **dest = (const void **)destArg;
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_OBJECT:
_Block_retain_object(object);
*dest = object;
break;
case BLOCK_FIELD_IS_BLOCK:
*dest = _Block_copy(object);
break;
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
*dest = _Block_byref_copy(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
*dest = object;
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
*dest = object;
break;
default:
break;
}
}
複製程式碼
以__weak
為例。我們為瞭解決迴圈引用,通常都會寫上這樣一句程式碼:__weak typeof(self) weakSelf = self;
,將self加入到弱引用技術表,但是這並不足以解釋打破迴圈引用的原因。而是weakSelf
和self
並不是同一個點,他們指向的空間都是同一個。而最關鍵的是這一行程式碼const void **dest = (const void **)destArg;
,他copy的是weakSelf
的指標地址,而不是self,所以weakSelf
被釋放的時候,不會影響到self。
Block是如何從棧到堆?
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
if (!arg) return NULL;
// The following would be better done as a switch statement
aBlock = (struct Block_layout *)arg;
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
else {
// Its a stack block. Make a copy.
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
memmove(result,aBlock,aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
result->invoke = aBlock->invoke;
#endif
// reset refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
_Block_call_copy_helper(result,aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
result->isa = _NSConcreteMallocBlock;
return result;
}
}
複製程式碼
這裡就是當前Block從棧->堆的過程。
1、aBlock->flags & BLOCK_NEEDS_FREE
會判斷當前Block的引用計數器,因為block的引用計數器是不受runtime下層處理的,所以它由自己來進行管理。而且這裡的引用計數器是+2,而不是+1,因為+1會對別的屬性有影響,所以這裡是+2(如下)。
static int32_t latching_incr_int(volatile int32_t *where) {
while (1) {
int32_t old_value = *where;
if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
return BLOCK_REFCOUNT_MASK;
}
if (OSAtomicCompareAndSwapInt(old_value,old_value+2,where)) {
return old_value+2;
}
}
}
複製程式碼
2、aBlock->flags & BLOCK_IS_GLOBAL
判斷當前的Block是否為全域性變數,是的話就直接返回
3、else就是將block複製到棧中,首先會建立一個新的結構體result,然後將舊的Block全部拷貝到新的Block中,然後isa就被標記為_NSConcreteMallocBlock
。
總結
1、解決block迴圈引用的思路就是中介者模式。
2、Block的本質就是結構體
3、當Block捕獲到外界變數時,ARC下就會從全域性block變成堆block,MRC下依然還是棧Block