Block的本質與使用

一、block的基本概念及使用

  blcok是一種特殊的數據結構,它能夠保存一段代碼,等到須要的時候進行調用執行這段代碼,經常使用於GCD、動畫、排序及各種回調。html

  Block變量的聲明格式爲: 返回值類型(^Block名字)(參數列表);ios

  //聲明一個沒有傳參和返回值的blcok
    void(^myBlock1)(void) ;
    //聲明一個有兩個傳參沒有返回值的blcok 形參變量名稱能夠省略,只留有變量類型便可
    void(^myBlock2)(NSString *name,int age);
    //聲明一個沒有傳參但有返回值的blcok
    NSString *(^myBlock3)();
    //聲明一個既有返回值也有參數的blcok
    int(^myBlock4)(NSString *name);

  block的賦值: Block變量 = ^(參數列表){函數體};c++

//若是沒有參數能夠省略寫(void)
    myBlock1 = ^{
        NSLog(@"hello,word");
    };
    
    myBlock2 = ^(NSString *name,int age){
        NSLog(@"%@的年齡是%d",name,age);
    };
    
    //一般狀況下都將返回值類型省略,由於編譯器能夠從存儲代碼塊的變量中肯定返回值的類型
    myBlock3 = ^{
        return @"小李";
    };
    
    myBlock4 = ^(NSString *name){
        NSLog(@"根據查找%@的年齡是10歲",name);
        return 10;
    };

  固然也能夠直接在聲明的時候就賦值: 返回值類型(^Block名字)(參數列表) = ^(參數列表){函數體};面試

int(^myBlock5)(NSString *address,NSString *name) = ^(NSString *address,NSString *name){
         NSLog(@"根據查找家住%@的%@今年18歲了",address,name);
         return 18;
    };

  blcok的調用:Block名字();數據庫

    //沒有返回值的話直接 Block名字();調用
    myBlock1();
    
    //有參數的話要傳遞相應的參數
    myBlock2(@"校花",12);
    
    //有返回值的話要對返回值進行接收
    NSString *name = myBlock3();
    NSLog(@"%@",name);
    
    //既有參數又有返回值的話就須要即傳參數又接收返回值
    int age = myBlock5(@"河北村",@"大梨");
    NSLog(@"%d",age);

  在實際使用Block的過程當中,咱們可能須要重複地聲明多個相同返回值相同參數列表的Block變量(blcok內部執行的代碼功能不同),若是老是重複地編寫一長串代碼來聲明變量會很是繁瑣,數組

因此咱們可使用typedef來定義Block類型。網絡

#import "ViewController.h" typedef void(^commentBlock)(NSString *name,int age); @interface ViewController ()
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    commentBlock commentBlock1 = ^(NSString *name,int age){
        //這裏的操做是將age的name從數據庫中篩選出來
    };
    commentBlock commentBlock2 = ^(NSString *name,int age){
        //這裏的操做是將age的name添加到數據庫
    };
    commentBlock commentBlock3 = ^(NSString *name,int age){
       //這裏的操做是將age的name從數據庫中刪除
    };
    commentBlock1(@"li",12);
    commentBlock2(@"dong",19);
    commentBlock3(@"mi",8);
    
}
    這樣能夠減小重複代碼,避免重複用void(^commentBlock)(NSString *name,int age);聲明blcok

  上面,只是講到了blcok的一些基本使用,那麼在咱們實際開發中,block是怎麼應用的呢?其實在實際開發中把block做爲方法的參數是一種比較常見的用法,好比咱們用到的網絡請求工具.數據結構

好比,咱們舉一個block做爲參數的小例子:app

 1 #import "ViewController.h"
 2 typedef void(^BtnBlock)(void);
 3 @interface ViewController ()
 4 @property(nonatomic,weak)BtnBlock currentBlcok;
 5 @end
 6 
 7 @implementation ViewController
 8 
 9 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
10     [self alertWithBlcok:^{
11         NSLog(@"用戶點擊了肯定  在這裏能夠執行對應的操做");
12     }];
13 //    [self alertWithBlcok:nil];
14 }
15 - (void)alertWithBlcok:(BtnBlock)block{
16     _currentBlcok = block;
17     //低層最大的背景View
18     UIView *alertBgView = [[UIView alloc]initWithFrame:self.view.bounds];
19     alertBgView.tag = 99;
20     alertBgView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.73];
21     [self.view addSubview:alertBgView];
22     
23     //中間的View
24     UIView *alertCenterView = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 240, 140)];
25     alertCenterView.clipsToBounds = YES;
26     alertCenterView.layer.cornerRadius = 10;
27     alertCenterView.center = alertBgView.center;
28     alertCenterView.backgroundColor = [UIColor redColor];
29     [alertBgView addSubview:alertCenterView];
30 
31     
32     //取消按鈕
33     UIButton *cancelBtn = [[UIButton alloc]initWithFrame:CGRectMake(0, 100, alertCenterView.frame.size.width/2, 40)];
34     [cancelBtn setTitle:@"取消" forState:UIControlStateNormal];
35     cancelBtn.titleLabel.font = [UIFont systemFontOfSize:15];
36     [cancelBtn setTitleColor:[UIColor colorWithRed:51/255.0 green:51/255.0 blue:51/255.0 alpha:1.0] forState:UIControlStateNormal];
37     [cancelBtn addTarget:self action:@selector(dismissAlertView) forControlEvents:UIControlEventTouchUpInside];
38     [alertCenterView addSubview:cancelBtn];
39     
40     //短的分割線
41     UIView *shortView = [[UIView alloc]initWithFrame:CGRectMake(alertCenterView.frame.size.width/2, 110, 1, 20)];
42     shortView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.53];
43     [alertCenterView addSubview:shortView];
44     
45     //取消按鈕
46     UIButton *continueBtn = [[UIButton alloc]initWithFrame:CGRectMake(alertCenterView.frame.size.width/2, 100, alertCenterView.frame.size.width/2,40)];
47     [continueBtn setTitle:@"肯定" forState:UIControlStateNormal];
48     [continueBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
49     continueBtn.titleLabel.font = [UIFont systemFontOfSize:15];
50     [continueBtn addTarget:self action:@selector(buttonAction) forControlEvents:UIControlEventTouchUpInside];
51     [alertCenterView addSubview:continueBtn];
52 }
53 - (void)dismissAlertView{
54     [[self.view viewWithTag:99] removeFromSuperview];
55 }
56 -(void)buttonAction{
57     if (_currentBlcok) {
58         _currentBlcok();
59     }
60 }
彈框點擊

用戶點擊肯定按鈕執行的操做能夠經過block先封存起來,等用戶點擊肯定按鈕時再調用,最終實現效果:iphone

  固然blcok除了做爲方法參數外,還能夠當作屬性和返回值。

 

二、block的底層結構

  接下來咱們來看一下block到底是一個什麼樣的結構?

  經過clang命令將oc代碼轉換成c++代碼(若是遇到_weak的報錯是由於_weak是個運行時函數,因此咱們須要在clang命令中指定運行時系統版本才能編譯):

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main.cpp
-(void)viewDidLoad{
    [super viewDidLoad];
    int i = 1;
    void(^block)(void) = ^{
        NSLog(@"%d",i);
    };
    block();
}

轉換成c++代碼以下:

//block的真實結構體
struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  int i;
    //構造函數(至關於OC中的init方法 進行初始化操做) i(_i):將_i的值賦給i flags有默認值,可忽略
  __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int _i, int flags=0) : i(_i) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//封存block代碼的函數
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
  int i = __cself->i; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_3g_7t9fzjm91xxgdq_ysxxghy_80000gn_T_ViewController_c252e7_mi_0,i);
    }

//計算block須要多大的內存
static struct __ViewController__viewDidLoad_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)};

//viewDidLoad方法
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
    //定義的局部變量i
    int i = 1;
    //定義的blcok底部實現
    void(*block)(void) = &__ViewController__viewDidLoad_block_impl_0(
                                            __ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, i));
    //block的調用
    bloc->FuncPtr(block);
}

從中咱們能夠看出,定義的block實際上就是一直指向結構體_ViewController_viewDidLoad_block_impl_0的指針(將一個_ViewController_viewDidLoad_block_impl_0結構體的地址賦值給了block變量)。而這個結構體中,咱們看到包含如下幾個部分:

impl、Desc、引用的局部變量、構造方法。

而從構造方法咱們又能夠看出impl中有如下幾個成員:isa、Flags、FuncPtr,因此綜合以上信息咱們能夠知道block內部有如下幾個成員:

接下來,咱們依依來看block底層結構中這些結構體j或者參數的做用是什麼?

首先Desc:

static struct __ViewController__viewDidLoad_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)};

  desc結構體中存儲着兩個參數,reserved和Block_size,而且reserved賦值爲0而Block_size則存儲着__ViewController__viewDidLoad_block_impl_0的佔用空間大小。最終將desc結構體的地址傳入__ViewController__viewDidLoad_block_impl_0中賦值給Desc。因此Desc的做用是記錄Block結構體的內存大小

接下來,咱們來看,int i:

  i也就是咱們定義的局部變量,由於在block塊中使用到i局部變量,因此在block聲明的時候這裏纔會將i做爲參數傳入,也就說block會捕獲i。若是沒有在block中使用age,這裏將只會傳入impl,Desc兩個參數。這裏須要注意的一點是,調用block結構體的構造函數時,是將咱們定義的局部變量i的值傳進去了的,也就是構造函數實現的時候i(_i)  這部分代碼的做用就是將_i的值傳給i。其實這也就解釋清楚爲何咱們在block中沒法修改i的值了,由於block用到的i根本和咱們本身定義的i不是同一個,block內部是本身單首創建了一個參數i,而後將咱們定義的局部變量i的值賦給了本身建立的i。

最後咱們來看一下impl這個結構體:

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

咱們在block結構體的構造函數中也能夠看出這幾個成員分別有什麼做用:

  isa:指針,存放結構體的內存地址

  Flags:這個用不到 有默認值

  FuncPtr:block代碼塊地址

因此經過以上分析,咱們能夠得出如下幾個結論:

一、block本質上也是一個OC對象,它內部也有個isa指針,這一點咱們也能夠經過打印其父類是NSObject來證實;

二、block是封裝了函數調用以及函數調用環境的OC對象.(所謂調用環境就是好比block用到了變量i就把它也封裝進來了);

三、FuncPtr則存儲着viewDidLoad_block_func_0函數的地址,也就是block代碼塊的地址。因此當調用block的時候,bloc->FuncPtr(block);是直接調用的FuncPtr方法。

四、impl結構體中isa指針存儲着&_NSConcreteStackBlock地址,能夠暫時理解爲其類對象地址,block就是_NSConcreteStackBlock類型的。

五、Desc存儲__viewDidLoad_block_impl_0結構體所佔用的內存大小,也就是存儲着Block的內存大小。

咱們 能夠用一張圖來表示各個結構體之間的關係:

再簡單化就是:(網絡圖片 可是原理同樣)

block底層的數據結構也能夠經過一張圖來展現(variables就是結構體中所引用的變量,invoke就是上面的FuncPtr也就是block封裝代碼的函數地址,這個圖是根據上面的分析能夠總結出):

1.isa指針,全部對象都有該指針,用於實現對象相關的功能。
2.flags,用於按bit位表示一些block的附加信息,block copy的實現代碼能夠看到對該變量的使用。
3.reserved,保留變量。
4.invoke,函數指針,指向具體的Block實現的函數調用地址。就是FuncPtr 5.descriptor,表示該Block的附加描述信息,主要是size大小,以及copy和dispose函數的指針。
6.variables,截取過來的變量,Block可以訪問它外部的局部變量,就是由於將這些變量(或變量的地址)複製到告終構體中。

 

三、blcok變量捕獲

