Block的本質與使用


1、block的基本概念及使用

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

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

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

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

//如果沒有參數可以省略寫(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作為參數的小例子:

 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先封存起來,等用戶點擊確定按鈕時再調用,最終實現效果:

  當然blcok除了作為方法參數外,還可以當做屬性和返回值。

 

2、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代碼塊地址

所以通過以上分析,我們可以得出以下幾個結論:

1、block本質上也是一個OC對象,它內部也有個isa指針,這一點我們也可以通過打印其父類是NSObject來證明;

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

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

4、impl結構體中isa指針存儲着&_NSConcreteStackBlock地址,可以暫時理解為其類對象地址,block就是_NSConcreteStackBlock類型的。

5、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能夠訪問它外部的局部變量,就是因為將這些變量(或變量的地址)復制到了結構體中。

 

3、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內部使用這個屬性的時候,也是通過實例對象來獲取的↓↓

 

4、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兩種。

 

5、__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了↓↓

 

6、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函數釋放捕獲的變量:

 

6、循環引用問題

循環引用也是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本質有着詳細的介紹,同學們也可以看一下↓↓

參考資料

參考資料

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM