重新回味·Block·

Block-Mind

說起Block,我想大家再熟悉不過了。每天的開發(fā)過程中都在與他打著交道,簡潔高效的使用給我們的工作帶來了便利。

正是因?yàn)?strong>Block對于我們而言不可或缺,我更覺得應(yīng)該抽出時(shí)間好好認(rèn)識一下Block,了解一些他的優(yōu)缺點(diǎn)、過往經(jīng)歷,讓我們對它的理解更深一層,進(jìn)而開發(fā)出更加高效的程序。

對Block的認(rèn)識

Block與C函數(shù)的區(qū)別和聯(lián)系

Block是C語言的擴(kuò)充,常被稱為帶有局部變量的匿名函數(shù);
看一下正規(guī)的C語言函數(shù)定義:

int test(int count);

int result = test(10);

以上聲明了函數(shù)名為test函數(shù);
使用函數(shù)指針調(diào)用函數(shù):

int (*testFuncPtr)(int) = &test;
int result = (* testFuncPtr)(10);

對比Block,Block就可以定義為不帶名稱的函數(shù),說的直白一點(diǎn),就是匿名函數(shù),即不用顯式的定義函數(shù)名稱,但是匿名函數(shù)都有固定的一個(gè)表達(dá)式。

比如:

^(int count){
  printf("%d", count);
}

等價(jià)于:
void test(int count){
}

Block語法定義格式如下:

^ 返回值類型 參數(shù)列表 表達(dá)式

省略之后:

^ 表達(dá)式

通過上面的例子可以看出兩點(diǎn)不同:

  • 沒有函數(shù)名稱;
  • 帶有“^”符號;

Block的基本使用

可以通過定義Block類型的變量對Block進(jìn)行調(diào)用,與函數(shù)指針有相似之處,具體如下:

int (^block)(int) = ^(int count){
      return count + 1;
}
block(5);
Block與Block的變量

Block類型變量用途:局部變量、參數(shù)、靜態(tài)變量、全局變量以及靜態(tài)全局變量等等。

Block 可以作為參數(shù)進(jìn)行傳遞,也可以作為返回值進(jìn)行傳遞,具體如下:

- (void)testFuncWithBlock:(void (^)(int count))block{
    if (block) {
        block(5);
    }
}

// 將Block作為返回值,代碼摘自·Masonry·
- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}

具體以Masonry使用為例講解一下,Block調(diào)用:

// Masonry 在常用布局時(shí)用到方法如下:
make.top.equalTo(self.titleLabel.mas_bottom).offset(15);

// 有些同學(xué)可能對上述的鏈?zhǔn)秸{(diào)用不是很清楚,其實(shí)上述鏈?zhǔn)秸{(diào)用就是使用Block實(shí)現(xiàn)的,分解如下:
MASConstraint *constraint = make.top;
MASConstraint *(^equalBlk)(id) = constraint.equalTo;
constraint = equalBlk(self.titleLabel.mas_bottom);
MASConstraint *(^offsetBlk)(CGFloat) = constraint.offset;
constraint = offsetBlk(15);

可使用關(guān)鍵字typedef,進(jìn)行Block類型變量的定義

typedef int(^Blk)(int);

// 調(diào)用
Blk blk = ^int (int count){
        return count ++;
    };
    int result = blk(5);

Block 作為屬性變量的用法:

@property (nonatomic, copy) void (^successCompletion)(int);
@property (nonatomic, copy) Blk failCompletion;

通過上面一小節(jié)了解到:

  • 函數(shù)與Block區(qū)別與聯(lián)系;
  • Block變量和Block表達(dá)式的聯(lián)系

Block的實(shí)質(zhì)

在前面小節(jié)中,介紹了Block的基本用法。那么問題來了,Block究竟是什么呢?現(xiàn)在公認(rèn)的說法是帶有自動(dòng)變量的匿名函數(shù),動(dòng)手一探究竟。

使用Clang -rewrite-objc命令對下面代碼進(jìn)行編譯:

void testBlock(){
    
    int (^blk)(int) = ^(int count){
        return count ++;
    };
    blk(5);
}

執(zhí)行完命令會(huì)在同級目錄下生成XX.cpp文件,這個(gè)文件就是編譯好的文件。
由于編譯后文件包含很多C函數(shù)聲明,在此就不一一解釋了,主要關(guān)注上述Block方法實(shí)現(xiàn):

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

