探究 Block 的奧秘

閑來無事,總結(jié)了一下 block 的幾點(diǎn)知識(shí),以作鞏固,歡迎指正。

一、block 的本質(zhì)
block 本質(zhì)上是一個(gè) OC 對(duì)象,它內(nèi)部有一個(gè) isa 指針。
block 是封裝了函數(shù)調(diào)用以及函數(shù)環(huán)境的 OC 對(duì)象。

要研究 block 在底層編譯器具體的源碼實(shí)現(xiàn)方式,可以使用 llvm 編譯器中的 clang 命令clang -rewrite-objc main.m查看,它會(huì)將 OC 的源碼 main.m 文件改寫成 C++ 的 main.cpp 文件(但只可作為參考,因?yàn)?llvm 編譯器生成的中間文件和 C++ 文件還是有所差異的)。更加完整的命令是:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m,感興趣的可以試一試。
從源碼中可以發(fā)現(xiàn),block 在轉(zhuǎn)換成 C++ 的時(shí)候顯示為:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;  //block中調(diào)用的函數(shù)的地址
};

// block的描述信息
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;  //block所占的內(nèi)存大小
}

// block的源碼結(jié)構(gòu)
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc; 
};

顯而易見,block 結(jié)構(gòu)體的第一個(gè)元素是結(jié)構(gòu)體 __block_impl,而 __block_impl 結(jié)構(gòu)體的第一個(gè)元素是 isa,由此可知 block 實(shí)際上是一個(gè) OC 對(duì)象。
其次,我們都知道 OC 對(duì)象都有其類型,可調(diào)用-class方法查看對(duì)象所屬的類。

void (^block)(void) = ^{
  NSLog(@"hello");
};
NSLog(@"%@", [block class]);
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);

控制臺(tái)打印結(jié)果如下:
image.png

明顯看出,__NSGlobalBlock __ 繼承自__NSGlobalBlock,__NSGlobalBlock 繼承自 NSBlock,NSBlock 繼承自NSObject,即 block 所屬的類最終是繼承自 NSObject 的,所以說 block 是 OC 對(duì)象。

二、block分類
OC 中有3種類型的 block,可以通過調(diào)用 class 方法或者 isa 指針查看具體類型,最終都是繼承自 NSBlock 類。
1、_NSConcreteGlobalBlock 全局的靜態(tài) block,在常量區(qū)(數(shù)據(jù)區(qū)域),不會(huì)訪問任何外部變量。
2、_NSConcreteStackBlock 保存在棧中的 block,當(dāng)函數(shù)返回時(shí)會(huì)被銷毀。
3、_NSConcreteMallocBlock 保存在堆中的 block,動(dòng)態(tài)分配內(nèi)存,當(dāng)引用計(jì)數(shù)為 0 時(shí)會(huì)被銷毀。

// _NSConcreteGlobalBlock:沒有訪問auto變量
void (^block)(void) = ^{
  NSLog(@"hello");
};
NSLog(@"%@", [block class]); //輸出__NSGlobalBlock__

// _NSConcreteStackBlock :訪問了auto變量
int a = 10;
NSLog(@"%@", [^{
  NSLog(@"hello %d", a);
} class]); //輸出__NSStackBlock__

// _NSConcreteMallocBlock:__NSStackBlock__調(diào)用copy方法
int a = 10;
void (^block)(void) = [^{
  NSLog(@"hello %d", a);
} copy];
NSLog(@"%@", [block class]);//輸出__NSMallocBlock__

每一種類型的 block,調(diào)用 copy 后的結(jié)果如下所示:
image.png

注意!在ARC環(huán)境下,編譯器會(huì)根據(jù)情況自動(dòng)將棧上的 block 復(fù)制到堆上,比如以下情況:
1、block 作為函數(shù)返回值時(shí)
2、將 block 賦值給 __strong 指針時(shí)
3、block 作為 Cocoa API 中方法名含有 usingBlock 的方法參數(shù)時(shí)
4、block 作為 GCD API 的方法參數(shù)時(shí)