咱們定義了幾個變量:全局變量name、局部變量-auto變量:i,obj、局部變量-靜態變量:height,分別在blcok內部修改和訪問↓↓

  

  咱們發如今block內部均可以訪問這些變量,可是沒法修改局部變量中的auto變量,沒法修改的緣由咱們在上面的分析中也能夠看出,是由於block內部本身建立了對應的變量,外部auto變量只是將值傳遞到block內賦給block建立的內部變量。block內部存在的只是本身建立的變量並不存在block外部的auto變量,因此沒辦法修改。

  可是爲何全局變量和靜態變量就能夠訪問呢?咱們將oc代碼轉換成c++代碼來看

 

發現,block內部雖然訪問了四個變量,可是其底層只捕獲了三個變量,並沒有捕獲全局變量name

並且比較局部變量中的auto變量和靜態變量,發現blcok底層捕獲auto變量時是捕獲的其值,而捕獲靜態變量時是捕獲的變量地址(i是值, *height是地址),這也就是爲何咱們能夠在block修改靜態變量,由於blcok內修改的靜態變量其實和blcok外的靜態變量是同一個內存地址,同一個東西。

  關於auto變量obj,blcok內部也是捕獲它的值,不要由於它有*就以爲捕獲的是地址,由於obj自己就是個對象,自己就是地址,若是block捕獲的是obj的地址的話應該是NSObject **obj 即指向指針的地址。

因此咱們經過上面這個例子能夠總結出:

  

爲何會出現這種區別呢?

首先,爲何捕獲局部變量而不捕獲全局變量這個問題很好理解:

  全局變量整個項目均可以訪問,block調用的時候能夠直接拿到訪問,不用擔憂變量被釋放的狀況;

  而局部變量則不一樣,局部變量是有做用域的,若是blcok調用的時候blcok已經被釋放了,就會出現嚴重的問題,因此爲了不這個問題block須要捕獲須要的局部變量。好比咱們局部變量和block都卸載了viewDidLoad方法,可是我在touchesBegan方法中調用block,這個時候局部變量早就釋放了,因此block要捕獲局部變量)

接下來,爲何auto變量是捕獲的值,而靜態變量是捕獲的地址呢?

  這是由於自動變量和靜態變量存儲的區域不一樣,二者釋放時間也不一樣。

  咱們在關於局部變量、全局變量的分析中講到了自動變量是存放在棧中的,建立與釋放是由系統設置的,隨時可能釋放掉,而靜態變量是存儲在全局存儲區的,生命週期和app是同樣的,不會被銷燬。因此對於隨時銷燬的自動變量確定是把值拿進來保存了,若是保存自動變量的地址,那麼等自動變量釋放後咱們根據地址去尋值確定會發生懷內存訪問的狀況,而靜態變量由於項目運行中永遠不會被釋放,因此保存它的地址值就徹底能夠了,等須要用的時候直接根據地址去尋值,確定能夠找到的。

那麼,又有一個問題了,爲何靜態變量和全局變量一樣不會被銷燬,爲何一個被捕獲地址一個則不會被捕獲呢

  我我的以爲是靜態變量和全局變量由於二者訪問方式不一樣形成的,咱們都知道全局變量整個項目均可以拿來訪問,因此某個全局變量在全局而言是惟一的(也就是全局變量不能出現同名的狀況,即便類型不一樣也不行,不然系統不知道你具體訪問的是哪個)而靜態變量則不是,全局存儲區可能存儲着若干個名爲height的靜態變量。

  因此這就致使了訪問方式的不一樣,好比說有個block,內部有一個靜態變量和一個全局變量,那麼在調用的時候系統能夠直接根據全局變量名去全局存儲區查找就能夠找到,名稱是唯一的,因此不用捕獲任何信息便可訪問。而靜態變量而不行,全局存儲區可能存儲着若干個名爲height的靜態變量,因此blcok只能根據內存地址去區分調用本身須要的那個。

  我以前有個想法:是否是由於二者訪問範圍不一樣,全局變量能夠全局訪問,靜態變量只能當前文件訪問。但仔細想一想block即便不是在當前文件調用的,但它的具體執行代碼塊內代碼確定是在當前文件執行的,也就是blcok內部訪問變量不存在跨文件訪問的狀況,既然二者均可以訪問到那麼訪問範圍就不是緣由了。

 