static struct __testBlock_block_desc_0 {
    size_t reserved;
    size_t Block_size;
} __testBlock_block_desc_0_DATA = { 0, sizeof(struct __testBlock_block_impl_0)};

struct __testBlock_block_impl_0 {
    struct __block_impl impl;
    struct __testBlock_block_desc_0* Desc;
    
    __testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;?
        Desc = desc;
    }
};

static int __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself, int count) {
    
    return count ++;
}

函數(shù)具體的實(shí)現(xiàn)如下:

void testBlock(){
    
    int (*blk)(int) = ((int (*)(int))&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA));
    ((int (*)(__block_impl *, int))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, 5);
}

雖然編譯后Block方法很長,但是并不復(fù)雜,Block調(diào)用就像普通的C函數(shù)的使用。

  • 首先,看一下源代碼中Block方法體實(shí)現(xiàn):

int (^blk)(int) = ^(int count){
        return count ++;
    };

static int __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself, int count) {
    
    return count ++;
}

可以發(fā)現(xiàn)兩者在語法格式上非常相近,實(shí)際上Block所使用的匿名函數(shù)就是被作為簡單的C函數(shù)處理的。

在代碼中:

__testBlock_block_impl_0 *__cself

其中,參數(shù)__cself相當(dāng)于C++中指向?qū)嵗陨碜兞康?code>this或者Objective-C中的self。參數(shù)__cself在這里是__testBlock_block_impl_0結(jié)構(gòu)體的指針。

  • 然后,看一下__testBlock_block_impl_0結(jié)構(gòu)體的組成:
struct __testBlock_block_impl_0 {
         struct __block_impl impl;
         struct __testBlock_block_desc_0* Desc;
};

去掉代碼中加入的構(gòu)造函數(shù),感覺結(jié)構(gòu)上清晰多了。第一個(gè)成員變量是impl,__block_impl結(jié)構(gòu)體的聲明如下所示:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

其中,有關(guān)對象isa指針的理解可以參考之前寫過的文章Objective-C Runtime:深入理解類與對象

第二個(gè)成員變量Desc,主要保存block所在內(nèi)存的區(qū)域以及Block的大小。

  • 關(guān)于__testBlock_block_impl_0結(jié)構(gòu)體構(gòu)造函數(shù)如下:
__testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;?
        Desc = desc;
    }

isa指針指向_NSConcreteStackBlock, __testBlock_block_impl_0結(jié)構(gòu)體相當(dāng)于objc_object結(jié)構(gòu)體的Objective-C類對象的結(jié)構(gòu)體。

isa = &_NSConcreteStackBlock;

上述代碼表明_NSConcreteStackBlock相當(dāng)于objc_class結(jié)構(gòu)體的實(shí)例。從而得知,Block就是Objective-C的對象了。
具體關(guān)于Objective-C類與對象的知識可以參考另一篇文章深入理解類與對象。

對于上述構(gòu)造函數(shù)調(diào)用部分如下:

int (*blk)(int) = ((int (*)(int))&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA));
    ((int (*)(__block_impl *, int))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, 5);

看著有些繁瑣,對其去掉相關(guān)轉(zhuǎn)換的部分:

struct __testBlock_block_impl_0 temp = __testBlock_block_impl_0(__testBlock_block_func_0, &__testBlock_block_desc_0_DATA);
struct __testBlock_block_impl_0 *blk = & temp;
(*blk->impl.FuncPtr)(blk, 5);

上述代碼中,將__testBlock_block_impl_0結(jié)構(gòu)體實(shí)例的指針賦值給變量blk,從而知道源代碼中的Block表達(dá)式就是__testBlock_block_impl_0結(jié)構(gòu)體類型的變量,同時(shí)也是該結(jié)構(gòu)體在棧上生成的實(shí)例。

從函數(shù)指針調(diào)用得知,由Block語法轉(zhuǎn)換的__testBlock_block_func_0函數(shù)指針被賦值到__testBlock_block_impl_0的成員變量FuncPtr,同時(shí)說明了__testBlock_block_func_0中的__cself指向Block值。

截獲局部變量