當(dāng) block 內(nèi)部訪問了對(duì)象類型的 auto 變量的時(shí)候,會(huì)產(chǎn)生兩種可能:
1、如果 block 是在棧上,將不會(huì)對(duì) auto 變量產(chǎn)生強(qiáng)引用;
2、如果 block 被拷貝到堆上,會(huì)調(diào)用 block 內(nèi)部的copy函數(shù),copy函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_assign函數(shù),_Block_object_assign函數(shù)會(huì)根據(jù) auto 變量的修飾符(__strong、__weak、__unsafe_unretained)操作,類似于 retain (形成強(qiáng)引用、弱引用)。
如果 block 從堆上移除,會(huì)調(diào)用 block 內(nèi)部的dispose函數(shù),dispose函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_dispose函數(shù),_Block_object_dispose函數(shù)會(huì)自動(dòng)釋放引用的 auto 變量,類似于 release。

三、block 的變量捕獲
問題1:下面的代碼輸出結(jié)果為何?

int a = 10;
void (^block)(void) = ^{
  NSLog(@"a = %d", a);
};
a = 20;
block();

通過終端命令,可以查看到編譯后的最終代碼如下:

// 當(dāng)前block的結(jié)構(gòu)
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

int a = 10;
//此時(shí)傳入的參數(shù)a=10,通過__main_block_impl_0函數(shù)傳入的最后一個(gè)參數(shù)為a,被block結(jié)構(gòu)體內(nèi)部的int a;元素所捕獲,故此block結(jié)構(gòu)體內(nèi)部的a元素保存的是傳入的參數(shù)a的值
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
//再修改外部參數(shù)a=20也無用,因?yàn)檫@并不影響block結(jié)構(gòu)體內(nèi)部a元素的值
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

在源碼的注釋中已經(jīng)解釋了實(shí)際的執(zhí)行邏輯,故此可以看出輸出的結(jié)果為:
image.png

問題2:若將上述問題中的int a = 10;修改成static int a = 10;,結(jié)果是否會(huì)有所不同?
同上,粘代碼便一目了然(截取部分 OC 代碼與 C++ 代碼)。

/***** 編譯前的OC代碼 *****/
static int a = 10;
void (^block)(void) = ^{
  NSLog(@"a = %d", a);
};
a = 20;
block();

/***** 編譯后的C++代碼 *****/
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static int a = 10;
//此時(shí)傳入的參數(shù)a=10,通過__main_block_impl_0函數(shù)傳入的最后一個(gè)參數(shù)為&a,被block結(jié)構(gòu)體內(nèi)部的int *a;元素所捕獲,故此block結(jié)構(gòu)體內(nèi)部的指針a元素保存的是傳入的參數(shù)a的地址值
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a));
//再修改外部參數(shù)a=20,在調(diào)用block函數(shù)時(shí),因其結(jié)構(gòu)體內(nèi)部a元素保存的是外部參數(shù)a的地址值,此時(shí)該地址值所指向的外部參數(shù)a=20
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

在源碼的注釋中已經(jīng)解釋了實(shí)際的執(zhí)行邏輯,故此可以看出輸出的結(jié)果為:
image.png

問題3:若將上述問題中的int a = 10;聲明為全局變量,結(jié)果又是否會(huì)有所不同?
同上,粘代碼便一目了然(截取部分 C++ 代碼)。

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

void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

很明顯,block 結(jié)構(gòu)體內(nèi)部并沒有捕獲全局變量a的值或者地址,而是直接訪問全局變量的,故此可以看出輸出的結(jié)果為:
image.png

總結(jié):
為了保證 block 內(nèi)部能夠正常訪問外部的變量,block 有個(gè)變量捕獲機(jī)制:
全局變量:不捕獲到 block 內(nèi)部,直接訪問
局部變量:捕獲到 block 內(nèi)部,其中auto變量通過值傳遞訪問,static變量通過指針傳遞訪問

備注:auto自動(dòng)變量,離開作用域就會(huì)被銷毀。

四、__block 的作用

int age = 10;
void (^block)(void) = ^{
  NSLog(@"age is %d", age);
};
block();

