1. 程式人生 > IOS開發 >iOS愛上底層-Block實現與原理

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:有兩個成員:reservedBlock_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內部進行修改。

解決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加入到弱引用技術表,但是這並不足以解釋打破迴圈引用的原因。而是weakSelfself並不是同一個點,他們指向的空間都是同一個。而最關鍵的是這一行程式碼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