在開發(fā)過程中,我們經(jīng)常遇到在Block中截獲局部變量或者改變局部變量,如下所示:

- (void)testBlock{
    
    int value = 1;
    void (^block) (void) = ^{
        NSLog(@"value = %d", value);
    };
    block();
}

看到這里,可能就有疑問了,Block內(nèi)部是如何獲取局部變量的,帶著疑問重新Clang了一下源文件,似乎找到了答案,摘取了關(guān)鍵代碼如下所示:


static void _I_TestBlock_testBlock(TestBlock * self, SEL _cmd) {

    int value = 1;
    void (*block) (void) = ((void (*)())&__TestBlock__testBlock_block_impl_0((void *)__TestBlock__testBlock_block_func_0, &__TestBlock__testBlock_block_desc_0_DATA, value));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

}

//Block內(nèi)部實(shí)現(xiàn):
static void __TestBlock__testBlock_block_func_0(struct __TestBlock__testBlock_block_impl_0 *__cself) {
  int value = __cself->value; // bound by copy
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_pf_nyb_8kl12yq6v2sxzwdr5v8w0000gn_T_TestBlock_516a43_mi_0, value);
}

// Block結(jié)構(gòu)體
struct __TestBlock__testBlock_block_impl_0 {
  struct __block_impl impl;
  struct __TestBlock__testBlock_block_desc_0* Desc;
 // 將局部變量追加成成員變量 
 int value;

  __TestBlock__testBlock_block_impl_0(void *fp, struct __TestBlock__testBlock_block_desc_0 *desc, int _value, int flags=0) : value(_value) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

通過上述Block語法表達(dá)式中使用的局部變量作為成員變量被加到了__TestBlock__testBlock_block_impl_0結(jié)構(gòu)體中,同時(shí),加到結(jié)構(gòu)體中的成員變量的類型與局部變量的類型保持一致。當(dāng)然了,如果Block表達(dá)式中沒有使用局部變量,則不會(huì)加入到Block的結(jié)構(gòu)體中。

通過初始化Block實(shí)例的構(gòu)造方法和調(diào)用也可以看到局部變量value作為參數(shù)傳遞,具體如下所示:

//Block 初始化方法
  __testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int _value, int flags=0) : value(_value)

// 初始化方法的調(diào)用:
void (*block) (void) = ((void (*)())&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA, value));

到這里,大致明白了Block如何截獲局部變量value的,小結(jié)如下:

  • 1、系統(tǒng)編譯時(shí),根據(jù)Block是否需要截獲外部變量來判斷是都將局部變量作為成員變量保存到Block的結(jié)構(gòu)體__testBlock_block_impl_0中;
  • 2、若需要截獲局部變量,需要在初始化結(jié)構(gòu)體函數(shù)中添加與局部變量相同類型的參數(shù);
  • 3、局部變量就通過初始化構(gòu)造函數(shù)傳遞到Block結(jié)構(gòu)體__testBlock_block_impl_0,同時(shí)賦值給Block中的成員變量;
  • 4、在執(zhí)行Block時(shí),局部變量value會(huì)初始化__testBlock_block_impl_0結(jié)構(gòu)體實(shí)例, 如下所示;
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = 0;
    impl.FuncPtr = __testBlock_block_func_0;
    Desc = &__testBlock_block_desc_0_DATA;
    value = 1;

總的來說,截獲局部變量就是將要使用的局部變量保存到Block結(jié)構(gòu)體實(shí)例中。

Block中使用C語言數(shù)組

  • 只使用C語言的字符串?dāng)?shù)組,也沒有向捕獲的局部變量賦值,然而下面的代碼再編譯時(shí)就會(huì)產(chǎn)生錯(cuò)誤,如下所示:
int testBlock1(){
    
    const char country[] = "China";
    void (^block) (void) = ^{
        printf("text=%c",  country[1]);
    };
    block();
    
    return 0;
}

編譯的時(shí)候會(huì)出現(xiàn)錯(cuò)誤:

錯(cuò)誤信息

看到這,你會(huì)感到疑惑,上面剛講到Block是如何截獲局部變量的,為什么到這里就失效了呢?

這是因?yàn)椋?strong>Block中,沒有實(shí)現(xiàn)對C語言數(shù)組的截獲。通過上述截獲局部變量的理論,C語言數(shù)組會(huì)作為成員變量保存到Block實(shí)例的結(jié)構(gòu)體中,在初始化Block時(shí),由成員變量賦值給局部變量,猜測代碼如下:

void func(char a[]){
    char b[] = a;
}

從而看出端倪,將C語言數(shù)組類型變量賦值給C語言數(shù)據(jù)類型變量,這個(gè)在C語言下是無法編譯,C語言不允許這樣編譯的。

如何解決呢?
目前,可以改成C語言數(shù)組的形式就可以解決上述問題了。

int testBlock1(){
    
    const char *country = "China";
    void (^block) (void) = ^{
        printf("text=%c", country[1]);
    };
    block();
    
    return 0;
}

__block關(guān)鍵字的作用

前面的小節(jié)中,我們僅僅講解了在Block中截獲局部變量,那么,在Block中修改截獲的局部變量又會(huì)帶來什么樣的問題呢?

修改截獲的局部變量

通過上面的代碼可以看出,雖然在Block中不能對局部變量進(jìn)行修改,但是在全局變量、靜態(tài)全局變量、靜態(tài)變量都是可以在Block中進(jìn)行修改的,具體如下:

int globalValue = 10;
static int staticGlobalValue = 5;
- (void)testBlockFunc {
    static int value = 5;
    void (^block)(void) = ^{
        
        globalValue = 11;
        staticGlobalValue = 12;
        value = 3;
        printf("globalValue = %d \n staticGlobalValue = %d \n value = %d \n", globalValue, staticGlobalValue, value);
    };
    
    block();
}

打印的結(jié)果如下:

2018-12-02 21:56:47.323727+0800 TestBlock[38880:1182883] 
globalValue = 11 
staticGlobalValue = 12 
value = 3

再次使用clang編譯了一下,發(fā)現(xiàn)靜態(tài)全局變量和全局變量轉(zhuǎn)換前后沒有任何變換,大家可能疑問靜態(tài)局部變量是如何轉(zhuǎn)換的呢?

__TestBlockMemory__testBlockFunc_block_impl_0 *__cself) {
  
    int *value = __cself->value; // bound by copy
        globalValue = 11;
        staticGlobalValue = 12;
        (*value) = 3;
        printf("globalValue = %d \n staticGlobalValue = %d \n value = %d \n", globalValue, staticGlobalValue, (*value));
    }

與截獲局部變量的轉(zhuǎn)換很相似,唯一的區(qū)別是Int類型的變量轉(zhuǎn)換成了指針,從而正確的改變了原有的值。

除了上述的幾種方式可以在Block中修改變量值外,OC專門提供了一個(gè)說明符——__block。

int testBlock(){
    __block int number = 1;
    void (^block) (void) = ^{
        number = 2;
    };
    block();
    return 0;
}

將上述代碼進(jìn)行Clang編譯,還原成以下代碼:

struct __Block_byref_number_0 {
  void *__isa;
__Block_byref_number_0 *__forwarding;
 int __flags;
 int __size;
 int number;
};

struct __testBlock_block_impl_0 {
  struct __block_impl impl;
  struct __testBlock_block_desc_0* Desc;
  __Block_byref_number_0 *number; // by ref
    
  __testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, __Block_byref_number_0 *_number, int flags=0) : number(_number->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself) {
  __Block_byref_number_0 *number = __cself->number; // bound by ref

        (number->__forwarding->number) = 2;
}

驚奇的發(fā)現(xiàn),局部變量number變成了結(jié)構(gòu)體__Block_byref_number_0,Block__testBlock_block_impl_0的結(jié)構(gòu)體實(shí)例持有__block變量的__Block_byref_number_0結(jié)構(gòu)體實(shí)例的指針。__testBlock_block_impl_0實(shí)例的成員變量__forwarding指向?qū)嵗陨恚⑼ㄟ^__forwarding變量訪問成員變量** number**。

__Block_byref_number_0 *number = __cself->number; // bound by ref
 (number->__forwarding->number) = 2;

值得注意一點(diǎn)是,__Block_byref_number_0結(jié)構(gòu)體并沒有在__testBlock_block_impl_0結(jié)構(gòu)體中,這樣主要是為了在多個(gè)Block中共用該__block變量。

Block的儲存