毫無疑問,控制臺(tái)會(huì)輸出 “age is 10”。如果想在 block 代碼塊中修改局部變量 age 的值為20,該怎么做?
直接修改肯定會(huì)報(bào)錯(cuò),當(dāng)然可以直接將 age 定義為 static 變量或者全局變量,問題就會(huì)迎刃而解。但是這樣的話,age 就會(huì)一直存在在內(nèi)存的全局區(qū)中,這并不是最理想的狀態(tài),希望 age 還是一個(gè)臨時(shí)變量,在不用的時(shí)候會(huì)自動(dòng)銷毀。這時(shí)候可以使用 __block 修飾 age 即可。

__block int age = 10;
void (^block)(void) = ^{
  age = 20;
  NSLog(@"age is %d", age);
};
block();

此時(shí)控制臺(tái)會(huì)輸出 “age is 20”。用 __block 的好處是不會(huì)修改變量的性質(zhì),age 還是一個(gè) auto 類型的自動(dòng)變量。

__block 可以用來用于解決 block 內(nèi)部無法修改 auto 變量值的問題。
__block 不能修飾全局變量、靜態(tài)變量(static)。
編譯器會(huì)將 __block 變量包裝成一個(gè)對(duì)象。
上面的代碼中__block int age = 10;會(huì)編譯成下面的代碼結(jié)構(gòu):

struct __Block_byref_age_0 {
  void *__isa;
  __Block_byref_age_0 *__forwarding; //指向結(jié)構(gòu)體自身的指針
  int __flags;
  int __size;
  int age; //這個(gè)age才是對(duì)應(yīng)外部 __block 修飾的局部變量,共享同一塊內(nèi)存地址
};

//block 內(nèi)部有個(gè) *age 指針,指向 __Block_byref_age_0 結(jié)構(gòu)體,__Block_byref_age_0 結(jié)構(gòu)體內(nèi)部有 int age ;通過 __Block_byref_age_0 結(jié)構(gòu)體的 __forwarding 指針找到其內(nèi)部 age 的內(nèi)存地址,并修改其值,完成修改。
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; // by ref
};

當(dāng) block 內(nèi)部訪問了 __block 變量的時(shí)候,會(huì)產(chǎn)生兩種可能:
1、如果 block 在棧上時(shí),并不會(huì)對(duì) __block 變量產(chǎn)生強(qiáng)引用。
2、如果 block 被拷貝到堆上時(shí),會(huì)調(diào)用 block 內(nèi)部的copy函數(shù),copy函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_assign函數(shù),_Block_object_assign函數(shù)會(huì)對(duì) __block 變量形成強(qiáng)引用(retain)。
當(dāng) block 從堆中移除時(shí),會(huì)調(diào)用 block 內(nèi)部的dispose函數(shù),dispose函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_dispose函數(shù),_Block_object_dispose函數(shù)會(huì)自動(dòng)釋放引用的 __block 變量(release)。

同時(shí),被 __block 修飾的對(duì)象類型也會(huì)產(chǎn)生兩種可能:
1、當(dāng) __block 變量在棧上時(shí),不會(huì)對(duì)指向的對(duì)象產(chǎn)生強(qiáng)引用。
2、當(dāng) __block 變量被拷貝到堆上時(shí),會(huì)調(diào)用 __block 變量 內(nèi)部的copy函數(shù),copy函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_assign函數(shù),_Block_object_assign函數(shù)會(huì)根據(jù)所指向?qū)ο蟮男揎椃╛_strong、__weak、__unsafe_unretained)做出相應(yīng)的操作,形成強(qiáng)引用(retain)或者弱引用(注意:這里僅限于 ARC 時(shí)會(huì) retain,MRC 時(shí)不會(huì) retain)。
當(dāng) __block 變量從堆中移除時(shí),會(huì)調(diào)用 __block 變量?jī)?nèi)部的dispose函數(shù),dispose函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_dispose函數(shù),_Block_object_dispose函數(shù)會(huì)自動(dòng)釋放指向的對(duì)象(release)。

五、block 的使用
1、作為對(duì)象的屬性
用 block 作為屬性,保存一段代碼塊,可以在任何想用它的地方隨時(shí)使用。MVVM 設(shè)計(jì)模式中,在綁定 view 和 VM 的時(shí)候就常用到。

#import <Foundation/Foundation.h>
@interface Person : NSObject
// ARC下block可以用strong修飾,MRC下用copy修飾
@property (nonatomic, strong) void(^myName)(NSString * name);
@end

ViewController中使用即可:

- (void)viewDidLoad {
    [super viewDidLoad];
    Person * p = [[Person alloc] init];
    p.myName = ^(NSString *name) {
        NSLog(@"你好,我的名字叫%@", name);
    };
    p.myName(@"Rac");
}

打印結(jié)果如下:
image.png

2、作為方法的參數(shù)

#import <Foundation/Foundation.h>
@interface Person : NSObject
- (void)eat:(void (^)(void))block;
@end

#import "Person.h"
@implementation Person
- (void)eat:(void (^)(void))block {
    //1、保存這個(gè)block
    //2、做特定的事情
    //3、得到一個(gè)結(jié)果,將結(jié)果給block
    block();
}
@end

ViewController中調(diào)用即可:

- (void)viewDidLoad {
    [super viewDidLoad];
    Person * p = [[Person alloc] init];
    [p eat:^{
        NSLog(@"我要吃東西");
    }];
}

打印結(jié)果如下:
image.png

3、作為方法的返回值
block作為方法的返回值,可以通過點(diǎn)語法進(jìn)行調(diào)用,進(jìn)而實(shí)現(xiàn)鏈?zhǔn)骄幊坦δ埽覀兂S玫淖詣?dòng)布局第三方Mansory的實(shí)現(xiàn)就是這個(gè)原理。

#import <Foundation/Foundation.h>
@interface Person : NSObject
//打點(diǎn)調(diào)用,必須是getter方法,滿足兩點(diǎn):1、有返回值;2、沒有參數(shù)
- (void(^)(int))run;
// 持續(xù)打點(diǎn)調(diào)用(即鏈?zhǔn)骄幊蹋琤lock返回當(dāng)前類的對(duì)象即可
- (Person *(^)(int))runAgain;
@end

#import "Person.h"
@implementation Person
- (void (^)(int))run {
    return ^(int m){
        NSLog(@"我跑了%d米", m);
    };
}
- (Person *(^)(int))runAgain {
    return ^(int m){
        NSLog(@"我這次又跑了%d米", m);
        return self;
    };
}
@end

ViewController中調(diào)用即可:

- (void)viewDidLoad {
    [super viewDidLoad];
    Person * p = [[Person alloc] init];
    p.run(10);
//    p.run(10)相當(dāng)于:
//    void (^blocka)(int m) = p.run;
//    blocka(10);
    p.runAgain(10).runAgain(20).runAgain(30);
}

打印結(jié)果如下:
image.png

六、block 循環(huán)引用問題
循環(huán)引用,肯定大家都知道的,通俗點(diǎn)說,就是A持有B,B持有A,互相強(qiáng)引用,導(dǎo)致雙方(或多方)都無法正常釋放,從而引起內(nèi)存泄漏。
在這里提供幾種解決block循環(huán)引用的方法,當(dāng)然還有其他的解決辦法,原則就一個(gè):打破這個(gè)循環(huán)鏈!
1、__weak
MRC 環(huán)境下,是不支持 __weak的。

//__weak:不會(huì)產(chǎn)生強(qiáng)引用,指向的對(duì)象銷毀時(shí),會(huì)自動(dòng)讓指針置為nil
__weak typeof (self) weakself = self;
self.block = ^(){
  NSLog(@"%@", weakself);
};

2、__unsafe_unretained

//__unsafe_unretained:不會(huì)產(chǎn)生強(qiáng)引用,不安全,指向的對(duì)象銷毀時(shí),指針存儲(chǔ)的地址值不變,容易造成野指針
__unsafe_unretained id unself = self;
self.block = ^(){
  NSLog(@"%@", unself);
};

3、__block
注意:必須要調(diào)用 block,而且 blockSelf 在代碼塊中用完要置為nil

__block id bkself = self;
self.block = ^{
  NSLog(@"%@", bkself);
  bkself = nil;
};
self.block();

4、self作為block的參數(shù)傳遞進(jìn)去

self.block = ^(id cself) {
  NSLog(@"%@", cself);
};

檢測(cè)是否解決循環(huán)引用問題,只需查看dealloc方法是否被調(diào)用即可,因?yàn)楫?dāng)前對(duì)象被銷毀時(shí)肯定會(huì)走dealloc方法,大家可自行測(cè)試。

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

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