-(void)viewDidLoad{
    [super viewDidLoad];
    void(^block)(void) = ^{
        NSLog(@"%@",self);
    };
    block();
    
    void(^block2)(void) = ^{
        NSLog(@"%@",self.address);
    };
    block2();
}

 

上面這段代碼中的block是怎麼捕獲變量的呢?

咱們轉換成c++代碼能夠看出,咱們能夠看到viewdidload其實是轉換成了 void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd)

也就是這個方法轉換爲底層實現時是有兩個參數的:self和_cmd,既然self是方法的參數,那麼self確定是個局部變量,又由於這個局部變量並無static修飾,因此self應該會被捕獲而且是值傳遞

在訪問實例對象(self)的屬性(address),咱們發現block並無捕獲這個具體的屬性而是捕獲的實例對象(self),這是由於經過self就能夠獲取到這個實例對象的屬性,捕獲一個實例對象就夠了,而在block內部使用這個屬性的時候,也是經過實例對象來獲取的↓↓

 

四、blcok的類型

咱們在分析block底層結構的時候,看到了isa存儲的是&_NSConcreteStackBlock地址,也就是這個block是個block類型的,那麼blcok只有這一種類型嗎?

答案是否認的,blcok有三種類型,咱們能夠經過代碼來驗證:

有的同窗可能將oc轉爲c++↓↓發現oc代碼編譯後三個block都是StackBlock類型的,和咱們剛纔打印的不同。這是由於runtime運行時過程當中進行了轉變。最終類型固然以runtime運行時類型也就是咱們打印出的類型爲準。

既然存在三種不一樣的類型,那系統是根據什麼來劃分block的類型的呢?不一樣類型的block分別存儲在哪呢?

也就是根據兩點:有沒有訪問auto變量、有沒有調用copy方法:

而這三種變量存放在內存中的位置也不一樣:__NSMallocBlock__是在平時編碼過程當中最常使用到的。存放在堆中須要咱們本身進行內存管理。

關於判斷類型的兩個條件,咱們第一個條件也就是判斷有誤訪問auto變量這個是明白的,可是第二個就不太清楚了,調用copy有什麼用?作了哪些操做了?

__NSGlobalBlock __ 調用copy操做後,什麼也不作
__NSStackBlock __ 調用copy操做後,複製效果是:從棧複製到堆;副本存儲位置是堆
__NSMallocBlock __ 調用copy操做後,複製效果是:引用計數增長;副本存儲位置是堆

也就是:

因爲ARC環境下,系統會對一下狀況下的block自動作copy處理:

//1.block做爲函數返回值時
typedef void (^Block)(void);
Block myblock()
{
    int a = 10;
    //此時block類型應爲__NSStackBlock__
    Block block = ^{
        NSLog(@"---------%d", a);
    };
    return block;
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block = myblock();
        block();
       // 打印block類型爲 __NSMallocBlock__
        NSLog(@"%@",[block class]);
    }
    return 0;
}

//2.將block賦值給__strong指針時,好比(arc中默認全部對象都是強指針指引)
 void (^block1)(void) = ^{ 
    NSLog(@"Hello");
}; 
//3.block做爲Cocoa API中方法名含有usingBlock的方法參數時。例如:遍歷數組的block方法,將block做爲參數的時候。 NSArray *array = @[]; [array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { }]; //4.block做爲GCD API的方法參數時 例如:GDC的一次性函數或延遲執行的函數,執行完block操做以後系統纔會對block進行release操做。 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ }); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ });

因此咱們關閉ARC才能更好的看清楚copy的做用:

project -> Build settings -> Apple LLVM complier 3.0 - Language -> objective-C Automatic Reference Counting設置爲NO

而後咱們定義幾個不一樣類型的block,並分別調用copy方法查看結果:

發現block的copy方法確實是有這樣的做用,須要說明的一點,block3雖然屢次copy後打印出來的retainCount始終是1,但其內存管理器中仍然會增長。

既然copy方法最大的一個做用是把block從棧拷貝到堆,它這樣作的緣由是什麼?block在棧中和堆中有什麼區別嗎?

  咱們都知道棧中對象的內存空間是隨時可能被系統釋放掉的,而堆中的內存空間是由開發者維護的,若是block存在於棧中就能夠出現一個問題,當咱們調用block的時候,他被釋放掉了,從而出現錯誤:

咱們發現,當咱們調用block的時候打印出來莫名其妙的東西,這是由於test方法執行完後,棧內存中block所佔用的內存已經被系統回收,所以就有可能出現亂得數據。

  可能有的同窗會根據上面block訪問auto變量的想法來思考,blcok不是已經捕獲了這個變量了麼,其實這徹底是兩碼事,block確實是把變量a捕獲到了本身內部,可是如今它本身的空間都被釋放掉了,更不用說它捕獲的變量了,確定被釋放掉了。

因此,這種狀況下,是須要將block移到堆上面的,讓開發者控制它的生命週期,這就用到了copy(arc環境下不用,由於test方法中將block賦值給了一個__strong指針,會生成copy)

既然blcok存放在堆中了,block內部有捕獲了a的值,因此就能夠正常輸出了。

咱們從這種狀況也能夠知道爲何不一樣環境下block的聲明屬性寫法的不一樣:

MRC下block屬性的建議寫法:

@property (copy, nonatomic) void (^block)(void);

ARC下block屬性的建議寫法:

@property (strong, nonatomic) void (^block)(void);

@property (copy, nonatomic) void (^block)(void);

copy屬性意味着,系統會自動對修飾的block進行一次copy操做,

因此在mrc環境下,copy屬性修飾block就不會出現上面block存在棧裏,在訪問時被釋放的狀況;

而在arc環境下,系統會在block被__strong指針引用時自動執行copy方法,因此就能夠寫strong和copy兩種。

 

五、__block

既然block內部能夠修改靜態變量和全局變量的值,而沒法修改自動變量的值,那麼有沒有什麼方式能夠解決這個問題呢?

答案是確定的,咱們能夠經過_ _block修飾這個自動變量,從而能夠在block內部訪問並修改這個自動變量了:

__block不能修飾全局變量、靜態變量(static)

 

那麼,_ _block 這個修飾符作了什麼操做呢?就讓可讓block內部能夠訪問自動變量

咱們經過底層代碼能夠看出,__weak將int類型的數據轉換成了一個__Block_byref_i_0的結構體類型

而這個結構體的結構是:

struct __Block_byref_i_0 {
  void *__isa;
__Block_byref_i_0 *__forwarding;
 int __flags;
 int __size;
 int i;
};

 而從賦值上看,isa爲0,既然有isa指針,那麼說明這個結構體也是一個對象,__forwarding存儲的是__Block_byref_i_0的地址值,flags爲0,size爲Block_byref_i_0的內存大小,i是真正存儲變量值的地方,其內部結構就是這樣的↓↓(關於des結構體爲何會多了一個copy函數和一個dispose函數在下面相互引用中會講到)

而當咱們在block內部修改和訪問這個變量是,底層實現是這樣的:

是經過__Block_byref_i_0結構體的指針__forwarding讀取和修改的變量i.

爲何要經過__forwarding轉一下呢,而不是直接讀取i

這是由於當咱們調用block的時候,block可能存在於棧中可能存在於堆中,

void (^block)(void);
void test()
{
    // __NSStackBlock__
    int a = 10;
    block = ^{
        NSLog(@"block---------%d", a);
    } ;

    //狀況一:此時調用block還存在與棧中
    block();
    //此時block有兩份 一個在棧中 一個在堆中
   [blcok copy];
    //一次copy對應一次release
    [block release];
    
    //這個方法執行完後雖然棧中的block釋放了 可是已經拷貝到堆裏一份,因此仍是能夠繼續調用的
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        //狀況二:test方法執行完後 棧中的block被釋放了 堆中還有一個copy的block
        block();
    }
    return 0;
}

__forwarding指向示意圖:

  若是是直接經過結構體的內存地址訪問變量,由於結構體在堆中的地址和在棧中的地址確定不同,狀況一和狀況二很明顯又是執行的同一個方法,因此就沒有辦法實現這個功能,也就是若是方法裏是根據棧中的地址訪問屬性的,那麼狀況二就會出錯,由於這個時候這個地址已經被釋放了,若是是根據堆中的值去訪問變量的話,那麼狀況一又有問題了,由於這個時候堆裏尚未這個block呢。因此須要根據__forwarding指針去訪問變量,這樣的話才能確保狀況一和狀況二都會訪問到這個結構體。

因此咱們總結一下上面的分析:

1.__block將int i進行包裝,包裝成一個__Block_byref_i_0結構體對象,結構體中的i是存儲i的int值的;

2.當咱們在block內修改或訪問該對象時,是經過該對象的__forwarding去找對應的結構體再找對應的屬性值,這是由於__forwarding在不一樣狀況下指向不一樣的地址,防止只根據單一的一個內存地址出現變量提早釋放沒法訪問的狀況。

 

那麼咱們就明白爲何能夠修改__block修飾的自動變量了,由於__block修飾變量變成了一個對象,咱們修改只是修改的這個對象中的一個屬性,並非修改的這個對象:就像這樣↓↓

__block修飾下的i再也不是int類型而變成一個對象(對象p),咱們block內部訪問修改和訪問的是這個對象內部的int i(字符串p),因此是能夠修改訪問的。只不過這個轉化爲對象的內部過程封裝起來不讓開發者看到,因此就給人的感受是能夠修改auto變量也就是修改時是int i。

block外部訪問__block修飾的變量也是經過__forwarding指針找到結構體對象內部的int i,既然是訪問你的block內部的屬性i,那麼就是修改後的21了↓↓

 

六、blcok的內存管理

上面咱們講到了,__block是將int類型的數據包裝成了一個對象,而後block內部捕獲這個對象訪問或者修改對象內部的int屬性,那麼block對其捕獲的對象變量是怎麼引用怎麼管理的呢? 

當block在棧上時,不會對指向的對象產生強引用

當block被copy到堆時
    會調用block內部的copy函數
    copy函數內部會調用_Block_object_assign函數
    _Block_object_assign函數會根據所指向對象的修飾符(__strong、__weak)作出相應的操做,造成強引用或者弱引用

若是block從堆上移除
    會調用block內部的dispose函數
    dispose函數內部會調用_Block_object_dispose函數
    _Block_object_dispose函數會自動釋放指向的對象(release)

也就是block對其捕獲的對象時強引用仍是弱引用,主要看block存在於哪?

若是在棧上,那麼對捕獲的對象一概不會產生強引用;

若是在堆上的話,這個要看這個對象自身的修飾符了,本身的修飾符是strong那就是強引用,是weak的話那就是弱引用(oc中的指針默認爲strong,除非指定爲weak)

這裏的copy函數和dispose函數就是咱們在解析__block中看到block結構體內存描述結構體(desc)中的多出來的那兩個函數:

咱們看到copy函數內部調用的是_Block_object_assign函數,assign函數傳遞了三個參數:對象p的地址,對象p以及3,這個函數的做用就是根據person對象是什麼類型的指針,對person對象產生強引用或者弱引用。

  能夠理解爲_Block_object_assign函數內部會對person進行引用計數器的操做,若是結構體內person指針是__strong類型,則爲強引用,引用計數+1,若是結構體內person指針是__weak類型,則爲弱引用,引用計數不變。

dispose函數內部調用的是_Block_object_dispose函數,dispose函數傳遞了兩個參數,對象p和3,這個函數的做用就是對person對象作釋放操做,相似於release,也就是斷開對person對象的引用,而person到底是否被釋放仍是取決於person對象本身的引用計數。

下面咱們經過圖片來形象化地看一下這個流程:

當block從棧copy到堆的時候回自動執行des結構體重的copy函數:

當block從堆中被釋放的時候,會調用dispose函數釋放捕獲的變量:

 

六、循環引用問題

循環引用也是block中一個常見的問題,什麼是循環引用呢?

  咱們從上面block捕獲對象變量的分析能夠看出,block在堆中的時候會根據變量本身的修飾符來進行強引用或者弱引用,假設block對person對象進行強引用,而person若是對block也進行強引用的話,那就造成了循環引用,person對象和block都有強指針指引着,使它們得不到釋放:

 咱們發現當最後一行代碼執行完後,dealloc並無執行,也就是person並無被釋放,這就是由於循環引用。

  block是p對象的一個屬性,因此p對block是一個強引用關係,而block內部又捕獲了p對象,p默認是強指針的,因此block對p也是一個強引用,雙方就造成了這樣一個關係:

  當block調用完後,系統對block說:用完了你就釋放吧,block說我如今釋放不了,由於person對象還要用我呢。而後系統又找到person對象說你先釋放吧,person說我也是放不了,由於block裏面還要用我呢。就這樣,person和block都雙雙沒法釋放。

  那麼怎麼解決這個循環引用呢?很簡單,只要把任意一根紅線設置爲弱引用就行,好比說這樣↓↓

這樣的話就要對代碼這樣修改:

@property(nonatomic,weak)MyBlock block;

測試發現確實被釋放了,可是這種方案不太合理,由於咱們前面講到了,block最好是放在棧中去操做,在arc中修飾符應該是strong或者copy,因此咱們須要再換種方案。

這樣的話就須要對代碼這樣修改了:

    person *p = [[person alloc]init];
    __weak person *weakP = p;
    p.block = ^{
        NSLog(@"%@",weakP);
    };
    p.block();

測試發現,block和person對象都被釋放了。

那麼除了這個方案還有其餘方法麼?答案是有的,下面是循環引用的結構↓↓只要這三條線有一條是弱引用就不會發生循環引用的狀況。

首先,①這個是沒有辦法改成弱引用的,由於block要copy到堆中就得用strong或者copy修飾,不能用weak;

咱們經過__weak是將②變爲弱引用,固然除了__weak,咱們也能夠用__unsafe_unretained:

__unsafe_unretained和__weak同樣,表示的是對象的一種弱引用關係。

  惟一的區別是:__weak修飾的對象被釋放後,指向對象的指針會置空,也就是指向nil,不會產生野指針;而__unsafe_unretained修飾的對象被釋放後,指針不會置空,而是變成一個野指針,那麼此時若是訪問這個對象的話,程序就會Crash,拋出BAD_ACCESS的異常。
 __weak person *weakP = p;
    __unsafe_unretained person *unsafeP = p;
    //當p釋放後,weakp會自動指向nil 而unsafeP則不會,會繼續指向對象的地址,對象已經銷燬,此時unsafeP訪問的是"殭屍"對象

那麼咱們還有一種辦法能夠解決循環引用,那就是將③在block執行完後手動釋放:經過__block

    __block person *p = [[person alloc]init];
    p.block = [^{
        NSLog(@"%@",p);
        p = nil;
    }copy];
    
    p.block();

可是這個方案的話必需要求調用block,不調用的話p=nil不會執行,也就是③這條強引用仍是存在的。

因此,咱們能夠總結一下解決循環引用的方案:

ARC環境:

1.用__weak、__unsafe_unretained解決;

2.用__block解決(必需要調用block)

MRC環境:

1.用__unsafe_unretained解決;

2.用__block解決(必需要調用block)。

 

最後再提一個面試中常常問道的問題:block能夠給NSMutableArray中添加元素嗎,需不須要添加__block?

答案是不須要,下面經過代碼驗證。

由於在block塊中僅僅是使用了array的內存地址,往內存地址中添加內容,並無修改arry的內存地址,所以array不須要使用__block修飾也能夠正確編譯。

 

下面這兩篇文章對block本質有着詳細的介紹,同窗們也能夠看一下↓↓

參考資料

參考資料

相關文章
相關標籤/搜索