Block分為三類:** _NSConcreteStackBlock**

種類 存儲域 Block變量類型 復(fù)制后的存儲域
_NSConcreteStackBlock 局部/自動(dòng)變量 由棧復(fù)制到堆上
_NSConcreteGlobalBlock 程序的數(shù)據(jù)區(qū)域(.data區(qū)) 全局變量 保持在數(shù)據(jù)區(qū)
_NSConcreteMallocBlock 局部變量 增加引用計(jì)數(shù)

定義在全局的Block在變量作用域之外通過指針訪問使用,定義在棧上的__block局部變量和Block在超出作用域時(shí)將被廢棄。那Block超出作用域是如何存在的呢?

這里就用到Block的Copy操作,即將分配在棧上的Block__block變量復(fù)制到堆上,從而延長了Block__block變量的生命周期。

那么,Block被復(fù)制到堆上的操作是何時(shí)進(jìn)行的呢?
首先,比較明確的一點(diǎn)是:將Block作為返回值返回時(shí),編譯器將會(huì)自動(dòng)將Block由棧上復(fù)制到堆上,其他情況下,需要手動(dòng)執(zhí)行Copy操作;在ARC有效的情況下,Block是否被復(fù)制到堆上,編譯器會(huì)進(jìn)行相關(guān)的判斷;

舉個(gè)栗子:

typedef int (^BlockCase)(int);

BlockCase getBlockCallBack(int value) {
    return ^(int count) {
        return count + value;
    };
}

在ARC有效的情況下,作為返回值的Block會(huì)被復(fù)制到堆上。當(dāng)然了,系統(tǒng)中存在的一些API是無需手動(dòng)復(fù)制的:Cocoa框架方法中帶有usingBlock方法、GCD相關(guān)的API等等。

__block變量的存儲域

當(dāng)在Block中使用__block變量時(shí),當(dāng)在Block從棧復(fù)制到堆上同時(shí),被使用過的__block變量也會(huì)從棧復(fù)制到堆上。如下圖所示:

__block變量所分配的存儲域 Block從棧復(fù)制到堆上后的影響
由棧復(fù)制到堆上并被Block持有
被Block持有

__fawarding指針存在的意義

前邊介紹了Block和__block變量從棧復(fù)制到堆上的情景,但都與__fawarding指針脫不開關(guān)系。

  • 由棧復(fù)制到堆之前,__fawarding指針指向自身;
  • 復(fù)制之后,棧上的__fawarding指針指向復(fù)制到堆上的__block變量,堆上的__fawarding指針指向自身。
    從而,解釋了無論__block變量配置在棧上還是配置在堆上時(shí)都能正確的訪問到__block變量;

Block截獲對象

通過了解上節(jié)的內(nèi)容,很好理解以下內(nèi)容:

  • __block修飾的變量從棧復(fù)制到堆上,賦值給該__block變量的對象也被從棧復(fù)制到堆上,當(dāng)__block變量從堆上釋放時(shí),該對象才能得到釋放;
  • 當(dāng)使用__weak修飾的__block變量在賦值的時(shí)候,由于賦值對象的作用域問題而釋放,從而導(dǎo)致__block變量不能強(qiáng)持有該對象。

Block循環(huán)引用

形成原因:

  • 對象與Block相互持有;
  • 形成強(qiáng)持有環(huán);
    解決方式:
  • 設(shè)置weak弱持有關(guān)系;
  • 手動(dòng)置為nil,梳理好持有關(guān)系,破壞環(huán)狀結(jié)構(gòu)。

結(jié)束

關(guān)于Block就大致說到這吧,從Block的使用到Block的原理剖析,希望能讓我們更客觀的認(rèn)識Block,寫出更高質(zhì)量的代碼。

好久沒有更新文章了,想必老鐵們也都等著急了吧。主要是從2018年3月底更新后最后一篇文章,就一直在忙,關(guān)于那段時(shí)間的事情會(huì)在另一篇文章中詳述。

謝謝大家的支持!

掃一掃下面的二維碼,歡迎關(guān)注我的個(gè)人微信公眾號:猿視角(ID:iOSDevSkills),可在微信公眾號進(jìn)行留言,更多精彩技術(shù)文章,期待您的加入!一起討論,一起成長!一起攻城獅!

公眾號
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容